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 = `
💳 Quick Pay
${carrier}
`; const payBtn = document.createElement('button'); payBtn.className = 'paste-btn'; payBtn.style.background = '#3b82f6'; payBtn.style.color = 'white'; payBtn.textContent = 'Pay Bill ↗'; payBtn.addEventListener('click', () => chrome.tabs.create({ url: billUrl })); payRow.querySelector('.paste-btn-container').appendChild(payBtn); container.appendChild(payRow); } }); } const currentProperty = getCurrentProperty(client); if (currentProperty) { const propertyPolicy = getCurrentPolicy(client); const propertyLimits = extractPropertyLimitsDetailed(currentProperty, propertyPolicy); const h = document.createElement('h4'); h.style.cssText = 'margin:16px 0 6px 0;font-size:13px;color:#1e293b;'; h.textContent = 'Property'; container.appendChild(h); const propertyAddress = [ currentProperty.Address1 || currentProperty.address1 || currentProperty.addressLine1, currentProperty.City || currentProperty.city, currentProperty.State || currentProperty.state, currentProperty.Zip || currentProperty.zip || currentProperty.zipCode ].filter(Boolean).join(', '); if (propertyAddress) { container.appendChild(makePasteRow('Property Address', propertyAddress, executePaste)); } if (propertyLimits.dwelling.value) { container.appendChild(makePasteRow('Building / Dwelling', propertyLimits.dwelling.value, executePaste, null, propertyLimits.dwelling)); } if (propertyLimits.personalProperty.value) { container.appendChild(makePasteRow('Personal Property', propertyLimits.personalProperty.value, executePaste, null, propertyLimits.personalProperty)); } if (propertyLimits.businessPersonalProperty.value) { container.appendChild(makePasteRow('Business Personal Property', propertyLimits.businessPersonalProperty.value, executePaste, null, propertyLimits.businessPersonalProperty)); } } if (container.children.length < 3) { const note = document.createElement('p'); note.style.cssText = 'font-size:11px;color:#64748b;text-align:center;'; note.textContent = 'No additional policy/vehicle data found.'; container.appendChild(note); } updatePasteFieldVisibility(); } // ── Main client display ──────────────────────────────────────────────────────── // ── ERIE LIFE QUOTE PROMPT ──────────────────────────────────────────────────── function hasEriePolicy(client) { const policies = Array.isArray(client?.policies) ? client.policies : []; return policies.some((p) => { const carrier = (p.CarrierName || p.carrierName || p.CompanyName || p.companyName || '').toLowerCase(); return carrier.includes('erie'); }); } function renderBirthdayBanner(client) { const banner = $('birthday-banner'); if (!banner) return; banner.className = 'hidden'; const dobStr = client?.BirthDate || client?.dateOfBirth || client?.DOB || client?.birthDate || ''; if (!dobStr) return; let dobMonth, dobDay; const isoMatch = String(dobStr).match(/^(\d{4})-(\d{2})-(\d{2})/); const slashMatch = String(dobStr).match(/^(\d{2})\/(\d{2})\/(\d{4})/); if (isoMatch) { dobMonth = parseInt(isoMatch[2], 10) - 1; dobDay = parseInt(isoMatch[3], 10); } else if (slashMatch) { dobMonth = parseInt(slashMatch[1], 10) - 1; dobDay = parseInt(slashMatch[2], 10); } else { const d = new Date(dobStr); if (isNaN(d.getTime())) return; dobMonth = d.getMonth(); dobDay = d.getDate(); } const today = new Date(); const yr = today.getFullYear(); const todayNorm = new Date(yr, today.getMonth(), today.getDate()); let bday = new Date(yr, dobMonth, dobDay); // Check previous/next year if needed for wrapping let diffDays = Math.round((bday - todayNorm) / 86400000); if (diffDays < -180) { bday = new Date(yr + 1, dobMonth, dobDay); diffDays = Math.round((bday - todayNorm) / 86400000); } if (diffDays > 180) { bday = new Date(yr - 1, dobMonth, dobDay); diffDays = Math.round((bday - todayNorm) / 86400000); } const firstName = client?.FirstName || client?.firstName || 'this client'; let icon, message, cls; if (diffDays === 0) { icon = '🎂🎉🥳'; message = `Today is ${firstName}'s birthday! Wish them a Happy Birthday!`; cls = 'birthday-today'; } else if (diffDays < 0 && diffDays >= -14) { const d = Math.abs(diffDays); icon = '🎉'; message = `${firstName}'s birthday was ${d} day${d === 1 ? '' : 's'} ago — wish them a belated Happy Birthday!`; cls = 'birthday-past'; } else if (diffDays > 0 && diffDays <= 14) { icon = '🎈'; message = `${firstName}'s birthday is in ${diffDays} day${diffDays === 1 ? '' : 's'} — wish them an early Happy Birthday!`; cls = 'birthday-upcoming'; } else { return; } banner.className = cls; banner.innerHTML = `${icon}${message}`; } function renderLifeQuotePrompt(client) { const container = $('life-quote-prompt'); if (!container) return; container.innerHTML = ''; container.classList.add('hidden'); const dob = client?.BirthDate || client?.dateOfBirth || client?.DOB; const age = parseInt(calculateAge(dob), 10); if (!age || age >= 50) return; if (!hasEriePolicy(client)) return; // Check if already asked this session (stored by client id) const clientId = client?.Id || client?.id || client?.DatabaseId || ''; const storageKey = 'lifeQuoteAsked_' + clientId; container.classList.remove('hidden'); const name = client?.FirstName || client?.firstName || 'your client'; container.innerHTML = ''; const hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;'; hdr.innerHTML = '💚Life Quote Opportunity'; container.appendChild(hdr); const ageBadge = document.createElement('div'); ageBadge.style.cssText = 'font-size:11px;color:#166534;background:#dcfce7;border-radius:99px;padding:2px 10px;display:inline-block;margin-bottom:8px;'; ageBadge.textContent = age + ' years old · Erie insured · Under 50'; container.appendChild(ageBadge); const scriptBox = document.createElement('div'); scriptBox.style.cssText = 'background:#f0fdf4;border:1px solid #bbf7d0;border-radius:var(--radius-md);padding:10px 12px;font-size:12px;color:#1e293b;line-height:1.6;margin-bottom:10px;font-style:italic;'; scriptBox.innerHTML = '"Hey ' + name + ', I actually wanted to mention something — Erie just released a bundling discount program for customers who also have life insurance with them. I can run a quick quote and see how much it could save you on your current policy. Would that be alright?"'; container.appendChild(scriptBox); const checkRow = document.createElement('label'); checkRow.style.cssText = 'display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;color:#166534;cursor:pointer;padding:8px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:var(--radius-sm);'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.style.cssText = 'width:16px;height:16px;accent-color:#16a34a;cursor:pointer;'; checkbox.id = 'life-quote-asked-cb'; // Restore checked state if already asked chrome.storage.local.get([storageKey]).then((res) => { if (res[storageKey]) { checkbox.checked = true; checkRow.style.opacity = '0.6'; checkRow.querySelector('span').textContent = '✅ Already asked this client'; } }); checkbox.addEventListener('change', async () => { if (checkbox.checked) { await chrome.storage.local.set({ [storageKey]: true }); checkRow.style.opacity = '0.6'; checkRow.querySelector('span').textContent = '✅ Already asked this client'; } else { await chrome.storage.local.remove(storageKey); checkRow.style.opacity = '1'; checkRow.querySelector('span').textContent = 'I asked the client about a life quote'; } }); const checkLabel = document.createElement('span'); checkLabel.textContent = 'I asked the client about a life quote'; checkRow.appendChild(checkbox); checkRow.appendChild(checkLabel); container.appendChild(checkRow); } // ───────────────────────────────────────────────────────────────────────────── // ── MISSING DATA PROMPT ─────────────────────────────────────────────────────── const MISSING_FIELD_SCRIPTS = { 'DOB': `"Could I get your date of birth for our records?"`, 'Primary Phone': `"What's the best phone number to reach you at?"`, 'Primary Email': `"Do you have an email address we can use to send you policy documents?"`, 'Street Address':`"Can I confirm your mailing address with you?"`, 'City': `"And what city is that in?"`, 'State': `"What state are you in?"`, 'ZIP': `"What's your ZIP code?"`, }; // Maps display field name → { clientKey, inputType, placeholder } const MISSING_FIELD_META = { 'DOB': { clientKey: 'BirthDate', inputType: 'date', placeholder: 'MM/DD/YYYY' }, 'Primary Phone': { clientKey: 'Phone', inputType: 'tel', placeholder: '(555) 555-5555' }, 'Primary Email': { clientKey: 'Email', inputType: 'email', placeholder: 'email@example.com' }, 'Street Address':{ clientKey: 'Address1', inputType: 'text', placeholder: '123 Main St' }, 'City': { clientKey: 'City', inputType: 'text', placeholder: 'City' }, 'State': { clientKey: 'State', inputType: 'text', placeholder: 'OH' }, 'ZIP': { clientKey: 'Zip', inputType: 'text', placeholder: '12345' }, }; function renderMissingDataPrompt(client) { const container = $('missing-data-prompt'); if (!container) return; container.innerHTML = ''; container.classList.add('hidden'); const missing = getMissingImportantFields(client); // Only show for fields we have scripts for, skip First/Last name const actionable = missing.filter(f => MISSING_FIELD_SCRIPTS[f]); if (!actionable.length) return; container.classList.remove('hidden'); const hdr = document.createElement('div'); hdr.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:8px;'; hdr.innerHTML = '📝Ask Client for Missing Info'; container.appendChild(hdr); const sub = document.createElement('div'); sub.style.cssText = 'font-size:11px;color:#92400e;margin-bottom:10px;'; sub.textContent = actionable.length + ' field' + (actionable.length > 1 ? 's' : '') + ' missing — suggested scripts below:'; container.appendChild(sub); const clientId = client?.Id || client?.id || client?.DatabaseId || ''; actionable.forEach((field) => { const script = MISSING_FIELD_SCRIPTS[field]; const meta = MISSING_FIELD_META[field]; const storageKey = 'missingAsked_' + clientId + '_' + field.replace(/\s/g,''); const row = document.createElement('div'); row.style.cssText = 'background:white;border:1px solid #fde68a;border-radius:var(--radius-sm);padding:8px 10px;margin-bottom:6px;'; const fieldLabel = document.createElement('div'); fieldLabel.style.cssText = 'font-size:11px;font-weight:700;color:#78350f;margin-bottom:4px;'; fieldLabel.textContent = field; row.appendChild(fieldLabel); const scriptEl = document.createElement('div'); scriptEl.style.cssText = 'font-size:12px;color:#1e293b;font-style:italic;margin-bottom:8px;line-height:1.5;'; scriptEl.textContent = script; row.appendChild(scriptEl); // ── Input + Save row ────────────────────────────────────────────────────── if (meta) { const inputRow = document.createElement('div'); inputRow.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:8px;'; const inp = document.createElement('input'); inp.type = meta.inputType; inp.placeholder = meta.placeholder; inp.style.cssText = 'flex:1;margin:0;padding:6px 8px;font-size:12px;'; const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style.cssText = 'width:auto;margin:0;padding:6px 12px;font-size:11px;background:#d97706;flex-shrink:0;'; const statusEl = document.createElement('span'); statusEl.style.cssText = 'font-size:11px;min-width:20px;'; saveBtn.addEventListener('click', async () => { const val = inp.value.trim(); if (!val) { statusEl.textContent = '⚠️'; return; } saveBtn.disabled = true; saveBtn.innerHTML = 'Saving…'; statusEl.textContent = ''; try { // Pull the latest stored client, apply the new value, then push it const { activeClient } = await storageGet(['activeClient']); if (!activeClient) throw new Error('No active client'); activeClient[meta.clientKey] = val; // Some fields have aliases — keep them in sync locally if (field === 'DOB') { activeClient.dateOfBirth = val; activeClient.DateOfBirth = val; } if (field === 'Primary Phone') { activeClient.phone = val; } if (field === 'Primary Email') { activeClient.email = val; activeClient.PrimaryEmail = val; } if (field === 'Street Address'){ activeClient.address1 = val; activeClient.addressLine1 = val; } if (field === 'City') { activeClient.city = val; } if (field === 'State') { activeClient.state = val; } if (field === 'ZIP') { activeClient.zip = val; activeClient.zipCode = val; } await storageSet({ activeClient }); await syncInsuredToNowCerts(activeClient); statusEl.textContent = '✅'; saveBtn.innerHTML = 'Saved'; saveBtn.style.background = '#059669'; inp.value = ''; inp.placeholder = val; // Re-render to clear the field from the missing list renderActiveClient(activeClient); } catch (err) { console.error('Failed to save field to NowCerts', err); statusEl.textContent = '❌'; saveBtn.disabled = false; saveBtn.innerHTML = 'Save'; } }); inputRow.appendChild(inp); inputRow.appendChild(saveBtn); inputRow.appendChild(statusEl); row.appendChild(inputRow); } // ── "I asked the client" checkbox ───────────────────────────────────────── const checkRow = document.createElement('label'); checkRow.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:#92400e;cursor:pointer;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.style.cssText = 'accent-color:#d97706;cursor:pointer;'; const cbLabel = document.createElement('span'); cbLabel.textContent = 'I asked the client'; chrome.storage.local.get([storageKey]).then((res) => { if (res[storageKey]) { cb.checked = true; cbLabel.textContent = '✅ Asked'; row.style.opacity = '0.55'; } }); cb.addEventListener('change', async () => { if (cb.checked) { await chrome.storage.local.set({ [storageKey]: true }); cbLabel.textContent = '✅ Asked'; row.style.opacity = '0.55'; } else { await chrome.storage.local.remove(storageKey); cbLabel.textContent = 'I asked the client'; row.style.opacity = '1'; } }); checkRow.appendChild(cb); checkRow.appendChild(cbLabel); row.appendChild(checkRow); container.appendChild(row); }); } // ───────────────────────────────────────────────────────────────────────────── function renderActiveClient(client) { reorganizeActiveClientLayout(); if (!client) { setHidden('active-client', true); renderMissingDataWarning(null); hideProgress(); return; } client = normalizeClientCollections(client); setHidden('active-client', false); setText('active-name', client.FullName || client.commercialName || client.CommercialName || 'Selected Client'); activeClientId = client.Id || client.insuredId || client.DatabaseId || client.id || ''; renderMissingDataWarning(client); renderLifeQuotePrompt(client); renderMissingDataPrompt(client); renderBirthdayBanner(client); renderPasteFields(client); renderSnapshots(client); renderExpirationAlerts(client); renderCarrierPortals(client); // Notes uses onclick so re-assigning is safe — no listener stacking renderNotesSection(); // Documents guard flag prevents duplicate listeners on upload/folder buttons renderDocumentsSection(); } // ── Load full client data with progress bar ─────────────────────────────────── async function loadFullClient(basicClient) { const insuredId = basicClient.Id || basicClient.insuredId || basicClient.id || ''; if (!insuredId) return; renderActiveClient(basicClient); showProgress('Loading client details...', 10); try { let token = await getApiToken(); if (!token) { try { token = await refreshToken(); } catch (e) { hideProgress(); return; } } const headers = { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' }; // Step 1: Full details showProgress('Loading client details...', 25); let insured = basicClient; try { const detailRes = await fetch(`https://api.nowcerts.com/api/InsuredDetailList?key=${insuredId}&Active=true&showAll=true`, { headers }); if (detailRes.ok) { const detailJson = await detailRes.json(); const raw = Array.isArray(detailJson?.value) ? detailJson.value[0] : Array.isArray(detailJson) ? detailJson[0] : detailJson || {}; insured = { ...basicClient, ...raw, FullName: raw.FullName || raw.fullName || basicClient.FullName || `${raw.FirstName || raw.firstName || ''} ${raw.LastName || raw.lastName || ''}`.trim(), FirstName: raw.FirstName || raw.firstName || basicClient.FirstName || '', LastName: raw.LastName || raw.lastName || basicClient.LastName || '', Email: raw.Email || raw.email || basicClient.Email || '', Phone: raw.Phone || raw.phone || raw.cellPhone || basicClient.Phone || '', Address1: raw.Address1 || raw.address1 || raw.addressLine1 || basicClient.Address1 || '', Address2: raw.Address2 || raw.address2 || raw.addressLine2 || basicClient.Address2 || '', City: raw.City || raw.city || basicClient.City || '', State: raw.State || raw.state || raw.StateAbbreviation || basicClient.State || '', Zip: raw.Zip || raw.zip || raw.zipCode || basicClient.Zip || '', BirthDate: raw.BirthDate || raw.dateOfBirth || raw.DateOfBirth || basicClient.BirthDate || '', SSN: raw.SocialSecurityNumber || raw.SSN || raw.socialSecurityNumber || '', Id: raw.Id || raw.id || raw.DatabaseId || insuredId, policies: basicClient.policies || [], vehicles: basicClient.vehicles || [], drivers: basicClient.drivers || [], }; } } catch (e) { console.warn('Detail fetch failed', e); } renderActiveClient(insured); showProgress('Loading policies & contacts...', 50); // Step 2: Policies AND Commercial Contacts in parallel let policies = []; let contacts = []; try { const [polRes, conRes] = await Promise.all([ fetch('https://api.nowcerts.com/api/Insured/InsuredPolicies', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ insuredDataBaseId: [insuredId] }) }), fetch('https://api.nowcerts.com/api/Insured/InsuredContacts', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ insuredDataBaseId: [insuredId] }) }) ]); if (polRes.ok) { const polJson = await polRes.json(); policies = Array.isArray(polJson) ? polJson : (polJson?.value || []); } if (conRes.ok) { const conJson = await conRes.json(); contacts = Array.isArray(conJson) ? conJson : (conJson?.value || []); } } catch (e) { console.warn('Policies/Contacts fetch failed', e); } // Extract Commercial Primary Contact let primaryContact = null; if (Array.isArray(contacts) && contacts.length > 0) { primaryContact = contacts.find(c => c.primaryContact === true || c.PrimaryContact === true) || contacts[0]; } if (primaryContact) { const pcFirst = primaryContact.firstName || primaryContact.FirstName || ''; const pcLast = primaryContact.lastName || primaryContact.LastName || ''; const pcEmail = primaryContact.businessEMail || primaryContact.personalEMail || primaryContact.email || ''; const pcPhone = primaryContact.cellPhone || primaryContact.officePhone || primaryContact.homePhone || primaryContact.phone || ''; insured.FirstName = insured.FirstName || pcFirst; insured.LastName = insured.LastName || pcLast; insured.FullName = insured.FullName === insured.CommercialName ? `${pcFirst} ${pcLast}`.trim() : insured.FullName; insured.Email = insured.Email || pcEmail; insured.Phone = insured.Phone || pcPhone; } insured = normalizeClientCollections({ ...insured, policies, contacts }); renderActiveClient(insured); showProgress('Loading vehicles, drivers & properties...', 75); // Step 3 & 4: Vehicles, Drivers, and Properties in parallel let vehicles = [], drivers = [], properties = []; const policyIds = policies.map(p => p.DatabaseId || p.databaseId || p.PolicyDatabaseId || '').filter(Boolean); if (policyIds.length) { await Promise.all([ fetch('https://api.nowcerts.com/api/Policy/PolicyVehicles', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }).then(r => r.ok ? r.json() : []).then(d => { vehicles = Array.isArray(d) ? d : []; }).catch(() => {}), fetch('https://api.nowcerts.com/api/Policy/PolicyDrivers', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }).then(r => r.ok ? r.json() : []).then(d => { drivers = Array.isArray(d) ? d : []; }).catch(() => {}), fetch('https://api.nowcerts.com/api/Policy/PolicyProperties', { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ policyDataBaseId: policyIds }) }).then(r => r.ok ? r.json() : []).then(d => { properties = Array.isArray(d) ? d : []; }).catch(() => {}), ]); } showProgress('Done!', 100); let fullClient = normalizeClientCollections({ ...insured, policies, vehicles, drivers, properties }); fullClient = await applyLearnedData(fullClient); await storageSet({ activeClient: fullClient }); await pushRecentClient(fullClient); renderActiveClient(fullClient); renderRecentClients(); setTimeout(hideProgress, 600); } catch (e) { console.error('loadFullClient error', e); hideProgress(); } } async function selectClient(client) { await storageSet({ activeClient: client }); await pushRecentClient(client); renderActiveClient(client); renderRecentClients(); loadFullClient(client); } async function renderRecentClients() { const container = $('recent-clients'); if (!container) return; const recents = await getRecentClients(); container.innerHTML = ''; if (!recents.length) { container.innerHTML = '
No recent clients yet.
'; return; } recents.forEach((client) => { const row = document.createElement('div'); row.className = 'recent-client-row'; const meta = document.createElement('div'); const name = document.createElement('div'); name.className = 'recent-client-name'; name.textContent = getClientDisplayName(client); const sub = document.createElement('div'); sub.className = 'recent-client-meta'; sub.textContent = [client.City, getClientStateAbbr(client), client.Email || client.Phone].filter(Boolean).join(' · '); meta.appendChild(name); meta.appendChild(sub); const btn = document.createElement('button'); btn.className = 'mini-btn'; btn.textContent = 'Open'; btn.addEventListener('click', () => selectClient(client)); row.appendChild(meta); row.appendChild(btn); container.appendChild(row); }); } function updatePasteFieldVisibility() { const searchTerm = ($('supapaste-search')?.value || '').toLowerCase().trim(); const showMissingOnly = !!$('show-missing-only')?.checked; const collapseEmptySections = !!$('collapse-empty-sections')?.checked; const rows = document.querySelectorAll('#paste-fields .paste-row'); rows.forEach((row) => { const text = row.textContent.toLowerCase(); const input = row.querySelector('input'); const val = input ? String(input.value || '').trim().toLowerCase() : row.querySelector('.paste-value')?.textContent?.toLowerCase() || ''; const matchesSearch = !searchTerm || text.includes(searchTerm) || val.includes(searchTerm); const isMissing = !val; const matchesMissing = !showMissingOnly || isMissing; row.style.display = (matchesSearch && matchesMissing) ? 'flex' : 'none'; }); document.querySelectorAll('#paste-fields h4').forEach((h) => { let next = h.nextElementSibling; let hasVisible = false; while (next && next.tagName !== 'H4') { if (next.style.display !== 'none' && next.classList.contains('paste-row')) { hasVisible = true; break; } next = next.nextElementSibling; } h.style.display = collapseEmptySections && !hasVisible ? 'none' : 'block'; }); } // ── Search filter ───────────────────────────────────────────────────────────── $('supapaste-search')?.addEventListener('input', (e) => { updatePasteFieldVisibility(); }); // ── Notes section ───────────────────────────────────────────────────────────── function renderNotesSection() { const editor = $('note-editor'); if (!editor) return; editor.innerHTML = ''; document.querySelectorAll('.rich-toolbar button').forEach(btn => { btn.onclick = () => { document.execCommand(btn.dataset.cmd, false, null); editor.focus(); }; }); $('save-note-btn').onclick = async () => { if (!activeClientId) return alert('No client selected'); const content = editor.innerHTML.trim(); if (!content) return alert('Note is empty'); try { await saveNote(activeClientId, content); alert('Note saved successfully'); editor.innerHTML = ''; } catch (e) { alert('Failed to save note: ' + e.message); } }; $('log-quote-btn').onclick = async () => { if (!activeClientId) return alert('No client selected'); const carrier = $('quote-carrier').value.trim(); const premium = $('quote-premium').value.trim(); if (!carrier || !premium) return alert('Enter carrier and premium.'); const noteHTML = `Quote Logged: ${carrier} — $${premium}`; try { const { activeClient } = await storageGet(['activeClient']); await syncQuoteToNowCerts(activeClient || {}, carrier, premium); await saveNote(activeClientId, `${noteHTML}
Synced to NowCerts quote records.`); $('quote-carrier').value = ''; $('quote-premium').value = ''; alert('Quote synced to NowCerts.'); } catch (e) { try { await saveNote(activeClientId, `${noteHTML}
Quote sync failed, saved as note instead.`); } catch (_) {} alert('Quote sync failed: ' + e.message); } }; } // ── Documents / Files section ───────────────────────────────────────────────── async function loadFiles(folderId = null) { if (!activeClientId) { $('files-list').innerHTML = '
No client selected
'; $('current-path').textContent = 'Current: Root (Files)'; return; } const list = $('files-list'); list.innerHTML = '
Loading documents...
'; try { const response = await getInsuredFiles(activeClientId, folderId); const data = response.Data || response.data || response; currentFolderId = data.CurrentFolderId || data.currentFolderId || folderId; $('current-path').textContent = currentFolderId ? `Current: Folder (ID: ${currentFolderId})` : 'Current: Root (Files)'; list.innerHTML = ''; const items = data.Files || data.files || data.value || []; if (!items.length) { list.innerHTML = '
This folder is empty
'; return; } items.forEach(item => { const div = document.createElement('div'); div.className = 'file-item'; const isFolder = item.Type === 2 || item.type === 2 || item.fileOrFolder?.toLowerCase() === 'folder'; if (isFolder) { div.innerHTML = `📁 ${item.Name || item.name || 'Folder'}`; div.style.cursor = 'pointer'; div.onclick = () => loadFiles(item.DatabaseId || item.databaseId || item.id || item.Id); } else { div.innerHTML = `📄 ${item.Name || item.name || 'File'}`; const dlUrl = item.DownloadUrl || item.downloadUrl; if (dlUrl) { const link = document.createElement('a'); link.href = dlUrl; link.textContent = ' ↓'; link.target = '_blank'; link.style.cssText = 'margin-left:10px;font-size:11px;'; div.appendChild(link); } } list.appendChild(div); }); } catch (e) { list.innerHTML = `
Error: ${e.message}
`; } } let documentsInitialized = false; function renderDocumentsSection() { if (documentsInitialized) { // Already wired up — just refresh the file list for the new client loadFiles(null); return; } documentsInitialized = true; $('refresh-files-btn')?.addEventListener('click', () => { currentFolderId = null; loadFiles(null); }); const dropZone = $('drop-zone'); if (dropZone) { dropZone.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.onchange = (e) => handleFileUpload(e.target.files); input.click(); }); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); handleFileUpload(e.dataTransfer.files); }); } $('create-folder-btn')?.addEventListener('click', async () => { const name = $('folder-name')?.value.trim(); if (!name) return alert('Enter a folder name.'); if (!activeClientId) return alert('No client selected.'); try { await createFolder(activeClientId, currentFolderId, name); if ($('folder-name')) $('folder-name').value = ''; await loadFiles(currentFolderId); } catch (e) { alert('Failed to create folder: ' + e.message); } }); loadFiles(null); } async function handleFileUpload(files) { if (!files || !files.length) return; if (!activeClientId) return alert('No client selected.'); const status = $('upload-status'); for (const file of files) { if (status) status.textContent = `Uploading ${file.name}...`; try { await uploadFile(activeClientId, file, currentFolderId); if (status) status.textContent = `✅ ${file.name} uploaded`; await loadFiles(currentFolderId); } catch (e) { if (status) status.textContent = `❌ Failed: ${e.message}`; } } } // ── Search results ──────────────────────────────────────────────────────────── function renderResults(results) { const container = $('results-container'); if (!container) return; container.innerHTML = ''; if (!Array.isArray(results) || results.length === 0) { const p = document.createElement('p'); p.style.cssText = 'font-size:12px;color:#64748b;text-align:center;'; p.textContent = 'No matching clients found.'; container.appendChild(p); const btnRow = document.createElement('div'); btnRow.style.marginTop = '10px'; const btn = document.createElement('button'); btn.className = 'btn'; btn.textContent = 'Create Prospect'; btn.addEventListener('click', () => showCreateProspectForm(lastSearchQuery)); btnRow.appendChild(btn); container.appendChild(btnRow); return; } results.slice(0, 20).forEach((c) => { const card = document.createElement('div'); card.className = 'card'; const name = document.createElement('h3'); name.textContent = c.FullName || c.commercialName || c.CommercialName || 'Client'; const sub = document.createElement('p'); sub.className = 'subtext'; sub.textContent = [c.PolicyNumber ? `Policy ${c.PolicyNumber}` : '', c.Email, c.Phone || c.CellPhone, fmtAddress(c)].filter(Boolean).join(' · '); card.appendChild(name); card.appendChild(sub); const tag = document.createElement('div'); tag.className = 'tag'; tag.textContent = c.Id ? `ID: ${c.Id}` : 'Select'; card.appendChild(tag); card.addEventListener('click', async () => { await selectClient(c); }); container.appendChild(card); }); } // ── Create Prospect form ────────────────────────────────────────────────────── function showCreateProspectForm(seedText) { const container = $('results-container'); if (!container) return; container.innerHTML = ''; const seed = String(seedText || '').trim(); const parts = seed.split(/\s+/).filter(Boolean); const guessFirst = parts[0] || ''; const guessLast = parts.length > 1 ? parts.slice(1).join(' ') : ''; const card = document.createElement('div'); card.className = 'card'; const title = document.createElement('h3'); title.textContent = 'Create Prospect'; card.appendChild(title); const note = document.createElement('p'); note.style.fontSize = '11px'; note.textContent = 'Creates an insured in NowCerts and refreshes results.'; card.appendChild(note); const grid = document.createElement('div'); grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px;'; const mkField = (label, value = '', span2 = false, placeholder = '') => { const wrap = document.createElement('div'); if (span2) wrap.style.gridColumn = 'span 2'; const l = document.createElement('div'); l.style.fontSize = '11px'; l.textContent = label; const inp = document.createElement('input'); inp.style.marginBottom = '5px'; inp.value = value; if (placeholder) inp.placeholder = placeholder; wrap.appendChild(l); wrap.appendChild(inp); return { wrap, inp }; }; const fFirst = mkField('First name', guessFirst); const fLast = mkField('Last name', guessLast); const fEmail = mkField('Email', '', false, 'name@example.com'); const fPhone = mkField('Phone', '', false, '(555) 555-5555'); const fStreet = mkField('Street', '', true, '123 Main St'); const fCity = mkField('City'); const fState = mkField('State', '', false, 'OH'); const fZip = mkField('ZIP', '', false, '43950'); [fFirst, fLast, fEmail, fPhone, fStreet, fCity, fState, fZip].forEach(f => grid.appendChild(f.wrap)); card.appendChild(grid); const actions = document.createElement('div'); actions.style.cssText = 'display:flex;gap:8px;margin-top:10px;'; const btnCreate = document.createElement('button'); btnCreate.textContent = 'Create'; btnCreate.style.background = '#10b981'; const btnCancel = document.createElement('button'); btnCancel.textContent = 'Cancel'; btnCancel.style.background = '#64748b'; actions.appendChild(btnCreate); actions.appendChild(btnCancel); card.appendChild(actions); const status = document.createElement('div'); status.style.cssText = 'font-size:11px;margin-top:8px;'; card.appendChild(status); btnCancel.addEventListener('click', async () => { if (lastSearchQuery) { if ($('search-input')) $('search-input').value = lastSearchQuery; await doSearch(); } else { renderResults([]); } }); btnCreate.addEventListener('click', async () => { const payload = { firstName: fFirst.inp.value, lastName: fLast.inp.value, email: fEmail.inp.value, phone: fPhone.inp.value, street: fStreet.inp.value, city: fCity.inp.value, state: fState.inp.value, zip: fZip.inp.value, }; try { status.textContent = 'Creating...'; btnCreate.disabled = true; const resp = await chrome.runtime.sendMessage({ action: 'create_prospect', payload }); if (!resp?.ok) throw new Error(resp?.error || 'Create failed'); const results = Array.isArray(resp.results) ? resp.results : []; const match = results[0]; if (match) { await selectClient(match); } status.textContent = 'Created successfully.'; renderResults(results); } catch (e) { status.textContent = 'Error: ' + (e?.message || String(e)); } finally { btnCreate.disabled = false; } }); container.appendChild(card); } // ── Settings ────────────────────────────────────────────────────────────────── async function loadSettingsIntoUI() { const { ncApiToken, ncTokenUrl } = await storageGet(['ncApiToken', 'ncTokenUrl']); if ($('api-token')) $('api-token').value = ncApiToken ? '••••••••••••' : ''; if ($('token-url')) $('token-url').value = ncTokenUrl || 'https://api.nowcerts.com/token'; } // ── AI lock/unlock UI state ─────────────────────────────────────────────────── async function syncAiLockUI() { const { geminiApiKey } = await storageGet(['geminiApiKey']); const isUnlocked = !!geminiApiKey; setHidden('ai-locked-view', isUnlocked); setHidden('ai-unlocked-view', !isUnlocked); } async function doSearch() { const q = ($('search-input')?.value || '').trim(); if (!q) return; lastSearchQuery = q; $('search-btn').disabled = true; $('search-btn').textContent = 'Searching...'; try { const resp = await chrome.runtime.sendMessage({ action: 'search_nowcerts', query: q }); if (!resp?.ok) throw new Error(resp?.error || 'Search failed'); renderResults(resp.results || []); } catch (err) { alert(err.message); } finally { $('search-btn').disabled = false; $('search-btn').textContent = 'Search NowCerts'; } } async function saveTokenManual() { const token = ($('api-token')?.value || '').trim(); if (!token || token.includes('••••')) { alert('Paste the real token first.'); return; } await storageSet({ ncApiToken: token }); alert('Token saved.'); await loadSettingsIntoUI(); } async function clearToken() { await storageRemove(['ncApiToken']); alert('Token cleared.'); await loadSettingsIntoUI(); } async function loginAndSaveToken() { const status = $('login-status'); const setStatus = (t) => { if (status) status.textContent = t; }; const tokenUrl = ($('token-url')?.value || '').trim() || 'https://api.nowcerts.com/token'; const username = ($('nc-username')?.value || '').trim(); const password = ($('nc-password')?.value || '').trim(); const clientId = ($('nc-client-id')?.value || '').trim(); const clientSecret = ($('nc-client-secret')?.value || '').trim(); if (!username || !password) { alert('Enter your username and password.'); return; } $('login-save').disabled = true; $('login-save').textContent = 'Signing in...'; setStatus('Requesting token...'); try { // Save token URL first — no token yet at this point await storageSet({ ncTokenUrl: tokenUrl }); const params = new URLSearchParams(); params.set('grant_type', 'password'); params.set('username', username); params.set('password', password); if (clientId) params.set('client_id', clientId); if (clientSecret) params.set('client_secret', clientSecret); const res = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params.toString(), }); const text = await res.text(); let data = null; try { data = JSON.parse(text); } catch (_) {} if (!res.ok) { const msg = data && (data.error_description || data.error || data.message) ? (data.error_description || data.error || data.message) : text; throw new Error(msg || `Token request failed (${res.status})`); } // token declared here — AFTER the fetch resolves const token = data?.access_token || data?.token || data?.AccessToken; if (!token) throw new Error('Token response did not include access_token.'); // Save token AND credentials together so auto-refresh works await storageSet({ ncApiToken: token, ncSavedUsername: username, ncSavedPassword: password, ncClientId: clientId, ncClientSecret: clientSecret }); setStatus('Token saved.'); alert('Signed in and token saved.'); await loadSettingsIntoUI(); } catch (e) { setStatus('Login failed: ' + (e?.message || String(e))); alert('Login failed: ' + (e?.message || String(e))); } finally { $('login-save').disabled = false; $('login-save').textContent = 'Sign in and save token'; } } // ── Main ────────────────────────────────────────────────────────────────────── async function main() { reorganizeActiveClientLayout(); $('toggle-settings')?.addEventListener('click', () => { $('settings-view')?.classList.toggle('hidden'); }); // ── Debug Button ── $('debug-btn')?.addEventListener('click', async () => { const { activeClient } = await storageGet(['activeClient']); if (!activeClient) return alert("Please search and select a client first!"); const blob = new Blob([JSON.stringify(activeClient, null, 2)], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `NC_Debug_${activeClient.Id || 'Commercial_Client'}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); // ── AI Unlock Button ── $('ai-unlock-btn')?.addEventListener('click', async () => { const code = ($('ai-unlock-input')?.value || '').trim(); const status = $('ai-unlock-status'); if (code !== AGENCY_UNLOCK_CODE) { if (status) { status.textContent = '❌ Incorrect code. Contact your agency admin.'; status.style.color = '#ef4444'; } return; } await storageSet({ geminiApiKey: AGENCY_GEMINI_KEY }); if ($('ai-unlock-input')) $('ai-unlock-input').value = ''; if (status) status.textContent = ''; await syncAiLockUI(); alert('✅ AI features unlocked!'); }); // ── AI Re-lock Button ── $('ai-relock-btn')?.addEventListener('click', async () => { await storageRemove(['geminiApiKey']); await syncAiLockUI(); }); // ── AI Rescue Button ── $('ai-rescue-btn')?.addEventListener('click', async () => { const { activeClient, geminiApiKey } = await storageGet(['activeClient', 'geminiApiKey']); if (!activeClient) return alert("Please select a client first!"); if (!geminiApiKey) { $('settings-view').classList.remove('hidden'); return alert("AI features are not unlocked. Enter your agency unlock code in Settings."); } const btn = $('ai-rescue-btn'); const originalText = btn.textContent; btn.textContent = "🤖 AI is analyzing data structure..."; btn.disabled = true; try { const schema = getJsonSkeleton(activeClient); const aiRes = await chrome.runtime.sendMessage({ action: 'ai_data_rescue', apiKey: geminiApiKey, schema: schema }); if (!aiRes.ok) throw new Error(aiRes.error); const rescueLog = []; const skippedLog = []; const alreadyLog = []; for (const [standardKey, jsonPath] of Object.entries(aiRes.smartMap)) { if (activeClient[standardKey]) { alreadyLog.push({ standardKey, jsonPath }); } else if (jsonPath) { const foundValue = getValueFromPath(activeClient, jsonPath); if (foundValue) { activeClient[standardKey] = foundValue; rescueLog.push({ standardKey, jsonPath, value: foundValue }); console.log( `[AI Rescue] ✅ FILLED\n` + ` Extension key : "${standardKey}"\n` + ` NowCerts path : "${jsonPath}"\n` + ` Value : "${foundValue}"` ); } else { skippedLog.push({ standardKey, jsonPath }); console.warn( `[AI Rescue] ⚠️ PATH EMPTY\n` + ` Extension key : "${standardKey}"\n` + ` NowCerts path : "${jsonPath}"\n` + ` (path exists in AI map but no value found in NowCerts data)` ); } } } const { learnedDataMap = {} } = await storageGet(['learnedDataMap']); await storageSet({ learnedDataMap: { ...learnedDataMap, ...aiRes.smartMap } }); if (rescueLog.length > 0) { await storageSet({ activeClient }); renderActiveClient(activeClient); } let alertMsg = ''; if (rescueLog.length > 0) { alertMsg += `✅ FILLED (${rescueLog.length} field${rescueLog.length > 1 ? 's' : ''}):\n`; alertMsg += rescueLog.map(({ standardKey, jsonPath, value }) => ` • ${standardKey}\n ← NowCerts: ${jsonPath}\n Value: "${value}"` ).join('\n') + '\n'; } if (skippedLog.length > 0) { alertMsg += `\n⚠️ MAPPED BUT EMPTY IN NOWCERTS (${skippedLog.length}):\n`; alertMsg += `(AI found these paths but the fields have no data — may need a different field name in the extension)\n`; alertMsg += skippedLog.map(({ standardKey, jsonPath }) => ` • ${standardKey} → NowCerts path: ${jsonPath}` ).join('\n') + '\n'; } if (alreadyLog.length > 0) { alertMsg += `\nℹ️ ALREADY POPULATED (${alreadyLog.length}) — skipped:\n`; alertMsg += alreadyLog.map(({ standardKey, jsonPath }) => ` • ${standardKey} (mapped to: ${jsonPath})` ).join('\n'); } if (!alertMsg) { alertMsg = 'AI scanned the data but produced no field mappings. The NowCerts response may be in an unexpected format.'; } alert(alertMsg); } catch (e) { alert("AI Rescue failed: " + e.message); console.error('[AI Rescue] Error:', e); } finally { btn.textContent = originalText; btn.disabled = false; } }); // ── Standard button wiring ── $('search-btn')?.addEventListener('click', doSearch); $('create-prospect-btn')?.addEventListener('click', () => showCreateProspectForm(($('search-input')?.value || lastSearchQuery || '').trim())); $('search-input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); }); $('save-token')?.addEventListener('click', saveTokenManual); $('clear-token')?.addEventListener('click', clearToken); $('login-save')?.addEventListener('click', loginAndSaveToken); $('carrier-prefs-toggle')?.addEventListener('click', () => { $('carrier-prefs-panel')?.classList.toggle('hidden'); }); $('fill-form-btn')?.addEventListener('click', async () => { try { await sendToActiveTab({ action: 'fill_form' }); } catch (e) { alert('Auto-fill failed: ' + (e?.message || String(e))); } }); $('fill-acord-btn')?.addEventListener('click', async () => { try { await sendToActiveTab({ action: 'fill_acord_form' }); } catch (e) { alert('ACORD prefill failed: ' + (e?.message || String(e))); } }); $('preview-fill-btn')?.addEventListener('click', async () => { try { await sendToActiveTab({ action: 'preview_fill' }); } catch (e) { alert('Preview fill failed: ' + (e?.message || String(e))); } }); $('refresh-client-btn')?.addEventListener('click', async () => { const { activeClient } = await storageGet(['activeClient']); if (!activeClient) return alert('No active client selected.'); await loadFullClient(activeClient); }); $('show-missing-only')?.addEventListener('change', async (e) => { await setPopupUiPrefs({ showMissingOnly: !!e.target.checked }); updatePasteFieldVisibility(); }); $('collapse-empty-sections')?.addEventListener('change', async (e) => { await setPopupUiPrefs({ collapseEmptySections: !!e.target.checked }); updatePasteFieldVisibility(); }); $('open-nc-btn')?.addEventListener('click', async () => { if (!activeClientId) return; const { activeClient } = await chrome.storage.local.get(['activeClient']); let isProspect = false; if (activeClient) { if (activeClient.IsProspect === true || activeClient.isProspect === true) isProspect = true; const typeStr = String(activeClient.type || activeClient.Type || activeClient.InsuredType || activeClient.insuredType || '').toLowerCase(); const statusStr = String(activeClient.Status || activeClient.status || '').toLowerCase(); if (typeStr.includes('prospect') || statusStr.includes('prospect')) isProspect = true; if (typeof activeClient.InsuredType === 'object' && activeClient.InsuredType !== null) { if (String(activeClient.InsuredType.Name || activeClient.InsuredType.name || '').toLowerCase().includes('prospect')) isProspect = true; } } const baseUrl = 'https://www6.nowcerts.com/AMSINS'; const urlPath = isProspect ? 'Prospects/Details' : 'Insureds/Details'; chrome.tabs.create({ url: `${baseUrl}/${urlPath}/${activeClientId}/Information` }); }); $('clear-client')?.addEventListener('click', async () => { await storageRemove(['activeClient']); renderActiveClient(null); }); await loadSettingsIntoUI(); await syncAiLockUI(); await syncSpecialModeUI(); wireSpecialMode(); await renderCarrierPortals(); const uiPrefs = await getPopupUiPrefs(); if ($('show-missing-only')) $('show-missing-only').checked = !!uiPrefs.showMissingOnly; if ($('collapse-empty-sections')) $('collapse-empty-sections').checked = !!uiPrefs.collapseEmptySections; await renderRecentClients(); // ── AI Scan: handle active tab hostname and existing map ── const activeTab = await getActiveTab(); if (activeTab && activeTab.url && activeTab.url.startsWith('http')) { try { currentHostname = new URL(activeTab.url).hostname; const mapKey = `smartMap_${currentHostname}`; const res = await storageGet([mapKey]); if (res[mapKey]) { $('ai-status-title').textContent = "✨ Site Mapped!"; $('ai-status-text').textContent = "AI knows these fields."; $('ai-status-icon').textContent = "✅"; $('ai-scan-btn').textContent = "Rescan"; $('ai-scan-btn').style.background = "#10b981"; await sendToActiveTab({ action: 'show_cute_boxes', smartMap: res[mapKey] }); } else { $('ai-status-title').textContent = "🔍 Unmapped Site"; $('ai-status-text').textContent = "Click to scan fields."; $('ai-status-icon').textContent = "🤖"; } } catch (e) {} } // ── AI Scan button ── $('ai-scan-btn')?.addEventListener('click', async () => { const activeTab = await getActiveTab(); if (!activeTab || !activeTab.url || !activeTab.url.startsWith('http')) { return alert("Open a valid webpage to scan."); } currentHostname = new URL(activeTab.url).hostname; const { geminiApiKey } = await storageGet(['geminiApiKey']); if (!geminiApiKey) { $('settings-view').classList.remove('hidden'); return alert("AI features are not unlocked. Enter your agency unlock code in Settings."); } $('ai-scan-btn').textContent = "Scanning..."; $('ai-scan-btn').disabled = true; try { const extractRes = await sendToActiveTab({ action: 'extract_form_structure' }); if (!extractRes || !extractRes.fields) throw new Error("Could not extract any fields from this page."); const aiRes = await chrome.runtime.sendMessage({ action: 'run_ai_scan', apiKey: geminiApiKey, fields: extractRes.fields, hostname: currentHostname }); if (!aiRes.ok) throw new Error(aiRes.error); $('ai-status-title').textContent = "✨ Site Mapped!"; $('ai-status-text').textContent = "AI learned these fields!"; $('ai-status-icon').textContent = "✅"; $('ai-scan-btn').textContent = "Rescan"; $('ai-scan-btn').style.background = "#10b981"; await sendToActiveTab({ action: 'show_cute_boxes', smartMap: aiRes.smartMap }); } catch (e) { alert("Scan failed: " + e.message); $('ai-scan-btn').textContent = "Scan Page"; } finally { $('ai-scan-btn').disabled = false; } }); // ── Restore last active client ── const { activeClient } = await storageGet(['activeClient']); if (activeClient) { renderActiveClient(activeClient); if (!activeClient.policies?.length && !activeClient.vehicles?.length) { loadFullClient(activeClient); } } } document.addEventListener('DOMContentLoaded', main); // ── SPECIAL MODE (RENEWALS) ─────────────────────────────────────────────────── async function getSpecialMode() { const res = await storageGet([SPECIAL_MODE_KEY]); return !!res[SPECIAL_MODE_KEY]; } async function setSpecialMode(enabled) { await storageSet({ [SPECIAL_MODE_KEY]: !!enabled }); } async function syncSpecialModeUI() { const enabled = await getSpecialMode(); const wrap = $('renewals-tab-wrap'); const status = $('special-mode-status'); const btn = $('special-mode-btn'); if (wrap) wrap.classList.toggle('hidden', !enabled); if (status) { status.textContent = enabled ? '✅ Special Mode active — Renewals Pipeline unlocked.' : ''; status.style.color = '#166534'; } if (btn) btn.textContent = enabled ? '🔒 Disable' : 'Unlock'; } // ── AUTO QUOTE ──────────────────────────────────────────────────────────────── const AUTO_QUOTE_CARRIERS = [ { id: 'erie', name: 'Erie (Agent Exchange)', url: 'https://agentexchange.com' }, { id: 'westfield', name: 'Westfield', url: 'https://wcprod.westfieldgrp.com' }, { id: 'encova', name: 'Encova', url: 'https://agent.encova.com/' }, ]; const STEP_WALK_DELAY = 1400; // ms to wait after clicking Next before filling next step const STEP_WALK_MAX_STEPS = 15; // safety cap // Track open quote tabs: { carrierId -> tabId } let quoteTabMap = {}; async function launchAutoQuote() { const { activeClient } = await storageGet(['activeClient']); if (!activeClient) { alert('Select a client first.'); return; } const btn = $('auto-quote-btn'); if (btn) { btn.textContent = 'Opening tabs...'; btn.disabled = true; } quoteTabMap = {}; for (const carrier of AUTO_QUOTE_CARRIERS) { const tab = await chrome.tabs.create({ url: carrier.url, active: false }); quoteTabMap[carrier.id] = tab.id; } await renderQuoteStatusPanel(); if (btn) { btn.textContent = '🚀 Re-launch All Carriers'; btn.disabled = false; } } // Helper: send a message to a tab, auto-injecting content.js if needed async function sendToTab(tabId, msg) { try { return await chrome.tabs.sendMessage(tabId, msg); } catch (_) { await chrome.scripting.executeScript({ target: { tabId, allFrames: true }, files: ['content.js'] }); return await chrome.tabs.sendMessage(tabId, msg); } } async function renderQuoteStatusPanel() { const container = $('auto-quote-status'); if (!container) return; container.innerHTML = ''; container.classList.remove('hidden'); // Load saved custom selectors const selectorKeys = AUTO_QUOTE_CARRIERS.map(c => `quoteNextSel_${c.id}`); const stored = await storageGet(selectorKeys); const hdr = document.createElement('div'); hdr.style.cssText = 'font-size:12px;font-weight:700;color:#1e293b;margin-bottom:8px;'; hdr.textContent = '📋 Quote Tabs — navigate to the quote form, then click Fill & Walk:'; container.appendChild(hdr); AUTO_QUOTE_CARRIERS.forEach((carrier) => { const tabId = quoteTabMap[carrier.id]; if (!tabId) return; const row = document.createElement('div'); row.className = 'quote-status-row'; row.id = `quote-row-${carrier.id}`; row.style.flexDirection = 'column'; row.style.alignItems = 'stretch'; row.style.gap = '6px'; // ── Top: name + buttons ────────────────────────────────────────────── const topRow = document.createElement('div'); topRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;'; const nameEl = document.createElement('div'); nameEl.className = 'quote-status-name'; nameEl.textContent = carrier.name; const stepLabel = document.createElement('span'); stepLabel.style.cssText = 'font-size:10px;color:#64748b;min-width:60px;text-align:right;'; const actions = document.createElement('div'); actions.style.cssText = 'display:flex;gap:6px;align-items:center;'; // Track which tabId this row is currently locked to (starts as the originally opened tab) let lockedTabId = tabId; const focusBtn = document.createElement('button'); focusBtn.className = 'mini-btn'; focusBtn.style.cssText = 'background:#f1f5f9;color:#1e293b;border:1px solid #e2e8f0;box-shadow:none;'; focusBtn.textContent = '👁 View'; focusBtn.addEventListener('click', () => chrome.tabs.update(lockedTabId, { active: true })); // ── Lock to Tab button ─────────────────────────────────────────────── const lockBtn = document.createElement('button'); lockBtn.className = 'mini-btn'; lockBtn.style.cssText = 'background:#fef3c7;color:#92400e;border:1px solid #fcd34d;box-shadow:none;font-size:10px;'; lockBtn.textContent = '📌 Lock to Tab'; lockBtn.title = 'Click this after Erie opens a new tab — locks Fill & Walk to whatever tab is active right now'; const lockLabel = document.createElement('span'); lockLabel.style.cssText = 'font-size:9px;color:#94a3b8;margin-left:2px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'; lockBtn.addEventListener('click', async () => { try { const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!activeTab?.id) { lockLabel.textContent = '⚠️ No active tab'; return; } lockedTabId = activeTab.id; quoteTabMap[carrier.id] = lockedTabId; const shortUrl = (activeTab.url || '').replace(/^https?:\/\//, '').slice(0, 40); lockBtn.style.background = '#d1fae5'; lockBtn.style.color = '#065f46'; lockBtn.style.borderColor = '#6ee7b7'; lockBtn.textContent = '🔒 Locked'; lockLabel.textContent = shortUrl; focusBtn.onclick = () => chrome.tabs.update(lockedTabId, { active: true }); } catch (e) { lockLabel.textContent = '❌ Failed'; console.error('Lock to tab failed', e); } }); const fillBtn = document.createElement('button'); fillBtn.className = 'mini-btn btn-success'; fillBtn.textContent = '▶ Fill & Walk'; fillBtn.addEventListener('click', async () => { const customSel = (stored[`quoteNextSel_${carrier.id}`] || '').trim() || null; fillBtn.disabled = true; stepLabel.textContent = ''; try { await chrome.tabs.update(lockedTabId, { active: true }); await new Promise(r => setTimeout(r, 500)); let step = 0; while (step < STEP_WALK_MAX_STEPS) { step++; fillBtn.innerHTML = `Step ${step}…`; stepLabel.textContent = `step ${step}`; const result = await sendToTab(lockedTabId, { action: 'fill_form_walk', customNextSelector: customSel }); if (!result?.hasNext) { // No next button found — we're done break; } stepLabel.textContent = `step ${step} → "${result.nextText || 'Next'}"`; await sendToTab(lockedTabId, { action: 'click_next_button', customSelector: customSel }); await new Promise(r => setTimeout(r, STEP_WALK_DELAY)); } fillBtn.innerHTML = '✅ Done!'; fillBtn.style.background = '#059669'; stepLabel.textContent = `${step} step${step !== 1 ? 's' : ''} filled`; row.style.background = '#f0fdf4'; row.style.borderColor = '#bbf7d0'; } catch(e) { fillBtn.innerHTML = '❌ Failed'; fillBtn.style.background = '#ef4444'; fillBtn.disabled = false; stepLabel.textContent = ''; console.error('Auto-quote walk failed for', carrier.name, e); } }); actions.appendChild(focusBtn); actions.appendChild(lockBtn); actions.appendChild(lockLabel); actions.appendChild(fillBtn); actions.appendChild(stepLabel); topRow.appendChild(nameEl); topRow.appendChild(actions); row.appendChild(topRow); // ── Custom Next selector input ─────────────────────────────────────── const selRow = document.createElement('div'); selRow.style.cssText = 'display:flex;gap:4px;align-items:center;'; const selLabel = document.createElement('span'); selLabel.style.cssText = 'font-size:10px;color:#94a3b8;white-space:nowrap;'; selLabel.textContent = 'Next btn:'; const selInput = document.createElement('input'); selInput.type = 'text'; selInput.placeholder = 'CSS selector (optional)'; selInput.value = stored[`quoteNextSel_${carrier.id}`] || ''; selInput.style.cssText = 'flex:1;font-size:10px;padding:3px 6px;margin:0;border:1px solid #e2e8f0;border-radius:4px;color:#475569;'; selInput.addEventListener('change', async () => { const val = selInput.value.trim(); stored[`quoteNextSel_${carrier.id}`] = val; await storageSet({ [`quoteNextSel_${carrier.id}`]: val }); }); selRow.appendChild(selLabel); selRow.appendChild(selInput); row.appendChild(selRow); container.appendChild(row); }); const note = document.createElement('div'); note.style.cssText = 'font-size:10px;color:#94a3b8;margin-top:8px;text-align:center;'; note.textContent = 'Navigate each carrier to the start of the quote form, then click Fill & Walk.'; container.appendChild(note); } async function fetchRenewalsPipeline(days = 90) { const container = $('renewals-list'); if (!container) return; container.innerHTML = 'Fetching renewals...'; try { const today = new Date(); const future = new Date(); future.setDate(future.getDate() + days); const recent = await getRecentClients(); const activeRes = await storageGet(['activeClient']); const candidates = activeRes.activeClient ? [activeRes.activeClient, ...recent.filter(r => (r.Id||r.id) !== (activeRes.activeClient?.Id||activeRes.activeClient?.id))] : recent; container.innerHTML = `
Next ${days} days: ${today.toLocaleDateString()} → ${future.toLocaleDateString()}
`; let found = 0; candidates.forEach((c) => { const policies = Array.isArray(c?.policies) ? c.policies : []; policies.forEach((p) => { const exp = p?.ExpirationDate || p?.expirationDate; if (!exp) return; const d = getDaysUntil(exp); if (d === null || d < 0 || d > days) return; found++; const name = c?.CommercialName || c?.commercialName || c?.FullName || `${c?.FirstName||''} ${c?.LastName||''}`.trim() || 'Unknown'; const carrier = p?.CarrierName || p?.carrierName || p?.CompanyName || ''; const lob = p?.LineOfBusinessName || p?.lineOfBusinessName || ''; const num = p?.Number || p?.number || ''; const badgeClass = d <= 30 ? 'renewal-days-urgent' : d <= 60 ? 'renewal-days-warning' : 'renewal-days-ok'; const item = document.createElement('div'); item.className = 'renewal-item'; item.innerHTML = `
${name}
${[lob, carrier, num ? '#'+num : ''].filter(Boolean).join(' · ')}
Exp: ${formatDisplayDate(exp)}
${d === 0 ? 'TODAY' : d+'d'}
`; // Auto-quote button per renewal item const aqBtn = document.createElement('button'); aqBtn.className = 'mini-btn btn-success'; aqBtn.style.cssText = 'margin-top:6px;width:100%;font-size:11px;'; aqBtn.textContent = '🚀 Auto Quote This Client'; aqBtn.addEventListener('click', async () => { // Set this client as active if not already const currentActive = (await storageGet(['activeClient'])).activeClient; const thisId = c?.Id || c?.id || c?.DatabaseId || c?.databaseId; const activeId = currentActive?.Id || currentActive?.id || currentActive?.DatabaseId; if (thisId && thisId !== activeId) { await storageSet({ activeClient: c }); renderActiveClient(c); } // Switch to main view and launch $('renewals-panel')?.classList.add('hidden'); await launchAutoQuote(); // Show the quote status panel $('auto-quote-status')?.scrollIntoView({ behavior: 'smooth' }); }); item.appendChild(aqBtn); container.appendChild(item); }); }); if (!found) { container.innerHTML += '
No expiring policies found in loaded clients. Open more clients to populate this pipeline.
'; } } catch(e) { container.innerHTML = `Error: ${e.message}`; } } function wireSpecialMode() { $('special-mode-btn')?.addEventListener('click', async () => { const current = await getSpecialMode(); if (current) { await setSpecialMode(false); await syncSpecialModeUI(); return; } const input = $('special-mode-input'); const val = (input?.value || '').trim().toLowerCase(); const status = $('special-mode-status'); if (val === SPECIAL_MODE_PASSWORD) { await setSpecialMode(true); if (input) input.value = ''; await syncSpecialModeUI(); } else { if (status) { status.textContent = '❌ Incorrect code.'; status.style.color = '#dc2626'; } if (input) { input.value = ''; input.focus(); } } }); $('special-mode-input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') $('special-mode-btn')?.click(); }); $('renewals-tab-btn')?.addEventListener('click', async () => { const panel = $('renewals-panel'); if (!panel) return; const wasHidden = panel.classList.contains('hidden'); panel.classList.toggle('hidden', !wasHidden); if (wasHidden) fetchRenewalsPipeline(90); }); $('auto-quote-btn')?.addEventListener('click', launchAutoQuote); } // ─────────────────────────────────────────────────────────────────────────────