const $ = (id) => document.getElementById(id);
// ── AGENCY AI UNLOCK ──────────────────────────────────────────────────────────
// Change these two values. The key never appears in any UI.
const AGENCY_UNLOCK_CODE = 'carnes2026'; // ← your secret unlock code
const AGENCY_GEMINI_KEY = 'AIzaSyChg8dB9vA3AHKRtOhzGZAzzIPfA_ki4ss'; // ← your Gemini key
// ─────────────────────────────────────────────────────────────────────────────
let lastSearchQuery = '';
let currentFolderId = null;
let activeClientId = null;
let currentHostname = '';
const CARRIER_PORTALS = [
{ id: 'erie', name: 'Erie Insurance', url: 'https://agentexchange.com', group: 'both' },
{ id: 'progressive', name: 'Progressive', url: 'https://www.foragentsonly.com', group: 'both' },
{ id: 'westfield', name: 'Westfield', url: 'https://agent.westfieldinsurance.com/', group: 'both' },
{ id: 'encova', name: 'Encova', url: 'https://agent.encova.com/', group: 'both' },
{ id: 'municipal-mutual', name: 'Municipal Mutual', url: 'https://municipal.britecore.com/login', group: 'both' },
{ id: 'western-reserve', name: 'Western Reserve', url: 'https://agency.wrg-ins.com/', group: 'both' },
{ id: 'foremost', name: 'Foremost', url: 'https://www.foremostagent.com/ia/portal/login', group: 'both' },
{ id: 'smart-choice', name: 'Smart Choice', url: 'https://commissions.smartchoiceagents.com/user/login', group: 'both' },
{ id: 'liberty-mutual', name: 'Liberty Mutual', url: 'https://agent.libertymutual.com/start', group: 'both' },
{ id: 'pl-rater', name: 'PL Rater', url: 'https://rating.vertafore.com/UserInterface/main/login.aspx', group: 'personal' },
{ id: 'geico', name: 'Geico', url: 'https://www.geico.com/agency/', group: 'personal' },
{ id: 'openly', name: 'Openly', url: 'https://portal.openly.com/', group: 'personal' },
{ id: 'safeco', name: 'Safeco', url: 'https://now.agent.safeco.com/start', group: 'personal' },
{ id: 'next', name: 'Next Insurance', url: 'https://www.nextinsurance.com/', group: 'commercial' },
{ id: 'usli', name: 'USLI Online', url: 'https://tapco-smartchoice.usli.com/', group: 'commercial' },
{ id: 'phly', name: 'PHLY', url: 'https://bff.phly.com/auth/login?returnUrl=https%3A%2F%2Fwww.phly.com%2Fmyphly%2Fmyphly.aspx', group: 'commercial' },
{ id: 'coterie', name: 'Coterie', url: 'https://dashboard.coterieinsurance.com/login', group: 'commercial' },
{ id: 'pathpoint', name: 'Pathpoint', url: 'https://app.pathpoint.com/login', group: 'commercial' },
];
const CARRIER_PORTAL_PREFS_KEY = 'carrierPortalPrefs';
const RECENT_CLIENTS_KEY = 'recentClients';
const POPUP_UI_PREFS_KEY = 'popupUiPrefs';
const SPECIAL_MODE_KEY = 'specialMode';
const SPECIAL_MODE_PASSWORD = 'renewals';
const COUNTY_TOOL_LINKS = {
'belmont|oh': { name: 'Belmont County, OH Auditor', url: 'https://belmontcountyauditor.org/Search' },
'jefferson|oh': { name: 'Jefferson County, OH Auditor', url: 'https://jeffersoncountyoh.com/auditor' },
'harrison|oh': { name: 'Harrison County, OH Auditor', url: 'https://www.harrisoncountyohio.gov/auditors-office' },
'guernsey|oh': { name: 'Guernsey County, OH Parcel Search', url: 'http://guernseycounty.org/gis/' },
'monroe|oh': { name: 'Monroe County, OH Auditor', url: 'https://monroecoauditoroh.gov/' },
'noble|oh': { name: 'Noble County, OH Parcel Map', url: 'https://noble.maps.arcgis.com/apps/instant/basic/index.html?appid=99a2499daf4f4d5595dc9ae1f7723bab' },
'ohio|wv': { name: 'Ohio County, WV Assessor', url: 'https://ohio.wvassessor.com/Search.aspx' },
'marshall|wv': { name: 'Marshall County, WV Assessor', url: 'https://marcoassessor.org/' },
'brooke|wv': { name: 'Brooke County, WV Assessor', url: 'https://brooke.wvassessor.com/' }
};
const STATE_FULL_NAMES = {
AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California', CO: 'Colorado',
CT: 'Connecticut', DC: 'District of Columbia', DE: 'Delaware', FL: 'Florida', GA: 'Georgia', HI: 'Hawaii',
IA: 'Iowa', ID: 'Idaho', IL: 'Illinois', IN: 'Indiana', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',
MA: 'Massachusetts', MD: 'Maryland', ME: 'Maine', MI: 'Michigan', MN: 'Minnesota', MO: 'Missouri',
MS: 'Mississippi', MT: 'Montana', NC: 'North Carolina', ND: 'North Dakota', NE: 'Nebraska', NH: 'New Hampshire',
NJ: 'New Jersey', NM: 'New Mexico', NV: 'Nevada', NY: 'New York', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon',
PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota', TN: 'Tennessee', TX: 'Texas',
UT: 'Utah', VA: 'Virginia', VT: 'Vermont', WA: 'Washington', WI: 'Wisconsin', WV: 'West Virginia', WY: 'Wyoming'
};
function setText(id, text) {
const el = $(id);
if (el) el.textContent = text;
}
function setHidden(id, hidden) {
const el = $(id);
if (!el) return;
el.classList.toggle('hidden', !!hidden);
}
async function storageGet(keys) {
return await chrome.storage.local.get(keys);
}
async function storageSet(obj) {
return await chrome.storage.local.set(obj);
}
async function storageRemove(keys) {
return await chrome.storage.local.remove(keys);
}
function fmtAddress(c) {
const parts = [c.Address1 || c.address1 || c.addressLine1, c.Address2, c.City || c.city, c.State || c.state, c.Zip || c.zip || c.zipCode].filter(Boolean);
return parts.join(' ');
}
function getBillPayUrl(carrierString) {
if (!carrierString || typeof carrierString !== 'string') return null;
const name = carrierString.toLowerCase();
if (name.includes('progressive')) return 'https://account.apps.progressive.com/access/ez-payment/policy-info';
if (name.includes('encova')) return 'https://www.encova.com/pay-bill/encova-insurance-pay-bill/';
if (name.includes('westfield') || name.includes('old guard')) return 'https://www.westfieldinsurance.com/billing';
if (name.includes('erie')) return 'https://www.erieinsurance.com/paymentcenterweb/billpay/payment/billpayment';
if (name.includes('western reserve') || name.includes('wrg')) return 'https://www.wrg-ins.com/Customers/Payment';
return null;
}
function formatDisplayDate(dateStr) {
if (!dateStr) return '';
const isoMatch = String(dateStr).match(/^(\d{4})-(\d{2})-(\d{2})/);
if (isoMatch) return `${isoMatch[2]}/${isoMatch[3]}/${isoMatch[1]}`;
try {
const d = new Date(dateStr);
if (!isNaN(d.getTime())) return d.toLocaleDateString();
} catch (e) {}
return dateStr;
}
function titleCaseGroup(group) {
if (!group) return '';
return group.charAt(0).toUpperCase() + group.slice(1);
}
function getDefaultCarrierPortalPrefs() {
return CARRIER_PORTALS.reduce((acc, portal) => {
acc[portal.id] = true;
return acc;
}, {});
}
async function getCarrierPortalPrefs() {
const defaults = getDefaultCarrierPortalPrefs();
const stored = await storageGet([CARRIER_PORTAL_PREFS_KEY]);
return { ...defaults, ...(stored[CARRIER_PORTAL_PREFS_KEY] || {}) };
}
async function setCarrierPortalPref(portalId, enabled) {
const prefs = await getCarrierPortalPrefs();
prefs[portalId] = !!enabled;
await storageSet({ [CARRIER_PORTAL_PREFS_KEY]: prefs });
return prefs;
}
function getPreferredLineOfBusiness(client) {
const firstPolicy = Array.isArray(client?.policies) ? client.policies[0] : null;
return (
firstPolicy?.LineOfBusinessName ||
firstPolicy?.lineOfBusinessName ||
firstPolicy?.LineOfBusiness ||
firstPolicy?.lineOfBusiness ||
''
);
}
function buildQuoteNumber(carrier) {
const carrierPart = String(carrier || 'QUOTE').toUpperCase().replace(/[^A-Z0-9]+/g, '').slice(0, 8) || 'QUOTE';
const stamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 12);
return `${carrierPart}-${stamp}`;
}
function normalizeMeaningfulString(value) {
const str = String(value || '').trim();
if (!str || str.toLowerCase() === 'null' || str.toLowerCase() === 'undefined') return '';
return str;
}
function getClientDisplayName(client) {
if (!client) return '';
return client?.CommercialName || client?.commercialName || client?.FullName || client?.fullName || `${client?.FirstName || client?.firstName || ''} ${client?.LastName || client?.lastName || ''}`.trim() || '';
}
function getClientStateAbbr(client) {
const raw = normalizeMeaningfulString(client?.State || client?.state);
if (!raw) return '';
if (raw.length === 2) return raw.toUpperCase();
const entry = Object.entries(STATE_FULL_NAMES).find(([, name]) => name.toLowerCase() === raw.toLowerCase());
return entry ? entry[0] : raw.toUpperCase().slice(0, 2);
}
function getClientStateName(client) {
const raw = normalizeMeaningfulString(client?.State || client?.state);
if (!raw) return '';
if (raw.length === 2) return STATE_FULL_NAMES[raw.toUpperCase()] || raw.toUpperCase();
return raw;
}
async function getPopupUiPrefs() {
const stored = await storageGet([POPUP_UI_PREFS_KEY]);
return {
showMissingOnly: false,
collapseEmptySections: true,
...(stored[POPUP_UI_PREFS_KEY] || {})
};
}
async function setPopupUiPrefs(nextPrefs) {
const current = await getPopupUiPrefs();
const merged = { ...current, ...nextPrefs };
await storageSet({ [POPUP_UI_PREFS_KEY]: merged });
return merged;
}
async function getRecentClients() {
const stored = await storageGet([RECENT_CLIENTS_KEY]);
return Array.isArray(stored[RECENT_CLIENTS_KEY]) ? stored[RECENT_CLIENTS_KEY] : [];
}
async function pushRecentClient(client) {
if (!client) return [];
const id = client.Id || client.id || client.DatabaseId || client.databaseId || client.insuredId;
if (!id) return getRecentClients();
const existing = await getRecentClients();
const next = [
{
Id: id,
DatabaseId: client.DatabaseId || client.databaseId || id,
FullName: client.FullName || client.fullName || '',
CommercialName: client.CommercialName || client.commercialName || '',
FirstName: client.FirstName || client.firstName || '',
LastName: client.LastName || client.lastName || '',
Email: client.Email || client.email || '',
Phone: client.Phone || client.phone || client.CellPhone || client.cellPhone || '',
City: client.City || client.city || '',
State: client.State || client.state || '',
updatedAt: new Date().toISOString()
},
...existing.filter((item) => (item.Id || item.id || item.DatabaseId || item.databaseId) !== id)
].slice(0, 8);
await storageSet({ [RECENT_CLIENTS_KEY]: next });
return next;
}
function normalizeNamePart(value) {
return normalizeMeaningfulString(value).toLowerCase().replace(/[^a-z0-9]/g, '');
}
function isMissingValue(value) {
if (value === null || value === undefined) return true;
const str = normalizeMeaningfulString(value);
if (!str) return true;
return false;
}
function dedupeBy(items, keyBuilder) {
const seen = new Set();
return (Array.isArray(items) ? items : []).filter((item) => {
const key = keyBuilder(item);
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
}
function findMatchingPersonRecord(client) {
const first = normalizeNamePart(client?.FirstName || client?.firstName);
const last = normalizeNamePart(client?.LastName || client?.lastName);
if (!first || !last) return null;
const pools = [
...(Array.isArray(client?.drivers) ? client.drivers : []),
...(Array.isArray(client?.insuredContacts) ? client.insuredContacts : []),
...(Array.isArray(client?.contacts) ? client.contacts : [])
];
return pools.find((person) => {
const personFirst = normalizeNamePart(person?.FirstName || person?.firstName);
const personLast = normalizeNamePart(person?.LastName || person?.lastName);
return personFirst === first && personLast === last;
}) || null;
}
function enrichClientWithMatchedPersonData(client) {
const match = findMatchingPersonRecord(client);
if (!match) return client;
return {
...client,
BirthDate: client?.BirthDate || client?.dateOfBirth || client?.DOB || match?.birthday || match?.BirthDate || '',
SSN: client?.SSN || client?.SocialSecurityNumber || client?.socialSecurityNumber || match?.socialSecurityNumber || '',
Phone: client?.Phone || client?.phone || client?.PrimaryPhone || match?.cellPhone || match?.homePhone || match?.officePhone || '',
CellPhone: client?.CellPhone || client?.cellPhone || client?.MobilePhone || match?.cellPhone || '',
Email: client?.Email || client?.email || client?.PrimaryEmail || match?.personalEMail || match?.businessEMail || '',
DriverLicenseNumber: client?.DriverLicenseNumber || client?.driverLicenseNumber || client?.LicenseNumber || match?.licenseNumber || match?.dlNumber || '',
DriverLicenseState: client?.DriverLicenseState || client?.driverLicenseState || client?.LicenseState || match?.licenseState || match?.dlStateName || '',
Gender: client?.Gender || client?.gender || client?.Sex || client?.sex || match?.gender || '',
MaritalStatus: client?.MaritalStatus || client?.maritalStatus || match?.maritalStatus || match?.maritalStatusCode || ''
};
}
function getPolicyId(policy) {
return policy?.DatabaseId || policy?.databaseId || policy?.PolicyDatabaseId || policy?.policyDatabaseId || '';
}
function getPolicyDates(policy) {
const eff = new Date(policy?.EffectiveDate || policy?.effectiveDate || 0);
const exp = new Date(policy?.ExpirationDate || policy?.expirationDate || 0);
return { eff, exp };
}
function getCurrentPolicies(policies) {
const list = Array.isArray(policies) ? policies.filter(Boolean) : [];
if (!list.length) return [];
const now = new Date();
const active = list.filter((policy) => {
const status = String(policy?.Status || policy?.status || '').toLowerCase();
const { eff, exp } = getPolicyDates(policy);
if (status.includes('active')) return true;
return !isNaN(eff.getTime()) && !isNaN(exp.getTime()) && eff <= now && exp >= now;
});
if (active.length) return active;
const sorted = [...list].sort((a, b) => {
const aEff = getPolicyDates(a).eff.getTime() || 0;
const bEff = getPolicyDates(b).eff.getTime() || 0;
return bEff - aEff;
});
return sorted.length ? [sorted[0]] : [];
}
function filterPolicyChildrenByPolicies(items, policies, possiblePolicyKeys = []) {
const allowed = new Set(getCurrentPolicies(policies).map(getPolicyId).filter(Boolean));
if (!allowed.size) return Array.isArray(items) ? items : [];
return (Array.isArray(items) ? items : []).filter((item) => {
const itemPolicyId = possiblePolicyKeys.map((key) => item?.[key]).find(Boolean);
return !itemPolicyId || allowed.has(itemPolicyId);
});
}
function normalizeClientCollections(client) {
if (!client) return client;
const normalizedPolicies = getCurrentPolicies(client.policies);
const vehicles = filterPolicyChildrenByPolicies(
client.vehicles,
normalizedPolicies,
['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']
);
const drivers = filterPolicyChildrenByPolicies(
client.drivers,
normalizedPolicies,
['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']
);
const properties = filterPolicyChildrenByPolicies(
client.properties,
normalizedPolicies,
['policyDatabaseId', 'PolicyDatabaseId', 'policyId', 'PolicyId']
);
const normalized = {
...client,
policies: dedupeBy(normalizedPolicies, (policy) => getPolicyId(policy) || normalizeMeaningfulString(policy?.number || policy?.Number)),
vehicles: dedupeBy(vehicles, (vehicle) => normalizeMeaningfulString(vehicle?.VIN || vehicle?.vin || `${vehicle?.Year || ''}${vehicle?.Make || ''}${vehicle?.Model || ''}`)),
drivers: dedupeBy(drivers, (driver) => normalizeMeaningfulString(driver?.databaseId || driver?.DatabaseId || `${driver?.firstName || driver?.FirstName || ''}|${driver?.lastName || driver?.LastName || ''}|${driver?.licenseNumber || driver?.LicenseNumber || ''}`)),
properties: dedupeBy(properties, (property) => normalizeMeaningfulString(property?.databaseId || property?.DatabaseId || property?.addressLine1 || property?.Address1))
};
return enrichClientWithMatchedPersonData(normalized);
}
function normalizeStateCode(value) {
const raw = normalizeMeaningfulString(value).toLowerCase();
if (!raw) return '';
const stateMap = {
ohio: 'oh',
oh: 'oh',
'west virginia': 'wv',
wv: 'wv'
};
return stateMap[raw] || raw.slice(0, 2);
}
function normalizeCountyName(value) {
return normalizeMeaningfulString(value)
.toLowerCase()
.replace(/county/g, '')
.replace(/[^a-z]/g, '');
}
function formatCoverageValue(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'number') {
if (!Number.isFinite(value)) return '';
if (Number.isInteger(value)) return value.toLocaleString();
return value.toFixed(2);
}
return normalizeMeaningfulString(value);
}
function collectScalarEntries(value, path = '', entries = []) {
if (value === null || value === undefined) return entries;
if (Array.isArray(value)) {
value.forEach((item, index) => collectScalarEntries(item, `${path}[${index}]`, entries));
return entries;
}
if (typeof value === 'object') {
Object.entries(value).forEach(([key, child]) => {
collectScalarEntries(child, path ? `${path}.${key}` : key, entries);
});
return entries;
}
const str = formatCoverageValue(value);
if (!str) return entries;
entries.push({
path,
normalizedPath: String(path || '').toLowerCase().replace(/[^a-z0-9]/g, ''),
value: str
});
return entries;
}
function findCoverageMatch(source, keywordGroups) {
const entries = collectScalarEntries(source);
let best = null;
entries.forEach((entry) => {
if (!entry.normalizedPath || !entry.value) return;
const matchedAll = keywordGroups.every((group) => group.some((pattern) => pattern.test(entry.normalizedPath)));
if (!matchedAll) return;
const score = keywordGroups.reduce((total, group) => {
const matches = group.filter((pattern) => pattern.test(entry.normalizedPath)).length;
return total + matches;
}, 0) - entry.normalizedPath.length / 1000;
if (!best || score > best.score) {
best = { value: entry.value, score, path: entry.path };
}
});
if (!best) return { value: '', confidence: 'low', sourcePath: '' };
const confidence = best.score >= 3.9 ? 'high' : best.score >= 2.4 ? 'medium' : 'low';
return { value: best.value || '', confidence, sourcePath: best.path || '' };
}
function findCoverageValue(source, keywordGroups) {
return findCoverageMatch(source, keywordGroups).value || '';
}
function getCurrentProperty(client) {
return Array.isArray(client?.properties) && client.properties.length ? client.properties[0] : null;
}
function getCountyLookupContext(client) {
const property = getCurrentProperty(client);
const county = (
property?.County ||
property?.county ||
property?.CountyName ||
property?.countyName ||
property?.PropertyCounty ||
property?.propertyCounty ||
client?.County ||
client?.county
);
const state = (
property?.State ||
property?.state ||
property?.StateAbbreviation ||
property?.stateAbbreviation ||
client?.State ||
client?.state
);
const key = `${normalizeCountyName(county)}|${normalizeStateCode(state)}`;
if (!normalizeCountyName(county) || !normalizeStateCode(state)) return null;
return COUNTY_TOOL_LINKS[key] ? { key, county, state, link: COUNTY_TOOL_LINKS[key] } : null;
}
function getCountyToolLinks(client) {
const preferred = getCountyLookupContext(client);
const supported = Object.entries(COUNTY_TOOL_LINKS).map(([key, link]) => ({ key, ...link }));
if (!preferred) return supported;
return [
{ key: preferred.key, ...preferred.link, preferred: true },
...supported.filter((entry) => entry.key !== preferred.key)
];
}
function buildQuickSearchLinks(client) {
const name = getClientDisplayName(client);
const stateName = getClientStateName(client);
const countyContext = getCountyLookupContext(client);
const queries = [];
if (name) {
queries.push({ name: 'Google Client', url: `https://www.google.com/search?q=${encodeURIComponent(name)}` });
}
if (countyContext) {
queries.push({
name: 'County GIS',
url: `https://www.google.com/search?q=${encodeURIComponent(`${countyContext.county} ${countyContext.state} GIS property search`)}`
});
}
const stateAbbr = getClientStateAbbr(client);
if (name && stateAbbr === 'OH') {
queries.push({
name: 'Ohio SOS',
url: `https://businesssearch.ohiosos.gov/?q=${encodeURIComponent(name)}`
});
} else if (name && stateAbbr === 'WV') {
queries.push({
name: 'WV SOS',
url: `https://apps.sos.wv.gov/business/corporations/BusinessEntitySearch/Search?SearchTerm=${encodeURIComponent(name)}`
});
} else if (name) {
queries.push({
name: 'Secretary of State',
url: `https://www.google.com/search?q=${encodeURIComponent(`${name} ${stateName} secretary of state business search`)}`
});
}
const addr = (client?.Address1 || client?.address1 || client?.addressLine1 || '').trim();
const city = (client?.City || client?.city || '').trim();
const state = (client?.State || client?.state || '').trim();
const zip = (client?.Zip || client?.zip || client?.zipCode || '').trim();
const fullAddr = [addr, city, state, zip].filter(Boolean).join(', ');
if (fullAddr) {
queries.push({ name: '📍 Street View', url: `https://www.google.com/maps?q=${encodeURIComponent(fullAddr)}&layer=c` });
const zillowAddr = [addr, city, state, zip].filter(Boolean).join(' ');
queries.push({ name: '🏠 Zillow', url: `https://www.zillow.com/homes/${encodeURIComponent(zillowAddr)}_rb/` });
}
return queries;
}
function getCurrentPolicy(client) {
return Array.isArray(client?.policies) && client.policies.length ? client.policies[0] : null;
}
function getVehicleDescription(vehicle, index) {
const desc = `${vehicle?.Year || vehicle?.year || ''} ${vehicle?.Make || vehicle?.make || ''} ${vehicle?.Model || vehicle?.model || ''}`.trim();
return desc || `Vehicle #${index + 1}`;
}
function extractVehicleDeductibles(vehicle) {
return {
comp: findCoverageValue(vehicle, [
[/comp/, /comprehensive/, /otherthancollision/, /otc/],
[/ded/, /deduct/]
]),
collision: findCoverageValue(vehicle, [
[/collision/, /coll/],
[/ded/, /deduct/]
])
};
}
function extractVehicleDeductiblesDetailed(vehicle) {
return {
comp: findCoverageMatch(vehicle, [
[/comp/, /comprehensive/, /otherthancollision/, /otc/],
[/ded/, /deduct/]
]),
collision: findCoverageMatch(vehicle, [
[/collision/, /coll/],
[/ded/, /deduct/]
])
};
}
function extractPropertyLimits(property, policy) {
const source = { property, policy };
return {
dwelling: findCoverageValue(source, [[/dwelling/, /coveragea/, /building/, /structure/], [/limit/, /amount/, /coverage/, /value/]]),
personalProperty: findCoverageValue(source, [[/personalproperty/, /contents/, /coveragec/], [/limit/, /amount/, /coverage/, /value/]]),
businessPersonalProperty: findCoverageValue(source, [[/businesspersonalproperty/, /businesscontents/, /coveragebpp/, /bpp/], [/limit/, /amount/, /coverage/, /value/]])
};
}
function extractPropertyLimitsDetailed(property, policy) {
const source = { property, policy };
return {
dwelling: findCoverageMatch(source, [[/dwelling/, /coveragea/, /building/, /structure/], [/limit/, /amount/, /coverage/, /value/]]),
personalProperty: findCoverageMatch(source, [[/personalproperty/, /contents/, /coveragec/], [/limit/, /amount/, /coverage/, /value/]]),
businessPersonalProperty: findCoverageMatch(source, [[/businesspersonalproperty/, /businesscontents/, /coveragebpp/, /bpp/], [/limit/, /amount/, /coverage/, /value/]])
};
}
function getMissingImportantFields(client) {
const important = [
['First Name', client?.FirstName || client?.firstName],
['Last Name', client?.LastName || client?.lastName],
['DOB', client?.BirthDate || client?.dateOfBirth || client?.DOB],
['Primary Phone', client?.Phone || client?.phone || client?.CellPhone || client?.cellPhone],
['Primary Email', client?.Email || client?.email || client?.PrimaryEmail],
['Street Address', client?.Address1 || client?.address1 || client?.addressLine1],
['City', client?.City || client?.city],
['State', client?.State || client?.state],
['ZIP', client?.Zip || client?.zip || client?.zipCode]
];
return important.filter(([, value]) => isMissingValue(value)).map(([label]) => label);
}
function renderMissingDataWarning(client) {
const el = $('missing-data-warning');
if (!el) return;
const missing = getMissingImportantFields(client);
if (!missing.length) {
el.classList.add('hidden');
el.innerHTML = '';
return;
}
el.classList.remove('hidden');
el.innerHTML = `
Missing Important Client Data
This data is not being found: ${missing.join(', ')}.
Recommend adding it in NowCerts or updating it below here when possible.
If the info does exist somewhere deeper in NowCerts, AI Data Rescue may help.
`;
}
function calculateAge(dateValue) {
if (!dateValue) return '';
const dob = new Date(dateValue);
if (Number.isNaN(dob.getTime())) return '';
const now = new Date();
let age = now.getFullYear() - dob.getFullYear();
const beforeBirthday = now.getMonth() < dob.getMonth()
|| (now.getMonth() === dob.getMonth() && now.getDate() < dob.getDate());
if (beforeBirthday) age -= 1;
return age >= 0 ? String(age) : '';
}
function getHouseholdSnapshotLines(client) {
const people = dedupeBy([
{
FirstName: client?.FirstName || client?.firstName,
LastName: client?.LastName || client?.lastName,
BirthDate: client?.BirthDate || client?.dateOfBirth || client?.DOB,
role: 'Named insured',
driverStatusCode: client?.driverStatusCode || client?.DriverStatusCode || ''
},
...(Array.isArray(client?.drivers) ? client.drivers : [])
], (person) => normalizeMeaningfulString(`${person?.DatabaseId || person?.databaseId || ''}|${person?.FirstName || person?.firstName || ''}|${person?.LastName || person?.lastName || ''}|${person?.BirthDate || person?.dateOfBirth || person?.DOB || ''}`));
return people.slice(0, 6).map((person, index) => {
const name = `${person?.FirstName || person?.firstName || ''} ${person?.LastName || person?.lastName || ''}`.trim() || `Household Member #${index + 1}`;
const age = calculateAge(person?.BirthDate || person?.birthday || person?.dateOfBirth || person?.DOB);
const status = normalizeMeaningfulString(person?.DriverStatusCode || person?.driverStatusCode || person?.DriverStatus || person?.driverStatus || person?.role || (index === 0 ? 'Named insured' : 'Driver'));
const extras = [status, age ? `Age ${age}` : ''].filter(Boolean).join(' • ');
return `${name}${extras ? ` — ${extras}` : ''}`;
});
}
function getNamedInsuredCards(client) {
const cards = [];
const primary = {
firstName: client?.FirstName || client?.firstName || '',
lastName: client?.LastName || client?.lastName || '',
birthDate: client?.BirthDate || client?.birthDate || client?.dateOfBirth || client?.DOB || '',
email: client?.Email || client?.email || client?.PrimaryEmail || '',
phone: client?.Phone || client?.phone || client?.CellPhone || client?.cellPhone || '',
gender: client?.Gender || client?.gender || client?.Sex || client?.sex || '',
license: client?.DriverLicenseNumber || client?.driverLicenseNumber || client?.LicenseNumber || client?.licenseNumber || client?.dlNumber || '',
role: 'First Named Insured'
};
const second = client?.coApplicant || client?.CoApplicant || (Array.isArray(client?.insuredContacts)
? client.insuredContacts.find((person) => {
const first = String(person?.FirstName || person?.firstName || '').trim();
const last = String(person?.LastName || person?.lastName || '').trim();
const primaryName = `${primary.firstName} ${primary.lastName}`.trim().toLowerCase();
return `${first} ${last}`.trim() && `${first} ${last}`.trim().toLowerCase() !== primaryName;
})
: null);
const people = [primary, second ? { ...second, role: 'Second Named Insured' } : null].filter(Boolean);
people.forEach((person, index) => {
const name = `${person?.firstName || person?.FirstName || ''} ${person?.lastName || person?.LastName || ''}`.trim();
const lines = [
name ? `Name: ${name}` : '',
(person?.birthDate || person?.BirthDate || person?.dateOfBirth || person?.DOB) ? `DOB: ${formatDisplayDate(person?.birthDate || person?.BirthDate || person?.dateOfBirth || person?.DOB)}` : '',
(person?.gender || person?.Gender || person?.sex || person?.Sex) ? `Gender: ${person?.gender || person?.Gender || person?.sex || person?.Sex}` : '',
(person?.license || person?.LicenseNumber || person?.licenseNumber || person?.DriverLicenseNumber || person?.driverLicenseNumber || person?.dlNumber) ? `License: ${person?.license || person?.LicenseNumber || person?.licenseNumber || person?.DriverLicenseNumber || person?.driverLicenseNumber || person?.dlNumber}` : '',
(person?.phone || person?.Phone || person?.CellPhone || person?.cellPhone) ? `Phone: ${person?.phone || person?.Phone || person?.CellPhone || person?.cellPhone}` : '',
(person?.email || person?.Email || person?.PrimaryEmail || person?.personalEMail || person?.businessEMail) ? `Email: ${person?.email || person?.Email || person?.PrimaryEmail || person?.personalEMail || person?.businessEMail}` : ''
].filter(Boolean);
cards.push({ title: person.role || `Named Insured #${index + 1}`, lines });
});
return cards;
}
function getVehicleCards(client) {
return (Array.isArray(client?.vehicles) ? client.vehicles : []).map((vehicle, index) => {
const deductibles = extractVehicleDeductiblesDetailed ? extractVehicleDeductiblesDetailed(vehicle) : {
comp: { value: extractVehicleDeductibles(vehicle).comp || '', confidence: '', sourcePath: '' },
collision: { value: extractVehicleDeductibles(vehicle).collision || '', confidence: '', sourcePath: '' }
};
const description = `${vehicle?.Year || vehicle?.year || ''} ${vehicle?.Make || vehicle?.make || ''} ${vehicle?.Model || vehicle?.model || ''}`.trim();
return {
title: description || `Vehicle #${index + 1}`,
lines: [
description ? `Vehicle: ${description}` : '',
(vehicle?.VIN || vehicle?.vin) ? `VIN: ${vehicle?.VIN || vehicle?.vin}` : '',
deductibles.comp?.value ? `Comp Ded: ${deductibles.comp.value}` : '',
deductibles.collision?.value ? `Coll Ded: ${deductibles.collision.value}` : ''
].filter(Boolean)
};
});
}
function getPropertySnapshotLines(client) {
const property = getCurrentProperty(client);
if (!property) return [];
const propertyPolicy = getCurrentPolicy(client);
const limitDetails = extractPropertyLimitsDetailed(property, propertyPolicy);
const address = [
property?.Address1 || property?.address1 || property?.addressLine1,
property?.City || property?.city,
property?.State || property?.state,
property?.Zip || property?.zip || property?.zipCode
].filter(Boolean).join(', ');
const occupancy = normalizeMeaningfulString(property?.Occupancy || property?.occupancy || property?.OccupancyType || property?.occupancyType);
const construction = normalizeMeaningfulString(property?.ConstructionType || property?.constructionType || property?.Construction || property?.construction);
const yearBuilt = normalizeMeaningfulString(property?.YearBuilt || property?.yearBuilt);
const squareFeet = normalizeMeaningfulString(property?.SquareFeet || property?.squareFeet || property?.LivingArea || property?.livingArea);
const lines = [];
if (address) lines.push(`Address: ${address}`);
if (occupancy || construction) lines.push(`Type: ${[occupancy, construction].filter(Boolean).join(' • ')}`);
if (yearBuilt || squareFeet) lines.push(`Details: ${[yearBuilt ? `Built ${yearBuilt}` : '', squareFeet ? `${squareFeet} sq ft` : ''].filter(Boolean).join(' • ')}`);
if (limitDetails.dwelling.value) lines.push(`Building: ${limitDetails.dwelling.value} (${limitDetails.dwelling.confidence})`);
if (limitDetails.personalProperty.value) lines.push(`Personal Property: ${limitDetails.personalProperty.value} (${limitDetails.personalProperty.confidence})`);
if (limitDetails.businessPersonalProperty.value) lines.push(`BPP: ${limitDetails.businessPersonalProperty.value} (${limitDetails.businessPersonalProperty.confidence})`);
return lines;
}
function renderSnapshots(client) {
const container = $('snapshot-grid');
if (!container) return;
container.innerHTML = '';
getNamedInsuredCards(client).forEach((card) => renderSnapshotCard(container, card.title, card.lines));
getVehicleCards(client).forEach((card) => renderSnapshotCard(container, card.title, card.lines));
renderSnapshotCard(container, 'Household Snapshot', getHouseholdSnapshotLines(client));
renderSnapshotCard(container, 'Property Snapshot', getPropertySnapshotLines(client));
}
function buildInsuredSyncPayload(client) {
// Field names must be camelCase per NowCerts API docs.
// eMail has a capital M — that's the actual API field name.
return {
databaseId: client.Id || client.DatabaseId || client.id || '',
commercialName:client.CommercialName || client.commercialName || '',
firstName: client.FirstName || client.firstName || '',
middleName: client.MiddleName || client.middleName || client.MI || '',
lastName: client.LastName || client.lastName || '',
addressLine1: client.Address1 || client.address1 || client.addressLine1 || '',
addressLine2: client.Address2 || client.address2 || client.addressLine2 || '',
city: client.City || client.city || '',
state: client.State || client.state || '',
zipCode: client.Zip || client.zip || client.zipCode || '',
eMail: client.Email || client.email || client.PrimaryEmail || '',
phone: client.Phone || client.phone || client.PrimaryPhone || '',
cellPhone: client.CellPhone || client.cellPhone || client.MobilePhone || '',
dateOfBirth: client.BirthDate || client.birthDate || client.DateOfBirth || client.dateOfBirth || '',
fein: client.TaxId || client.taxId || client.TaxID || '',
active: true,
type: 0,
insuredType: 0
};
}
async function syncInsuredToNowCerts(client) {
const payload = buildInsuredSyncPayload(client);
if (!payload.databaseId) throw new Error('Missing insured database id.');
return apiFetch('https://api.nowcerts.com/api/Insured/Insert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
async function syncQuoteToNowCerts(client, carrier, premium) {
const insuredId = client?.Id || client?.DatabaseId || client?.id || activeClientId;
if (!insuredId) throw new Error('No client selected');
const normalizedCarrier = String(carrier || '').trim();
if (!normalizedCarrier) throw new Error('Carrier is required');
const premiumValue = Number(premium);
if (!Number.isFinite(premiumValue)) throw new Error('Premium must be a valid number');
const payload = {
insured_database_id: insuredId,
insured_first_name: client?.FirstName || client?.firstName || '',
insured_last_name: client?.LastName || client?.lastName || '',
insured_commercial_name: client?.CommercialName || client?.commercialName || '',
insured_email: client?.Email || client?.email || '',
carrier_name: normalizedCarrier,
premium: premiumValue,
number: buildQuoteNumber(normalizedCarrier),
line_of_business_name: getPreferredLineOfBusiness(client),
effective_date: client?.policies?.[0]?.EffectiveDate || client?.policies?.[0]?.effectiveDate || null,
expiration_date: client?.policies?.[0]?.ExpirationDate || client?.policies?.[0]?.expirationDate || null,
description: `Carrier Bridge quote sync from ${window.location.hostname || 'extension'}`
};
return apiFetch('https://api.nowcerts.com/api/Zapier/InsertQuote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
async function renderCarrierPortals(client) {
const wrap = $('carrier-portals');
const prefsPanel = $('carrier-prefs-panel');
if (!wrap || !prefsPanel) return;
const prefs = await getCarrierPortalPrefs();
wrap.innerHTML = '';
prefsPanel.innerHTML = '';
CARRIER_PORTALS.forEach((portal) => {
const row = document.createElement('label');
row.className = 'carrier-pref-row';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = !!prefs[portal.id];
checkbox.addEventListener('change', async () => {
await setCarrierPortalPref(portal.id, checkbox.checked);
await renderCarrierPortals(client);
});
const name = document.createElement('span');
name.textContent = portal.name;
const meta = document.createElement('span');
meta.className = 'carrier-pref-meta';
meta.textContent = titleCaseGroup(portal.group);
row.appendChild(checkbox);
row.appendChild(name);
row.appendChild(meta);
prefsPanel.appendChild(row);
});
['both', 'personal', 'commercial'].forEach((group) => {
const visible = CARRIER_PORTALS.filter((portal) => portal.group === group && prefs[portal.id]);
const heading = document.createElement('div');
heading.className = 'carrier-group-label';
heading.textContent = group === 'both' ? 'Both Personal + Commercial' : `${titleCaseGroup(group)} Only`;
wrap.appendChild(heading);
if (!visible.length) {
const empty = document.createElement('div');
empty.className = 'carrier-empty';
empty.textContent = `No ${titleCaseGroup(group)} carrier portals selected.`;
wrap.appendChild(empty);
return;
}
const grid = document.createElement('div');
grid.className = 'carrier-grid';
visible.forEach((portal) => {
const btn = document.createElement('button');
btn.textContent = portal.name;
btn.addEventListener('click', () => chrome.tabs.create({ url: portal.url }));
grid.appendChild(btn);
});
wrap.appendChild(grid);
});
const countyLinks = getCountyToolLinks(client);
if (countyLinks.length) {
const heading = document.createElement('div');
heading.className = 'carrier-group-label';
heading.textContent = 'County Lookup Tools';
wrap.appendChild(heading);
const countyContext = getCountyLookupContext(client);
if (countyContext) {
const note = document.createElement('div');
note.className = 'carrier-empty';
note.textContent = `Recommended for ${countyContext.county}, ${countyContext.state}.`;
wrap.appendChild(note);
}
const grid = document.createElement('div');
grid.className = 'carrier-grid';
countyLinks.forEach((tool) => {
const btn = document.createElement('button');
btn.textContent = tool.preferred ? `${tool.name} Recommended` : tool.name;
btn.addEventListener('click', () => chrome.tabs.create({ url: tool.url }));
grid.appendChild(btn);
});
wrap.appendChild(grid);
}
const quickLinks = buildQuickSearchLinks(client);
if (quickLinks.length) {
const heading = document.createElement('div');
heading.className = 'carrier-group-label';
heading.textContent = 'Quick Search Links';
wrap.appendChild(heading);
const grid = document.createElement('div');
grid.className = 'carrier-grid';
quickLinks.forEach((link) => {
const btn = document.createElement('button');
btn.textContent = link.name;
btn.addEventListener('click', () => chrome.tabs.create({ url: link.url }));
grid.appendChild(btn);
});
wrap.appendChild(grid);
}
}
function createCollapsibleCard(title, bodyNodes = [], open = false, subtitle = '') {
const card = document.createElement('div');
card.className = 'panel-card';
const details = document.createElement('details');
details.open = !!open;
const summary = document.createElement('summary');
summary.textContent = title;
details.appendChild(summary);
const body = document.createElement('div');
body.className = 'panel-body';
if (subtitle) {
const copy = document.createElement('p');
copy.className = 'section-copy';
copy.textContent = subtitle;
body.appendChild(copy);
}
bodyNodes.forEach((node) => {
if (node) body.appendChild(node);
});
details.appendChild(body);
card.appendChild(details);
return card;
}
function reorganizeActiveClientLayout() {
return;
}
// ── Progress bar helpers ───────────────────────────────────────────────────────
function showProgress(label, pct) {
const wrap = $('load-progress');
const bar = $('load-progress-bar');
const lbl = $('load-progress-label');
if (!wrap) return;
wrap.style.display = 'block';
if (bar) bar.style.width = pct + '%';
if (lbl) lbl.textContent = label;
}
function hideProgress() {
const wrap = $('load-progress');
if (wrap) wrap.style.display = 'none';
}
// ── AI DATA RESCUE HELPERS ────────────────────────────────────────────────────
function getJsonSkeleton(obj, prefix = '') {
let paths = [];
if (!obj || typeof obj !== 'object') return paths;
for (let key in obj) {
let newKey = prefix ? `${prefix}.${key}` : key;
if (Array.isArray(obj[key])) {
if (obj[key].length > 0 && typeof obj[key][0] === 'object') {
paths = paths.concat(getJsonSkeleton(obj[key][0], `${newKey}[0]`));
}
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
paths = paths.concat(getJsonSkeleton(obj[key], newKey));
} else {
paths.push(newKey);
}
}
return paths;
}
function getValueFromPath(obj, path) {
if (!path || typeof path !== 'string') return undefined;
return path.split('.').reduce((acc, part) => {
if (acc === null || acc === undefined) return undefined;
const match = part.match(/(.+)\[(\d+)\]/);
if (match) return acc[match[1]]?.[match[2]];
return acc[part];
}, obj);
}
async function applyLearnedData(client) {
const { learnedDataMap } = await storageGet(['learnedDataMap']);
if (!learnedDataMap || !client) return client;
let updated = false;
for (const [standardKey, jsonPath] of Object.entries(learnedDataMap)) {
if (!client[standardKey] && jsonPath) {
const foundValue = getValueFromPath(client, jsonPath);
if (foundValue) {
client[standardKey] = foundValue;
updated = true;
}
}
}
if (updated) {
console.log('[Carrier Bridge] Applied learned AI mappings to this client automatically.');
}
return client;
}
// ─────────────────────────────────────────────────────────────────────────────
function makeConfidenceBadge(meta) {
if (!meta?.confidence || !meta?.sourcePath) return null;
const badge = document.createElement('span');
badge.className = `confidence-badge confidence-${meta.confidence}`;
badge.textContent = meta.confidence === 'high' ? 'Current policy' : meta.confidence === 'medium' ? 'Likely match' : 'Fuzzy match';
badge.title = `Source: ${meta.sourcePath}`;
return badge;
}
function renderSnapshotCard(container, title, lines) {
if (!container || !Array.isArray(lines) || !lines.length) return;
const card = document.createElement('div');
card.className = 'snapshot-card';
const heading = document.createElement('h5');
heading.textContent = title;
card.appendChild(heading);
lines.filter(Boolean).forEach((line) => {
const row = document.createElement('div');
row.className = 'snapshot-line';
row.innerHTML = line;
card.appendChild(row);
});
container.appendChild(card);
}
function makePasteRow(label, initialValue, onPaste, editConfig = null, meta = null) {
const row = document.createElement('div');
row.className = 'paste-row';
const l = document.createElement('div');
l.className = 'paste-label';
l.textContent = label;
const badge = makeConfidenceBadge(meta);
if (badge) l.appendChild(badge);
let v;
let currentValue = initialValue || '';
if (editConfig) {
v = document.createElement('input');
v.className = 'paste-input';
v.value = currentValue;
v.placeholder = `Add ${label}...`;
v.title = 'Click to edit and save';
v.addEventListener('change', async (e) => {
const newVal = e.target.value.trim();
currentValue = newVal;
v.style.borderColor = '#10b981';
v.style.backgroundColor = '#ecfdf5';
setTimeout(() => {
v.style.borderColor = '';
v.style.backgroundColor = '';
}, 1000);
if (editConfig.onSave) editConfig.onSave(newVal, editConfig.key);
});
} else {
v = document.createElement('div');
v.className = 'paste-value';
v.title = currentValue;
v.textContent = currentValue;
}
const pasteBtn = document.createElement('button');
pasteBtn.className = 'paste-btn';
pasteBtn.textContent = 'Paste';
pasteBtn.addEventListener('click', () => onPaste(currentValue));
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.innerHTML = '📋';
copyBtn.title = 'Copy to clipboard';
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(currentValue).then(() => {
const orig = copyBtn.innerHTML;
copyBtn.textContent = '✓';
copyBtn.style.background = '#059669';
copyBtn.style.color = 'white';
setTimeout(() => {
copyBtn.innerHTML = orig;
copyBtn.style.background = '';
copyBtn.style.color = '';
}, 1200);
}).catch(() => alert('Copy failed'));
});
const btnContainer = document.createElement('div');
btnContainer.className = 'paste-btn-container';
btnContainer.appendChild(pasteBtn);
btnContainer.appendChild(copyBtn);
row.appendChild(l);
row.appendChild(v);
row.appendChild(btnContainer);
return row;
}
async function getActiveTab() {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
return tabs && tabs[0] ? tabs[0] : null;
}
async function sendToActiveTab(message) {
const tab = await getActiveTab();
if (!tab?.id) throw new Error('No active tab');
try {
return await chrome.tabs.sendMessage(tab.id, message);
} catch (err) {
const msg = String(err?.message || err || '');
const url = String(tab.url || '');
const injectable = /^https?:/i.test(url);
if (!injectable || !msg.includes('Receiving end does not exist')) {
throw err;
}
await chrome.scripting.executeScript({
target: { tabId: tab.id, allFrames: true },
files: ['content.js']
});
return await chrome.tabs.sendMessage(tab.id, message);
}
}
// ── Returns null instead of throwing so apiFetch can attempt auto-refresh ─────
async function getApiToken() {
const { ncApiToken } = await storageGet(['ncApiToken']);
return ncApiToken || null;
}
// ── Token refresh using saved credentials ─────────────────────────────────────
async function refreshToken() {
const { ncSavedUsername, ncSavedPassword, ncTokenUrl, ncClientId, ncClientSecret } =
await storageGet(['ncSavedUsername', 'ncSavedPassword', 'ncTokenUrl', 'ncClientId', 'ncClientSecret']);
if (!ncSavedUsername || !ncSavedPassword) {
throw new Error('No saved credentials — please sign in again in Settings.');
}
const tokenUrl = ncTokenUrl || 'https://api.nowcerts.com/token';
const params = new URLSearchParams();
params.set('grant_type', 'password');
params.set('username', ncSavedUsername);
params.set('password', ncSavedPassword);
if (ncClientId) params.set('client_id', ncClientId);
if (ncClientSecret) params.set('client_secret', ncClientSecret);
const res = await fetch(tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString()
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Token refresh failed (${res.status}): ${text}`);
}
const data = await res.json();
const token = data?.access_token || data?.token || data?.AccessToken;
if (!token) throw new Error('Token refresh response did not include access_token.');
await storageSet({ ncApiToken: token });
console.log('[Carrier Bridge] Token auto-refreshed successfully.');
return token;
}
// ── Single apiFetch with auto-refresh on 401 ──────────────────────────────────
async function apiFetch(url, options = {}) {
let token = await getApiToken();
// No token at all — try a refresh before giving up
if (!token) {
console.warn('[Carrier Bridge] No token found — attempting auto-refresh...');
try {
token = await refreshToken();
} catch (e) {
throw new Error('No API token. Please sign in via Settings.');
}
}
const doRequest = async (t) => fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${t}`,
'Accept': 'application/json',
...(options.headers || {})
}
});
let res = await doRequest(token);
// Token expired mid-session — auto-refresh and retry once
if (res.status === 401) {
console.warn('[Carrier Bridge] Token expired — attempting auto-refresh...');
try {
token = await refreshToken();
res = await doRequest(token);
} catch (refreshErr) {
throw new Error('Session expired and auto-refresh failed: ' + refreshErr.message);
}
}
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`API Error ${res.status}: ${text || res.statusText}`);
}
return res.json();
}
async function saveNote(insuredId, htmlContent) {
const body = {
insured_database_id: insuredId,
subject: htmlContent,
body: htmlContent,
creator_name: "Carrier Bridge Extension",
type: "General"
};
return apiFetch('https://api.nowcerts.com/api/Zapier/InsertNote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
async function getInsuredFiles(insuredId, folderId = null) {
let url = `https://api.nowcerts.com/api/Insured/GetInsuredFilesList?insuredId=${insuredId}&isInsuredVisibleFolder=false`;
if (folderId) url += `&folderId=${folderId}`;
return apiFetch(url);
}
async function createFolder(insuredId, parentId, name) {
const body = {
parentId: parentId,
name: name,
InsuredDatabaseId: insuredId,
creatorName: "Carrier Bridge Extension"
};
return apiFetch('https://api.nowcerts.com/api/Files/CreateFolder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
// uploadFile bypasses apiFetch (FormData can't use JSON headers) but still
// needs auto-refresh support, so we handle 401 manually here
async function uploadFile(insuredId, file, folderId = null) {
const url = new URL('https://api.nowcerts.com/api/Insured/UploadInsuredFile');
url.searchParams.append('insuredId', insuredId);
if (folderId) url.searchParams.append('folderId', folderId);
url.searchParams.append('creatorName', 'Carrier Bridge Extension');
url.searchParams.append('isInsuredVisibleFolder', 'false');
const formData = new FormData();
formData.append('file', file);
let token = await getApiToken();
if (!token) token = await refreshToken();
const doUpload = async (t) => fetch(url.toString(), {
method: 'PUT',
headers: { 'Authorization': `Bearer ${t}` },
body: formData
});
let res = await doUpload(token);
if (res.status === 401) {
token = await refreshToken();
res = await doUpload(token);
}
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`Upload failed ${res.status}: ${text || res.statusText}`);
}
return res.json();
}
// ── EXPIRATION ALERTS ─────────────────────────────────────────────────────────
function getDaysUntil(dateStr) {
if (!dateStr) return null;
const d = new Date(dateStr);
if (isNaN(d.getTime())) return null;
const now = new Date();
now.setHours(0,0,0,0);
d.setHours(0,0,0,0);
return Math.round((d - now) / (1000*60*60*24));
}
function renderExpirationAlerts(client) {
const container = $('expiration-alerts');
if (!container) return;
container.innerHTML = '';
const policies = Array.isArray(client?.policies) ? client.policies : [];
const alerts = [];
policies.forEach((p) => {
const exp = p.ExpirationDate || p.expirationDate;
if (!exp) return;
const days = getDaysUntil(exp);
if (days === null) return;
const carrier = p.CarrierName || p.carrierName || p.CompanyName || p.companyName || 'Unknown Carrier';
const num = p.Number || p.number || p.PolicyNumber || '';
const lob = p.LineOfBusinessName || p.lineOfBusinessName || p.LineOfBusiness || p.lineOfBusiness || '';
alerts.push({ days, carrier, num, lob, exp });
});
const urgent = alerts.filter(a => a.days >= 0 && a.days <= 30);
const warning = alerts.filter(a => a.days > 30 && a.days <= 60);
const expired = alerts.filter(a => a.days < 0);
if (!urgent.length && !warning.length && !expired.length) { container.classList.add('hidden'); return; }
container.classList.remove('hidden');
const makeAlert = (a, type) => {
const div = document.createElement('div');
div.className = `exp-alert exp-alert-${type}`;
const label = a.days < 0 ? `Expired ${Math.abs(a.days)}d ago` : a.days === 0 ? 'Expires TODAY' : `Expires in ${a.days}d`;
const desc = [a.lob, a.carrier, a.num ? `#${a.num}` : ''].filter(Boolean).join(' · ');
div.innerHTML = `${label}${desc}`;
return div;
};
if (expired.length || urgent.length) {
const hdr = document.createElement('div');
hdr.className = 'exp-header exp-header-urgent';
hdr.textContent = '⚠️ Policy Alert' + (expired.length ? ` — ${expired.length} Expired` : '') + (urgent.length ? ` — ${urgent.length} Expiring Soon` : '');
container.appendChild(hdr);
expired.forEach(a => container.appendChild(makeAlert(a, 'expired')));
urgent.forEach(a => container.appendChild(makeAlert(a, 'urgent')));
}
if (warning.length) {
const hdr = document.createElement('div');
hdr.className = 'exp-header exp-header-warning';
hdr.textContent = '🕐 Upcoming Renewals (31–60 days)';
container.appendChild(hdr);
warning.forEach(a => container.appendChild(makeAlert(a, 'warning')));
}
}
// ─────────────────────────────────────────────────────────────────────────────
// ── Render the paste fields for a client ──────────────────────────────────────
function renderPasteFields(client) {
const container = $('paste-fields');
if (!container) return;
container.innerHTML = '';
const executePaste = async (val) => {
try {
await sendToActiveTab({ action: 'paste_to_focused', value: String(val || '') });
} catch (e) {
alert('Paste failed: ' + (e?.message || String(e)));
}
};
const handleSaveField = async (newVal, key) => {
client[key] = newVal;
await storageSet({ activeClient: client });
try {
await syncInsuredToNowCerts(client);
console.log(`[Carrier Bridge] Synced ${key} to NowCerts.`);
} catch (e) {
console.warn(`[Carrier Bridge] Direct sync failed for ${key}, falling back to note.`, e);
const noteHTML = `INFO QUICK-EDITED via Extension:
${key} was updated to: ${newVal}`;
saveNote(activeClientId, noteHTML).catch(err => console.warn('Silent note save failed', err));
}
};
// ── Client Info ──
const clientHeader = document.createElement('h4');
clientHeader.style.cssText = 'margin:12px 0 6px 0;font-size:13px;color:#1e293b;';
clientHeader.textContent = 'Client Info';
container.appendChild(clientHeader);
const clientFields = [
{ label: 'Commercial', value: client.CommercialName || client.commercialName || '', key: 'CommercialName' },
{ label: 'First Name', value: client.FirstName || client.firstName || '', key: 'FirstName' },
{ label: 'Middle Name', value: client.MiddleName || client.middleName || client.MI || '', key: 'MiddleName' },
{ label: 'Last Name', value: client.LastName || client.lastName || '', key: 'LastName' },
{ label: 'Email', value: client.Email || client.email || client.PrimaryEmail || '', key: 'Email' },
{ label: 'Phone', value: client.Phone || client.phone || client.PrimaryPhone || '', key: 'Phone' },
{ label: 'Mobile', value: client.CellPhone || client.cellPhone || client.MobilePhone || '', key: 'CellPhone' },
{ label: 'Home Phone', value: client.HomePhone || client.homePhone || '', key: 'HomePhone' },
{ label: 'DOB', value: formatDisplayDate(client.BirthDate || client.dateOfBirth || client.DOB || ''), key: 'BirthDate', ageLabel: true },
{ label: 'SSN', value: client.SocialSecurityNumber || client.SSN || client.socialSecurityNumber || '', key: 'SocialSecurityNumber' },
{ label: 'Tax ID', value: client.TaxId || client.taxId || client.TaxID || '', key: 'TaxId' },
{ label: 'Street', value: client.Address1 || client.address1 || client.addressLine1 || '', key: 'Address1' },
{ label: 'Apt/Suite', value: client.Address2 || client.address2 || '', key: 'Address2' },
{ label: 'City', value: client.City || client.city || '', key: 'City' },
{ label: 'State', value: client.State || client.state || '', key: 'State' },
{ label: 'State Abbr', value: getClientStateAbbr(client), key: 'State' },
{ label: 'State Name', value: getClientStateName(client), key: 'State' },
{ label: 'Zip', value: client.Zip || client.zip || client.zipCode || '', key: 'Zip' },
];
const hideIfEmpty = ['CommercialName', 'MiddleName', 'CellPhone', 'HomePhone', 'TaxId', 'Address2'];
clientFields.forEach(f => {
if (hideIfEmpty.includes(f.key) && !f.value) return;
const label = f.ageLabel
? f.label + (() => { const age = calculateAge(client.BirthDate || client.dateOfBirth || client.DOB); return age ? ' (Age ' + age + ')' : ''; })()
: f.label;
container.appendChild(makePasteRow(label, f.value, executePaste, { key: f.key, onSave: handleSaveField }));
});
// ── Vehicles ──
if (Array.isArray(client.vehicles) && client.vehicles.length > 0) {
const h = document.createElement('h4');
h.style.cssText = 'margin:16px 0 6px 0;font-size:13px;color:#1e293b;';
h.textContent = `Vehicles (${client.vehicles.length})`;
container.appendChild(h);
client.vehicles.forEach((v, i) => {
const vin = v.VIN || v.vin || '';
const desc = `${v.Year || v.year || ''} ${v.Make || v.make || ''} ${v.Model || v.model || ''}`.trim();
const deductibles = extractVehicleDeductiblesDetailed(v);
if (vin) container.appendChild(makePasteRow(`VIN #${i+1}${desc ? ' (' + desc + ')' : ''}`, vin, executePaste));
if (desc) container.appendChild(makePasteRow(`Vehicle #${i+1}`, desc, executePaste));
if (deductibles.comp.value) container.appendChild(makePasteRow(`Comp Deductible #${i+1}${desc ? ` (${desc})` : ''}`, deductibles.comp.value, executePaste, null, deductibles.comp));
if (deductibles.collision.value) container.appendChild(makePasteRow(`Collision Deductible #${i+1}${desc ? ` (${desc})` : ''}`, deductibles.collision.value, executePaste, null, deductibles.collision));
});
}
// ── Drivers ──
if (Array.isArray(client.drivers) && client.drivers.length > 0) {
const h = document.createElement('h4');
h.style.cssText = 'margin:16px 0 6px 0;font-size:13px;color:#1e293b;';
h.textContent = `Drivers (${client.drivers.length})`;
container.appendChild(h);
client.drivers.forEach((d, i) => {
const name = `${d.firstName || d.FirstName || ''} ${d.lastName || d.LastName || ''}`.trim();
const lic = d.LicenseNumber || d.licenseNumber || '';
const dob = formatDisplayDate(d.birthday || d.BirthDate || d.dateOfBirth || d.DOB || '');
if (name) container.appendChild(makePasteRow(`Driver #${i+1}`, name, executePaste));
if (lic) container.appendChild(makePasteRow(`License #${i+1}`, lic, executePaste));
if (dob) container.appendChild(makePasteRow(`DOB #${i+1}`, dob, executePaste));
});
}
// ── Policies ──
if (Array.isArray(client.policies) && client.policies.length > 0) {
const h = document.createElement('h4');
h.style.cssText = 'margin:16px 0 6px 0;font-size:13px;color:#1e293b;';
h.textContent = `Policies (${client.policies.length})`;
container.appendChild(h);
client.policies.forEach((p, i) => {
const num = p.Number || p.number || p.PolicyNumber || '';
const eff = formatDisplayDate(p.EffectiveDate || p.effectiveDate || '');
const exp = formatDisplayDate(p.ExpirationDate || p.expirationDate || '');
const carrier = p.CarrierName || p.carrierName || p.CompanyName || p.companyName || (p.Carrier && p.Carrier.Name) || (p.Company && p.Company.Name) || '';
if (num) container.appendChild(makePasteRow(`Policy #${i+1}`, num, executePaste));
if (eff) container.appendChild(makePasteRow(`Eff Date #${i+1}`, eff, executePaste));
if (exp) container.appendChild(makePasteRow(`Exp Date #${i+1}`, exp, executePaste));
if (carrier) container.appendChild(makePasteRow(`Carrier #${i+1}`, carrier, executePaste));
const billUrl = getBillPayUrl(carrier);
if (billUrl) {
const payRow = document.createElement('div');
payRow.className = 'paste-row';
payRow.style.background = '#eff6ff';
payRow.style.borderColor = '#bfdbfe';
payRow.innerHTML = `