(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);
}
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) {
getEl('sl-users-val').textContent = fmt(userTotal);
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;
getEl('sl-endpoints-val').textContent = fmt(epOnly);
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) getEl('sl-servers-val').textContent = fmt(serverBase);
show('sl-zt', ztNetTotal > 0);
if (ztNetTotal > 0) getEl('sl-zt-val').textContent = fmt(ztNetTotal);
show('sl-voip', voipTotal > 0);
if (voipTotal > 0) getEl('sl-voip-val').textContent = fmt(voipTotal);
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);
}
getEl('mrrDisplay').textContent = fmt(effectiveMrr);
const monthlyTotalEl = getEl('sl-monthly-total-val');
if (monthlyTotalEl) monthlyTotalEl.textContent = 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) => {
const el = document.getElementById(id);
if (!el) return;
const secId = id.replace('-summary', '').replace('sec0', 'sec-0');
const show = collapsed(secId) && !!text;
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` : '');
setSummary('sec03-summary', q.endpoints > 0 ? `${fmt(q.endpointTotal - q.serverBase)}/mo` : '');
setSummary('sec04-summary', q.servers > 0 ? `${fmt(q.serverBase)}/mo` : '');
setSummary('sec05-summary', q.ztNetTotal > 0 ? `${fmt(q.ztNetTotal)}/mo` : '');
setSummary('sec06-summary', q.voipSeats > 0 ? `${fmt(q.voipTotal)}/mo` : '');
}
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);