(function(global) { 'use strict'; let nudges = []; let nudgeIndex = 0; let nudgeTimer; function fmt(n) { return '$' + Math.round(n).toLocaleString('en-US'); } function getEl(id) { return document.getElementById(id); } /* ── Animated number counter ───────────────────────────────── Smoothly interpolates a displayed dollar value from old → new. Targets both the desktop element and its _m mobile clone. Duration: 350ms ease-out via requestAnimationFrame. ──────────────────────────────────────────────────────────── */ var _animFrames = {}; function animateValue(elId, newVal, formatFn) { var el = document.getElementById(elId); if (!el) return; var elM = document.getElementById(elId + '_m'); var raw = (el.textContent || '').replace(/[^0-9.\-]/g, ''); var from = parseFloat(raw) || 0; var to = parseFloat(newVal) || 0; if (from === to) { el.textContent = formatFn(to); if (elM) elM.textContent = formatFn(to); return; } if (_animFrames[elId]) cancelAnimationFrame(_animFrames[elId]); var duration = 350; var start = null; function step(ts) { if (!start) start = ts; var progress = Math.min((ts - start) / duration, 1); var eased = 1 - Math.pow(1 - progress, 3); var current = from + (to - from) * eased; el.textContent = formatFn(current); if (elM) elM.textContent = formatFn(current); if (progress < 1) { _animFrames[elId] = requestAnimationFrame(step); } else { delete _animFrames[elId]; } } _animFrames[elId] = requestAnimationFrame(step); } function getQuote(q) { return q || (typeof global.calcQuote === 'function' ? global.calcQuote() : null); } function getPricing() { return typeof global.getPricingConfig === 'function' ? global.getPricingConfig() : global; } function getM365BundleSavings(options, pricing) { if (options && typeof options.m365BundleSavings === 'number') { return options.m365BundleSavings; } // Use the higher M365 rate (m2m) for maximum savings comparison const bestRate = Math.max(pricing.RATE_M365_M2M || 0, pricing.RATE_M365 || 0); return Math.max(0, bestRate - (pricing.RATE_BYOL || 0)); } function renderQuoteUi(q, options) { if (!q) return; const pricing = getPricing(); const m365BundleSavings = getM365BundleSavings(options, pricing); const onboardingWaived = !!options?.onboardingWaived; const onboardingHalfOff = !!options?.onboardingHalfOff; const onboardingWouldBe = typeof options?.onboardingWouldBe === 'number' ? options.onboardingWouldBe : Math.round((q.MRR || 0) / 2); const { users, voipTier, byol, addPWM, ztActive, adminWaived, clientName, contractTerm, baseSubtotal, siteAdminBase, admin1PWM, adminFeeNet, discountAmt } = q; if (adminWaived) { getEl('adminFeeDisplay').innerHTML = `${fmt(adminFeeNet)}/mo WAIVED`; } else { getEl('adminFeeDisplay').textContent = fmt(adminFeeNet) + '/mo'; } const fillPct = Math.min(100, Math.max(0, (baseSubtotal / pricing.ADMIN_FEE_MINIMUM) * 100)); getEl('floorBar').style.width = fillPct + '%'; const atFloor = baseSubtotal >= pricing.ADMIN_FEE_MINIMUM; getEl('floorBar').style.background = atFloor ? 'var(--green)' : 'var(--accent)'; getEl('floorNote').textContent = atFloor ? `✓ Oversight fee is now at its minimum level (${fmt(pricing.ADMIN_FEE_FLOOR)}/mo)` : `Add ${fmt(Math.max(0, pricing.ADMIN_FEE_MINIMUM - baseSubtotal))} more in managed services and this fee will reduce further`; getEl('fb-base').textContent = fmt(siteAdminBase); getEl('fb-zt-row').classList.toggle('hidden', !ztActive); getEl('fb-zt').textContent = '+' + fmt(pricing.ADMIN_FEE_ZT); getEl('fb-pwm-row').classList.toggle('hidden', !addPWM); getEl('fb-pwm').textContent = '+' + fmt(admin1PWM); if (adminWaived) { getEl('fb-total').innerHTML = `${fmt(adminFeeNet)} WAIVED`; } else { getEl('fb-total').textContent = fmt(adminFeeNet); } const adminWaivedSavingsEl = document.getElementById('adminWaivedSavings'); if (adminWaivedSavingsEl) { adminWaivedSavingsEl.classList.toggle('hidden', !adminWaived || adminFeeNet === 0); const awAmt = document.getElementById('adminWaivedAmt'); if (awAmt) awAmt.textContent = fmt(adminFeeNet); } getEl('byolCalloutGreen').classList.toggle('hidden', byol); getEl('byolCalloutRed').classList.toggle('hidden', !byol); getEl('userIncluded').classList.toggle('byol-mode', byol); getEl('userBundleStrip')?.classList.toggle('byol-disabled', byol); // M365 term-aware pricing display const isAnnualTerm = contractTerm === '12mo' || contractTerm === '24mo'; const m365PriceM2m = getEl('m365PriceM2m'); const m365PriceAnnual = getEl('m365PriceAnnual'); if (m365PriceM2m && m365PriceAnnual) { if (isAnnualTerm) { m365PriceM2m.className = 'm365-price-grey'; m365PriceM2m.textContent = '$' + pricing.RATE_M365_M2M; m365PriceAnnual.classList.remove('hidden'); m365PriceAnnual.textContent = '$' + pricing.RATE_M365; } else { m365PriceM2m.className = ''; m365PriceM2m.textContent = '$' + pricing.RATE_M365_M2M; m365PriceAnnual.classList.add('hidden'); } } const retailRate = isAnnualTerm ? pricing.M365_RETAIL_ANNUAL : pricing.M365_RETAIL_MONTHLY; const svsRate = isAnnualTerm ? pricing.RATE_M365 : pricing.RATE_M365_M2M; const officePortion = svsRate - pricing.RATE_BYOL; const officeSavings = retailRate - officePortion; const savingsLine = getEl('m365SavingsLine'); if (savingsLine) savingsLine.classList.add('hidden'); const m365CalloutText = getEl('m365CalloutText'); if (m365CalloutText && !byol) { const retailLabel = isAnnualTerm ? '$' + retailRate + '/user annual' : '$' + retailRate + '/user monthly'; m365CalloutText.textContent = 'Recommended bundle \u2014 M365 Business Premium is built into this seat, saving $' + officeSavings + '/user vs ' + retailLabel + ' retail licensing'; } if (byol) getEl('byolRedSavings').textContent = fmt(users * m365BundleSavings); ['basic', 'standard', 'premium'].forEach(tier => { const seg = getEl('seg-' + tier); if (seg) seg.classList.toggle('active', tier === voipTier); }); ['m2m', '12mo', '24mo'].forEach(term => { const seg = getEl('seg-term-' + term); if (seg) seg.classList.toggle('active', term === contractTerm); }); const savingsRow = document.getElementById('qsSavingsDisplay'); const savingsCopy = document.getElementById('qsSavingsCopy'); const firstYearRow = document.getElementById('qsFirstYearDisplay'); const firstYearCopy = document.getElementById('qsFirstYearCopy'); const termLabel = contractTerm === '12mo' ? '12-Month' : contractTerm === '24mo' ? '24-Month' : 'Month-to-Month'; const annualizedTermSavings = discountAmt > 0 ? discountAmt * 12 : 0; const termOnboardingValue = onboardingWaived ? onboardingWouldBe : onboardingHalfOff ? Math.round(onboardingWouldBe / 2) : 0; const firstYearValue = annualizedTermSavings + termOnboardingValue; if (savingsRow && savingsCopy) { if (discountAmt > 0) { savingsCopy.innerHTML = `${termLabel} savings: ${fmt(discountAmt)}/mo vs. month-to-month`; savingsRow.classList.remove('hidden'); } else { savingsRow.classList.add('hidden'); } } if (firstYearRow && firstYearCopy) { if (firstYearValue > 0) { let suffix = ' in year-one value'; if (annualizedTermSavings > 0 && onboardingWaived) { suffix = ' from annualized term savings and complimentary onboarding'; } else if (annualizedTermSavings > 0 && onboardingHalfOff) { suffix = ' from annualized term savings and 50% off onboarding'; } else if (annualizedTermSavings > 0) { suffix = ' from annualized term savings'; } else if (onboardingWaived) { suffix = ' from complimentary onboarding'; } else if (onboardingHalfOff) { suffix = ' from 50% off onboarding'; } firstYearCopy.innerHTML = `Year-one value unlocked: ${fmt(firstYearValue)}${suffix}`; firstYearRow.classList.remove('hidden'); } else { firstYearRow.classList.add('hidden'); } } document.querySelectorAll('.addon-preview-pill[data-addon]').forEach(pill => { const cb = document.getElementById(pill.dataset.addon); pill.classList.toggle('active', cb?.checked || false); }); } function renderSidebar(q, options) { if (!q) return; const pricing = getPricing(); const { users, endpoints, servers, voipSeats, byol, addPWM, addINKY, addExtHours, addZT, addUSB, addBMB, ztActive, adminWaived, contractTerm, hstEnabled, baseUserRate, userPWM, userINKY, userExt, userZT, userTotal, serverBase, endpointUSB, endpointBMB, endpointTotal, ztNetTotal, voipTotal, siteAdminBase, admin1PWM, adminFeeNet, clientName, MRR, discountPct, discountAmt, effectiveMrr, effectiveAnnual, hstAmt, mrrWithHst, perUserServices, perUserSiteOvhd } = q; const onboardingWaived = !!options?.onboardingWaived; const onboardingHalfOff = !!options?.onboardingHalfOff; const onboardingWouldBe = typeof options?.onboardingWouldBe === 'number' ? options.onboardingWouldBe : Math.round(q.MRR / 2); const oneTimeFee = typeof options?.oneTimeFee === 'number' ? options.oneTimeFee : (parseFloat(q.oneTimeFee) || 0); const annualizedTermSavings = discountPct > 0 ? discountAmt * 12 : 0; const onboardingValueUnlocked = onboardingWaived ? onboardingWouldBe : onboardingHalfOff ? Math.round(onboardingWouldBe / 2) : 0; const isAnnualTermSb = contractTerm === '12mo' || contractTerm === '24mo'; const retailRateSb = isAnnualTermSb ? (pricing.M365_RETAIL_ANNUAL || 30) : (pricing.M365_RETAIL_MONTHLY || 35); const svsRateSb = isAnnualTermSb ? pricing.RATE_M365 : (pricing.RATE_M365_M2M || pricing.RATE_M365); const officePortionSb = svsRateSb - pricing.RATE_BYOL; const officeSavingsSb = Math.max(0, retailRateSb - officePortionSb); const m365AnnualValue = (!byol && users > 0) ? (users * officeSavingsSb * 12) : 0; const byolOpportunityValue = (byol && users > 0) ? (users * getM365BundleSavings(options, pricing) * 12) : 0; const adminIncludedValue = adminWaived ? (adminFeeNet * 12) : 0; const totalValueUnlocked = m365AnnualValue + annualizedTermSavings + onboardingValueUnlocked + adminIncludedValue; const firstInvoiceTotal = effectiveMrr + (hstEnabled ? hstAmt : 0) + oneTimeFee; const sidebarClientNameEl = getEl('sidebarFocusClientName'); if (sidebarClientNameEl) { sidebarClientNameEl.textContent = clientName && clientName.trim() ? clientName.trim() : 'Client Name'; } const show = (id, val) => { const el = getEl(id); if (!el) return; el.classList.toggle('hidden', !val); }; const renderSubRows = (rows) => rows.map(row => { if (!row.addon) { return `
${row.copy}
`; } return `
${row.copy}${row.value}
`; }).join(''); show('sl-users', users > 0); getEl('sl-users-sub')?.classList.toggle('hidden', users === 0); if (users > 0) { animateValue('sl-users-val', userTotal, fmt); const sub = getEl('sl-users-sub'); sub.classList.remove('hidden'); const subRows = [{ copy: `${users} × ${fmt(baseUserRate)}/user (${byol ? 'BYOL' : 'M365 Incl.'})` }]; if (addExtHours) subRows.push({ copy: 'Extended Help Desk Hours', value: `${fmt(userExt)}/mo`, addon: true }); if (addPWM) subRows.push({ copy: '1Password', value: `${fmt(userPWM)}/mo`, addon: true }); if (addINKY) subRows.push({ copy: 'INKY Pro Upgrade', value: `${fmt(userINKY)}/mo`, addon: true }); if (addZT) subRows.push({ copy: 'Zero Trust', value: `${fmt(userZT)}/mo`, addon: true }); sub.innerHTML = renderSubRows(subRows); } show('sl-endpoints', endpoints > 0); getEl('sl-endpoints-sub')?.classList.toggle('hidden', endpoints === 0); if (endpoints > 0) { const epOnly = endpointTotal - serverBase; animateValue('sl-endpoints-val', epOnly, fmt); const sub = getEl('sl-endpoints-sub'); sub.classList.remove('hidden'); const epRows = [{ copy: `${endpoints} × ${fmt(pricing.RATE_ENDPOINT)}/endpoint` }]; if (addBMB) epRows.push({ copy: 'Bare Metal Backup', value: `${fmt(endpointBMB)}/mo`, addon: true }); if (addUSB) epRows.push({ copy: 'USB Blocking', value: `${fmt(endpointUSB)}/mo`, addon: true }); sub.innerHTML = renderSubRows(epRows); } show('sl-servers', servers > 0); if (servers > 0) animateValue('sl-servers-val', serverBase, fmt); show('sl-zt', ztNetTotal > 0); if (ztNetTotal > 0) animateValue('sl-zt-val', ztNetTotal, fmt); show('sl-voip', voipTotal > 0); if (voipTotal > 0) animateValue('sl-voip-val', voipTotal, fmt); const slAdminEl = getEl('sl-admin'); const slAdminValEl = getEl('sl-admin-val'); const slAdminSubEl = getEl('sl-admin-sub'); if (adminWaived) { slAdminEl?.classList.add('sl-admin-waived'); if (slAdminValEl) { slAdminValEl.innerHTML = `${fmt(adminFeeNet)} WAIVED`; } } else { slAdminEl?.classList.remove('sl-admin-waived'); if (slAdminValEl) slAdminValEl.textContent = fmt(adminFeeNet); } if (slAdminSubEl) { const adminRows = [{ copy: `Base ${fmt(siteAdminBase)}/mo` }]; if (ztActive) adminRows.push({ copy: 'Zero Trust Supplement', value: `${fmt(pricing.ADMIN_FEE_ZT)}/mo`, addon: true }); if (addPWM && admin1PWM > 0) adminRows.push({ copy: '1Password Admin', value: `${fmt(admin1PWM)}/mo`, addon: true }); slAdminSubEl.classList.remove('hidden'); slAdminSubEl.innerHTML = renderSubRows(adminRows); } animateValue('mrrDisplay', effectiveMrr, fmt); const monthlyTotalEl = getEl('sl-monthly-total-val'); if (monthlyTotalEl) monthlyTotalEl.innerHTML = fmt(effectiveMrr) + '/mo'; getEl('annualDisplay').textContent = fmt(effectiveAnnual); getEl('perUserRow').classList.toggle('hidden', users === 0); if (users > 0) getEl('perUserDisplay').textContent = fmt(effectiveMrr / users) + '/user'; const discountRow = getEl('sl-discount-row'); if (discountRow) { discountRow.classList.toggle('hidden', discountPct === 0); if (discountPct > 0) { const termLabel = contractTerm === '12mo' ? '12-Month' : '24-Month'; const discDetailEl = getEl('sl-discount-detail'); if (discDetailEl) { discDetailEl.textContent = `(${termLabel}, ${Math.round(discountPct * 100)}% off)`; } const discEl = getEl('sl-discount-val'); if (discEl) { discEl.textContent = `−${fmt(discountAmt)}/mo`; } } } const baseMrrRow = getEl('sl-base-mrr-row'); if (baseMrrRow) baseMrrRow.classList.toggle('hidden', discountPct === 0); const baseMrrEl = getEl('sl-base-mrr-val'); if (baseMrrEl && discountPct > 0) baseMrrEl.textContent = fmt(MRR) + '/mo'; const firstMriEl = getEl('sl-first-mri-val'); if (firstMriEl) firstMriEl.textContent = fmt(effectiveMrr); show('sl-first-hst-row', hstEnabled); const firstHstEl = getEl('sl-first-hst-val'); if (firstHstEl && hstEnabled) firstHstEl.textContent = fmt(hstAmt); getEl('sl-hst-row')?.classList.toggle('hidden', !hstEnabled); getEl('sl-hst-total-row')?.classList.toggle('hidden', !hstEnabled); if (hstEnabled) { const hstEl = getEl('sl-hst-val'); if (hstEl) hstEl.textContent = `+${fmt(hstAmt)}`; const totalEl = getEl('sl-hst-total-val'); if (totalEl) totalEl.textContent = fmt(mrrWithHst) + '/mo'; } const otfRow = getEl('sl-otf-row'); const otfValEl = getEl('sl-otf-val'); if (otfRow) { if (onboardingWaived && onboardingWouldBe > 0) { otfRow.classList.remove('hidden'); otfRow.classList.add('sl-otf-waived'); if (otfValEl) { otfValEl.innerHTML = '' + fmt(onboardingWouldBe) + ' COMPLIMENTARY'; } } else if (onboardingHalfOff && onboardingWouldBe > 0) { otfRow.classList.remove('hidden'); otfRow.classList.add('sl-otf-waived'); if (otfValEl) { otfValEl.innerHTML = '50% OFF ' + '' + fmt(onboardingWouldBe) + ' ' + '' + fmt(oneTimeFee) + ''; } } else if (oneTimeFee > 0) { otfRow.classList.remove('hidden', 'sl-otf-waived'); if (otfValEl) otfValEl.textContent = fmt(oneTimeFee) + ' (one-time)'; } else { otfRow.classList.add('hidden'); otfRow.classList.remove('sl-otf-waived'); } } const firstTotalEl = getEl('sl-first-total-val'); if (firstTotalEl) firstTotalEl.textContent = fmt(firstInvoiceTotal); show('sl-value-m365-row', m365AnnualValue > 0); const valueM365El = getEl('sl-value-m365-val'); if (valueM365El && m365AnnualValue > 0) valueM365El.textContent = fmt(m365AnnualValue) + '/yr'; show('sl-value-term-row', annualizedTermSavings > 0); const valueTermEl = getEl('sl-value-term-val'); if (valueTermEl && annualizedTermSavings > 0) valueTermEl.textContent = fmt(annualizedTermSavings) + '/yr'; show('sl-value-onboarding-row', onboardingValueUnlocked > 0); const valueOnboardingEl = getEl('sl-value-onboarding-val'); if (valueOnboardingEl && onboardingValueUnlocked > 0) valueOnboardingEl.textContent = fmt(onboardingValueUnlocked); const valueOnboardingLabel = getEl('sl-value-onboarding-label'); if (valueOnboardingLabel) valueOnboardingLabel.textContent = onboardingWaived ? 'Complimentary Onboarding' : 'Onboarding Savings (50% off)'; show('sl-value-admin-row', adminIncludedValue > 0); const valueAdminEl = getEl('sl-value-admin-val'); if (valueAdminEl && adminIncludedValue > 0) valueAdminEl.textContent = fmt(adminIncludedValue) + '/yr'; show('sl-value-byol-row', byolOpportunityValue > 0); const valueByolEl = getEl('sl-value-byol-val'); if (valueByolEl && byolOpportunityValue > 0) valueByolEl.textContent = fmt(byolOpportunityValue) + '/yr'; show('sl-value-total-row', totalValueUnlocked > 0); const valueTotalEl = getEl('sl-value-total-val'); if (valueTotalEl && totalValueUnlocked > 0) valueTotalEl.textContent = fmt(totalValueUnlocked) + '/yr'; const puBreakdown = getEl('perUserBreakdown'); if (puBreakdown) { puBreakdown.classList.toggle('hidden', users === 0); if (users > 0) { puBreakdown.textContent = `Includes ${fmt(perUserServices)} in user services + ${fmt(perUserSiteOvhd)} in shared site overhead`; } } } function buildNudges(q, options) { if (!q) return []; const pricing = getPricing(); const m365BundleSavings = getM365BundleSavings(options, pricing); const { users, endpoints, servers, voipSeats, voipTier, contractTerm, MRR, byol, addPWM, addExtHours, addZT, addBMB } = q; const nextNudges = []; if (!addZT && users > 0) { nextNudges.push({ text: `Zero Trust Deny By Default adds enterprise-grade access control — ${fmt(users * pricing.ADDON_ZERO_TRUST_USER)}/mo for all ${users} user${users !== 1 ? 's' : ''}. Recommended for any client with remote staff or sensitive data.`, color: 'amber' }); } if (!addPWM && users > 0) { nextNudges.push({ text: `1Password Management — one vault for every credential, enforce strong passwords, revoke access instantly when staff leave. Only ${fmt(users * pricing.ADDON_1PASSWORD)}/mo for ${users} user${users !== 1 ? 's' : ''}.`, color: 'green' }); } if (contractTerm === 'm2m' && MRR > 0) { const onboardingWouldBe = typeof options?.onboardingWouldBe === 'number' ? options.onboardingWouldBe : Math.round(MRR / 2); nextNudges.push({ text: `A 12-month term saves 3% on MRR and 50% off onboarding. The 24-month option saves ${fmt(MRR * pricing.DISCOUNT_24MO)}/mo (5%) and includes complimentary onboarding — saving an estimated ${fmt(onboardingWouldBe)}.`, color: 'green' }); } if (byol && users > 0) { const m365RateForTerm = contractTerm === 'm2m' ? (pricing.RATE_M365_M2M || pricing.RATE_M365) : pricing.RATE_M365; nextNudges.push({ text: `BYOL keeps the client's existing licensing in place. Switching to M365 Included (${fmt(m365RateForTerm)}/user) bundles licensing at well below retail — saving up to ${fmt(users * m365BundleSavings)}/mo across ${users} user${users !== 1 ? 's' : ''}.`, color: 'green' }); } if (q.ztActive) { nextNudges.push({ text: `Zero Trust is active — a ${fmt(pricing.ADMIN_FEE_ZT)}/mo site admin supplement applies to cover additional tenant management, policy enforcement, and network oversight.`, color: 'amber' }); } if (endpoints > 0 && !addBMB) { nextNudges.push({ text: `Bare Metal Backup adds the recovery layer to this endpoint package — ${fmt(endpoints * pricing.ADDON_BARE_METAL_BACKUP)}/mo protects all ${endpoints} endpoint${endpoints !== 1 ? 's' : ''} with image-level restore after ransomware, hardware failure, or a full rebuild event.`, color: 'amber' }); } if (voipSeats > 0 && voipTier === 'basic') { nextNudges.push({ text: `Standard tier adds HD video, call analytics, and advanced call routing — only ${fmt(voipSeats * (pricing.VOIP_RATE_STANDARD - pricing.VOIP_RATE_BASIC))}/mo more for ${voipSeats} seat${voipSeats !== 1 ? 's' : ''}. Most clients upgrade within 6 months.`, color: 'green' }); } if (servers > 0 && !addBMB) { nextNudges.push({ text: `Servers are in scope but Bare Metal Backup is not selected — adding BMB closes the DR gap by pairing endpoint protection with full-system recovery across workstations and server workloads.`, color: 'amber' }); } if (users > 0 && endpoints > 0 && endpoints > users * 1.5) { nextNudges.push({ text: `${endpoints} endpoints vs ${users} users — that's a high endpoint-to-user ratio. Consider whether unmanaged devices should be attached to user seats to ensure full coverage.`, color: 'amber' }); } if (users > 0 && endpoints === 0) { nextNudges.push({ text: `${users} user${users !== 1 ? 's' : ''} configured but no endpoints — most environments need workstation management alongside user services. Add endpoints to close the coverage gap.`, color: 'amber' }); } if (voipSeats > 0 && users > 0 && voipSeats !== users) { const diff = Math.abs(voipSeats - users); const direction = voipSeats > users ? 'more VoIP seats than users' : 'fewer VoIP seats than users'; nextNudges.push({ text: `${voipSeats} VoIP seat${voipSeats !== 1 ? 's' : ''} vs ${users} user${users !== 1 ? 's' : ''} — ${diff} ${direction}. Verify whether every user needs a phone line, or if shared/common-area lines explain the difference.`, color: 'amber' }); } if (MRR > 0 && !q.adminWaived && q.adminFeeNet > MRR * 0.25) { nextNudges.push({ text: `Site admin is ${fmt(q.adminFeeNet)}/mo — over 25% of the ${fmt(MRR)} MRR. Adding more managed services will reduce this ratio as the admin fee floors out at ${fmt(pricing.ADMIN_FEE_FLOOR)}/mo.`, color: 'amber' }); } if (!addExtHours && users > 0) { nextNudges.push({ text: `Extended Help Desk Hours adds after-hours and weekend support coverage — only ${fmt(users * pricing.ADDON_EXT_HOURS)}/mo for ${users} user${users !== 1 ? 's' : ''}. Ideal for clients with shift workers or off-hours operations.`, color: 'green' }); } return nextNudges; } function setNudges(nextNudges) { nudges = Array.isArray(nextNudges) ? nextNudges : []; if (!nudges.length) { nudgeIndex = 0; return; } if (nudgeIndex >= nudges.length) { nudgeIndex = 0; } } function renderNudge() { const currentIndex = nudges.length ? nudgeIndex : 0; function applyNudge(suffix) { const s = suffix || ''; const banner = document.getElementById('nudgeBanner' + s); if (!banner) return; if (!nudges.length) { banner.classList.add('hidden'); return; } const n = nudges[currentIndex]; banner.classList.remove('hidden', 'amber', 'green'); banner.classList.add(n.color); const textEl = document.getElementById('nudgeText' + s); const cntEl = document.getElementById('nudgeCounter' + s); if (textEl) textEl.textContent = n.text; if (cntEl) cntEl.textContent = nudges.length > 1 ? `${currentIndex + 1}/${nudges.length}` : ''; const btns = banner.querySelectorAll('button'); btns.forEach(btn => { btn.style.display = nudges.length > 1 ? 'flex' : 'none'; }); } applyNudge(''); applyNudge('_m'); } function cycleNudge(dir) { if (!nudges.length) return; // Crossfade: fade out, swap, fade in var banners = [document.getElementById('nudgeBanner'), document.getElementById('nudgeBanner_m')]; banners.forEach(function(b) { if (b) b.classList.add('nudge-fading'); }); setTimeout(function() { nudgeIndex = (nudgeIndex + dir + nudges.length) % nudges.length; renderNudge(); banners.forEach(function(b) { if (b) b.classList.remove('nudge-fading'); }); }, 180); } function startNudgeRotation() { if (nudgeTimer) clearInterval(nudgeTimer); nudgeTimer = setInterval(() => { if (nudges.length > 1) { cycleNudge(1); } }, 30000); } function updateSectionSummaries(q) { q = getQuote(q); if (!q) return; const collapsed = id => !document.getElementById(id)?.classList.contains('sec-open'); const setSummary = (id, text, numericVal) => { const el = document.getElementById(id); if (!el) return; const secId = id.replace('-summary', '').replace('sec0', 'sec-0'); const show = collapsed(secId) && !!text; if (numericVal !== undefined && show) { animateValue(id, numericVal, function(v) { return fmt(v) + '/mo'; }); } else { el.textContent = text || ''; } el.hidden = !show; el.style.display = show ? 'flex' : 'none'; }; setSummary('sec01-summary', q.adminWaived ? 'WAIVED' : fmt(q.adminFeeNet) + '/mo'); setSummary('sec02-summary', q.users > 0 ? `${fmt(q.userTotal)}/mo` : '', q.users > 0 ? q.userTotal : undefined); setSummary('sec03-summary', q.endpoints > 0 ? `${fmt(q.endpointTotal - q.serverBase)}/mo` : '', q.endpoints > 0 ? q.endpointTotal - q.serverBase : undefined); setSummary('sec04-summary', q.servers > 0 ? `${fmt(q.serverBase)}/mo` : '', q.servers > 0 ? q.serverBase : undefined); setSummary('sec05-summary', q.ztNetTotal > 0 ? `${fmt(q.ztNetTotal)}/mo` : '', q.ztNetTotal > 0 ? q.ztNetTotal : undefined); setSummary('sec06-summary', q.voipSeats > 0 ? `${fmt(q.voipTotal)}/mo` : '', q.voipSeats > 0 ? q.voipTotal : undefined); } function updateVsComparison(q) { const vsEl = document.getElementById('vsComparison'); if (!vsEl) return; q = getQuote(q); if (!q) return; const pricing = getPricing(); const { users, endpoints, effectiveAnnual } = q; if (users < 1 && endpoints < 1) { vsEl.classList.add('hidden'); return; } vsEl.classList.remove('hidden'); const toolsMonthly = Math.max( pricing.TOOL_COST_MIN, (users * pricing.TOOL_COST_PER_USER) + (endpoints * pricing.TOOL_COST_PER_ENDPOINT) ); const toolsAnnual = toolsMonthly * 12; const cost1 = pricing.IT_SALARY_1 + toolsAnnual; const cost5 = pricing.IT_SALARY_5 + toolsAnnual; const save1 = cost1 - effectiveAnnual; const save5 = cost5 - effectiveAnnual; const fmtK = n => n >= 0 ? '$' + Math.round(n / 1000) + 'K/yr' : '–$' + Math.round(Math.abs(n) / 1000) + 'K/yr'; getEl('vs-svs-annual').textContent = '$' + Math.round(effectiveAnnual / 1000) + 'K/yr'; getEl('vs-1man-cost').textContent = '$' + Math.round(cost1 / 1000) + 'K/yr'; getEl('vs-5man-cost').textContent = '$' + Math.round(cost5 / 1000) + 'K/yr'; const applyVsRow = (rowId, saveId, labelId, saving) => { const row = getEl(rowId); const val = getEl(saveId); const lbl = getEl(labelId); if (!row || !val || !lbl) return; val.textContent = fmtK(saving); row.classList.remove('vs-save-green', 'vs-save-amber'); val.classList.remove('vs-val-green', 'vs-val-amber'); lbl.classList.remove('vs-val-green', 'vs-val-amber'); if (saving > 0) { row.classList.add('vs-save-green'); val.classList.add('vs-val-green'); lbl.classList.add('vs-val-green'); lbl.textContent = 'EST. SAVINGS'; } else { row.classList.add('vs-save-amber'); val.classList.add('vs-val-amber'); lbl.classList.add('vs-val-amber'); lbl.textContent = 'DIFFERENCE'; } }; applyVsRow('vs-1man-save-row', 'vs-1man-save', 'vs-1man-save-lbl', save1); applyVsRow('vs-5man-save-row', 'vs-5man-save', 'vs-5man-save-lbl', save5); const toolsLabel = toolsMonthly <= pricing.TOOL_COST_MIN ? `min $${pricing.TOOL_COST_MIN}/mo` : `~$${toolsMonthly}/mo`; getEl('vs-footnote').textContent = `Based on ~$${Math.round(pricing.IT_SALARY_1 / 1000)}K Ottawa IT salary + ${toolsLabel} tool licensing (M365, EDR, RMM, backup, SAT & more). Excludes benefits, recruiting, PTO coverage, and turnover costs.`; } function updateSavings(q) { const billEl = document.getElementById('currentPhoneBill'); const comparator = document.getElementById('savingsComparator'); const prompt = document.getElementById('savingsPrompt'); if (!billEl || !comparator) return; const bill = parseFloat(billEl.value) || 0; q = getQuote(q); if (!q) return; const { voipTotal, voipSeats } = q; if (voipSeats < 1) { comparator.classList.add('hidden'); if (prompt) prompt.style.display = 'none'; return; } if (prompt) prompt.style.display = bill === 0 ? '' : 'none'; if (bill === 0) { comparator.classList.add('hidden'); return; } comparator.classList.remove('hidden'); const saving = bill - voipTotal; if (saving > 0) { comparator.textContent = `✓ Switching to SVS VoIP saves ~${fmt(saving)}/mo (${fmt(saving * 12)}/yr) vs your current bill of ${fmt(bill)}/mo`; comparator.classList.remove('savings-amber'); } else { comparator.textContent = `Your current bill (${fmt(bill)}/mo) is lower than this VoIP quote (${fmt(voipTotal)}/mo) — consider reviewing the tier or seat count.`; comparator.classList.add('savings-amber'); } } const api = { renderQuoteUi, renderSidebar, buildNudges, setNudges, updateSectionSummaries, updateVsComparison, renderNudge, cycleNudge, startNudgeRotation, updateSavings }; global.SVSQuoteRender = api; global.updateSectionSummaries = updateSectionSummaries; global.updateVsComparison = updateVsComparison; global.renderNudge = renderNudge; global.cycleNudge = cycleNudge; global.startNudgeRotation = startNudgeRotation; global.updateSavings = updateSavings; startNudgeRotation(); })(window);