My very First own bigboy config
This commit is contained in:
@@ -1,26 +1,97 @@
|
||||
// ── PRICING CONSTANTS ───────────────────────────────────────────
|
||||
// All rates in CAD. Edit here — calcQuote() reads these only.
|
||||
// ── PRICING VARIABLES ───────────────────────────────────────────
|
||||
// All rates in CAD. Loaded from package-prices.csv at startup.
|
||||
// Falls back to built-in defaults if CSV is unavailable.
|
||||
// ADMIN_FEE_FLOOR = minimum site admin fee regardless of seat count
|
||||
// ADMIN_FEE_MINIMUM = engagement threshold; admin = max(FLOOR, MIN-subtotal)
|
||||
// ADMIN_FEE_ZT = supplement added to admin when ZT is active
|
||||
// TOOL_COST_* = internal cost estimates (VS comparison only, not billed)
|
||||
// IT_SALARY_1/5 = Ottawa benchmark salaries for VS comparison
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
const ADMIN_FEE_FLOOR = 150;
|
||||
const ADMIN_FEE_MINIMUM = 650;
|
||||
const ADMIN_FEE_ZT = 250;
|
||||
const VOIP_RATES = { basic: 28, standard: 35, premium: 45 };
|
||||
const VOIP_PHONE_RATE = 15;
|
||||
const VOIP_FAX_RATE = 10;
|
||||
const TOOL_COST_PER_USER = 42;
|
||||
const TOOL_COST_PER_ENDPOINT = 23;
|
||||
const TOOL_COST_MIN = 650;
|
||||
const IT_SALARY_1 = 85000;
|
||||
const IT_SALARY_5 = 420000;
|
||||
let RATE_M365 = 130;
|
||||
let RATE_BYOL = 110;
|
||||
let ADDON_EXT_HOURS = 25;
|
||||
let ADDON_1PASSWORD = 9;
|
||||
let ADDON_INKY = 5;
|
||||
let ADDON_ZERO_TRUST_USER= 55;
|
||||
let RATE_ENDPOINT = 35;
|
||||
let RATE_SERVER = 120;
|
||||
let ADDON_USB_BLOCKING = 4;
|
||||
let ADDON_BARE_METAL_BACKUP = 25;
|
||||
let ZT_SEAT_RATE = 25;
|
||||
let ZT_ROUTER_RATE = 100;
|
||||
let ADMIN_FEE_FLOOR = 150;
|
||||
let ADMIN_FEE_MINIMUM = 650;
|
||||
let ADMIN_FEE_ZT = 250;
|
||||
let ADMIN_1PWM_PCT = 0.10;
|
||||
let VOIP_RATE_BASIC = 28;
|
||||
let VOIP_RATE_STANDARD = 35;
|
||||
let VOIP_RATE_PREMIUM = 45;
|
||||
let VOIP_PHONE_RATE = 15;
|
||||
let VOIP_FAX_RATE = 10;
|
||||
let TOOL_COST_PER_USER = 42;
|
||||
let TOOL_COST_PER_ENDPOINT = 23;
|
||||
let TOOL_COST_MIN = 650;
|
||||
let IT_SALARY_1 = 85000;
|
||||
let IT_SALARY_5 = 420000;
|
||||
// CONTRACT_DISCOUNT: discount applied to MRR based on contract length
|
||||
// m2m = month-to-month (no discount), 12mo = 3% off, 24mo = 5% off
|
||||
const CONTRACT_DISCOUNT = { 'm2m': 0, '12mo': 0.03, '24mo': 0.05 };
|
||||
const HST_RATE = 0.13; // Ontario HST 13%
|
||||
let DISCOUNT_M2M = 0;
|
||||
let DISCOUNT_12MO = 0.03;
|
||||
let DISCOUNT_24MO = 0.05;
|
||||
let HST_RATE = 0.13; // Ontario HST 13%
|
||||
|
||||
// ── loadPricing() ────────────────────────────────────────────────
|
||||
// Fetches package-prices.csv and overrides the pricing variables above.
|
||||
// Silently falls back to built-in defaults if CSV is missing or malformed.
|
||||
async function loadPricing() {
|
||||
try {
|
||||
const res = await fetch('package-prices.csv');
|
||||
if (!res.ok) return;
|
||||
const text = await res.text();
|
||||
const lines = text.split('\n').slice(1); // skip header row
|
||||
lines.forEach(line => {
|
||||
const parts = line.split(',');
|
||||
if (parts.length < 3) return;
|
||||
const key = parts[1].trim();
|
||||
const val = parseFloat(parts[2].trim());
|
||||
if (isNaN(val)) return;
|
||||
switch (key) {
|
||||
case 'RATE_M365': RATE_M365 = val; break;
|
||||
case 'RATE_BYOL': RATE_BYOL = val; break;
|
||||
case 'ADDON_EXT_HOURS': ADDON_EXT_HOURS = val; break;
|
||||
case 'ADDON_1PASSWORD': ADDON_1PASSWORD = val; break;
|
||||
case 'ADDON_INKY': ADDON_INKY = val; break;
|
||||
case 'ADDON_ZERO_TRUST_USER': ADDON_ZERO_TRUST_USER = val; break;
|
||||
case 'RATE_ENDPOINT': RATE_ENDPOINT = val; break;
|
||||
case 'RATE_SERVER': RATE_SERVER = val; break;
|
||||
case 'ADDON_USB_BLOCKING': ADDON_USB_BLOCKING = val; break;
|
||||
case 'ADDON_BARE_METAL_BACKUP': ADDON_BARE_METAL_BACKUP = val; break;
|
||||
case 'ZT_SEAT_RATE': ZT_SEAT_RATE = val; break;
|
||||
case 'ZT_ROUTER_RATE': ZT_ROUTER_RATE = val; break;
|
||||
case 'ADMIN_FEE_FLOOR': ADMIN_FEE_FLOOR = val; break;
|
||||
case 'ADMIN_FEE_MINIMUM': ADMIN_FEE_MINIMUM = val; break;
|
||||
case 'ADMIN_FEE_ZT': ADMIN_FEE_ZT = val; break;
|
||||
case 'ADMIN_1PWM_PCT': ADMIN_1PWM_PCT = val; break;
|
||||
case 'VOIP_RATE_BASIC': VOIP_RATE_BASIC = val; break;
|
||||
case 'VOIP_RATE_STANDARD': VOIP_RATE_STANDARD = val; break;
|
||||
case 'VOIP_RATE_PREMIUM': VOIP_RATE_PREMIUM = val; break;
|
||||
case 'VOIP_PHONE_RATE': VOIP_PHONE_RATE = val; break;
|
||||
case 'VOIP_FAX_RATE': VOIP_FAX_RATE = val; break;
|
||||
case 'TOOL_COST_PER_USER': TOOL_COST_PER_USER = val; break;
|
||||
case 'TOOL_COST_PER_ENDPOINT':TOOL_COST_PER_ENDPOINT= val; break;
|
||||
case 'TOOL_COST_MIN': TOOL_COST_MIN = val; break;
|
||||
case 'IT_SALARY_1': IT_SALARY_1 = val; break;
|
||||
case 'IT_SALARY_5': IT_SALARY_5 = val; break;
|
||||
case 'DISCOUNT_M2M': DISCOUNT_M2M = val; break;
|
||||
case 'DISCOUNT_12MO': DISCOUNT_12MO = val; break;
|
||||
case 'DISCOUNT_24MO': DISCOUNT_24MO = val; break;
|
||||
case 'HST_RATE': HST_RATE = val; break;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// CSV unavailable — built-in defaults remain active
|
||||
}
|
||||
}
|
||||
|
||||
// --- CALC ---
|
||||
// ── calcQuote() ─────────────────────────────────────────────────
|
||||
@@ -51,33 +122,36 @@ function calcQuote() {
|
||||
const oneTimeFee = parseFloat(document.getElementById('oneTimeFee')?.value) || 0;
|
||||
const ztActive = addZT || ztSeats > 0;
|
||||
|
||||
const baseUserRate = byol ? 110 : 130;
|
||||
const addonRate = (addExtHours ? 25 : 0) + (addPWM ? 9 : 0) + (addINKY ? 5 : 0) + (addZT ? 55 : 0);
|
||||
const VOIP_RATES = { basic: VOIP_RATE_BASIC, standard: VOIP_RATE_STANDARD, premium: VOIP_RATE_PREMIUM };
|
||||
const CONTRACT_DISCOUNT = { 'm2m': DISCOUNT_M2M, '12mo': DISCOUNT_12MO, '24mo': DISCOUNT_24MO };
|
||||
|
||||
const baseUserRate = byol ? RATE_BYOL : RATE_M365;
|
||||
const addonRate = (addExtHours ? ADDON_EXT_HOURS : 0) + (addPWM ? ADDON_1PASSWORD : 0) + (addINKY ? ADDON_INKY : 0) + (addZT ? ADDON_ZERO_TRUST_USER : 0);
|
||||
const totalUserRate= baseUserRate + addonRate;
|
||||
|
||||
const userBase = users * baseUserRate;
|
||||
const userExt = addExtHours ? users * 25 : 0;
|
||||
const userPWM = addPWM ? users * 9 : 0;
|
||||
const userINKY = addINKY ? users * 5 : 0;
|
||||
const userZT = addZT ? users * 55 : 0;
|
||||
const userExt = addExtHours ? users * ADDON_EXT_HOURS : 0;
|
||||
const userPWM = addPWM ? users * ADDON_1PASSWORD : 0;
|
||||
const userINKY = addINKY ? users * ADDON_INKY : 0;
|
||||
const userZT = addZT ? users * ADDON_ZERO_TRUST_USER : 0;
|
||||
const userTotal = userBase + userExt + userPWM + userINKY + userZT;
|
||||
|
||||
const endpointBase = endpoints * 35;
|
||||
const serverBase = servers * 120;
|
||||
const endpointUSB = addUSB ? endpoints * 4 : 0;
|
||||
const endpointBMB = addBMB ? endpoints * 25 : 0;
|
||||
const endpointBase = endpoints * RATE_ENDPOINT;
|
||||
const serverBase = servers * RATE_SERVER;
|
||||
const endpointUSB = addUSB ? endpoints * ADDON_USB_BLOCKING : 0;
|
||||
const endpointBMB = addBMB ? endpoints * ADDON_BARE_METAL_BACKUP : 0;
|
||||
const endpointTotal= endpointBase + serverBase + endpointUSB + endpointBMB;
|
||||
|
||||
const baseSubtotal = userBase + endpointBase + serverBase;
|
||||
const siteAdminBase = Math.max(ADMIN_FEE_FLOOR, ADMIN_FEE_MINIMUM - baseSubtotal);
|
||||
const admin1PWM = addPWM ? Math.round(userPWM * 0.10) : 0;
|
||||
const admin1PWM = addPWM ? Math.round(userPWM * ADMIN_1PWM_PCT) : 0;
|
||||
const adminFeeNet = siteAdminBase + (ztActive ? ADMIN_FEE_ZT : 0) + admin1PWM;
|
||||
|
||||
const ztNetSeats = ztSeats * 25;
|
||||
const ztNetRouters = ztRouters * 100;
|
||||
const ztNetSeats = ztSeats * ZT_SEAT_RATE;
|
||||
const ztNetRouters = ztRouters * ZT_ROUTER_RATE;
|
||||
const ztNetTotal = ztNetSeats + ztNetRouters;
|
||||
|
||||
const voipSeatRate = VOIP_RATES[voipTier] || 28;
|
||||
const voipSeatRate = VOIP_RATES[voipTier] || VOIP_RATE_BASIC;
|
||||
const voipSeatsAmt = voipSeats * voipSeatRate;
|
||||
const voipPhoneAmt = addVoipPhone ? voipSeats * VOIP_PHONE_RATE : 0;
|
||||
const voipFaxAmt = addVoipFax ? VOIP_FAX_RATE : 0;
|
||||
@@ -149,9 +223,9 @@ function update() {
|
||||
let oneTimeFee;
|
||||
if (waived) {
|
||||
oneTimeFee = 0;
|
||||
if (feeEl) { feeEl.value = ''; feeEl.disabled = true; }
|
||||
if (feeEl) { feeEl.value = ''; feeEl.disabled = true; feeEl.placeholder = 'Complimentary'; }
|
||||
} else {
|
||||
if (feeEl) feeEl.disabled = false;
|
||||
if (feeEl) { feeEl.disabled = false; feeEl.placeholder = 'auto'; }
|
||||
if (feeEl && !feeEl.dataset.manual) {
|
||||
oneTimeFee = Math.round(q.MRR / 2);
|
||||
feeEl.value = oneTimeFee > 0 ? oneTimeFee : '';
|
||||
@@ -284,7 +358,7 @@ function update() {
|
||||
if (_waived && _wouldBe > 0) {
|
||||
row.classList.remove('hidden');
|
||||
row.classList.add('sl-otf-waived');
|
||||
if (valEl) valEl.textContent = fmt(_wouldBe) + ' WAIVED';
|
||||
if (valEl) valEl.innerHTML = '<span class="otf-amt">' + fmt(_wouldBe) + '</span> <span class="otf-waived-label">WAIVED</span>';
|
||||
} else if (oneTimeFee > 0) {
|
||||
row.classList.remove('hidden', 'sl-otf-waived');
|
||||
if (valEl) valEl.textContent = fmt(oneTimeFee) + ' (one-time)';
|
||||
@@ -304,7 +378,6 @@ function update() {
|
||||
}
|
||||
|
||||
// Sidebar notes
|
||||
getEl('adminPct').textContent = MRR > 0 ? Math.round(adminFeeNet / MRR * 100) : '—';
|
||||
getEl('sideNote-m365').classList.toggle('hidden', byol);
|
||||
getEl('sideNote-byol').classList.toggle('hidden', !byol);
|
||||
if (!byol && users > 0) getEl('m365SaveAmt').textContent = fmt(users * 15);
|
||||
@@ -326,6 +399,18 @@ function update() {
|
||||
if (seg) seg.classList.toggle('active', t === contractTerm);
|
||||
});
|
||||
|
||||
// ── Contract savings display ─────────────────────────────────
|
||||
const savingsRow = document.getElementById('qsSavingsDisplay');
|
||||
const savingsAmt = document.getElementById('qsSavingsAmt');
|
||||
if (savingsRow && savingsAmt) {
|
||||
if (q.discountAmt > 0) {
|
||||
savingsAmt.textContent = fmt(q.discountAmt);
|
||||
savingsRow.classList.remove('hidden');
|
||||
} else {
|
||||
savingsRow.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Nudges — dynamic dollar values, context-sensitive conditions
|
||||
const nudges = [];
|
||||
if (!addZT && users > 0) nudges.push({
|
||||
@@ -345,7 +430,7 @@ function update() {
|
||||
color: 'amber'
|
||||
});
|
||||
if (voipSeats > 0 && voipTier === 'basic') nudges.push({
|
||||
text: `Standard tier adds HD video, call analytics, and advanced call routing — only ${fmt(voipSeats * (VOIP_RATES.standard - VOIP_RATES.basic))}/mo more for ${voipSeats} seat${voipSeats !== 1 ? 's' : ''}. Most clients upgrade within 6 months.`,
|
||||
text: `Standard tier adds HD video, call analytics, and advanced call routing — only ${fmt(voipSeats * (VOIP_RATE_STANDARD - VOIP_RATE_BASIC))}/mo more for ${voipSeats} seat${voipSeats !== 1 ? 's' : ''}. Most clients upgrade within 6 months.`,
|
||||
color: 'green'
|
||||
});
|
||||
if (servers > 0 && !addBMB) nudges.push({
|
||||
@@ -357,7 +442,7 @@ function update() {
|
||||
color: 'amber'
|
||||
});
|
||||
window._nudges = nudges;
|
||||
if (!window._nudgeIndex || window._nudgeIndex >= nudges.length) window._nudgeIndex = 0;
|
||||
if (window._nudgeIndex == null || window._nudgeIndex >= nudges.length) window._nudgeIndex = 0;
|
||||
|
||||
renderNudge();
|
||||
updateSavings(q);
|
||||
@@ -378,6 +463,42 @@ function toggleSection(id) {
|
||||
const isOpen = section.classList.toggle('sec-open');
|
||||
body.style.display = isOpen ? '' : 'none';
|
||||
updateSectionSummaries();
|
||||
updateToggleAllBtn();
|
||||
}
|
||||
|
||||
// ── toggleAllSections() / updateToggleAllBtn() ────────────────────
|
||||
// Collapse all if any are open; expand all if all are closed.
|
||||
// Button label reflects current state.
|
||||
const _allSecIds = ['sec-01','sec-02','sec-03','sec-04','sec-05','sec-06'];
|
||||
function toggleAllSections() {
|
||||
const anyOpen = _allSecIds.some(id => document.getElementById(id)?.classList.contains('sec-open'));
|
||||
_allSecIds.forEach(id => {
|
||||
const section = document.getElementById(id);
|
||||
const body = document.getElementById(id + '-body');
|
||||
if (!section || !body) return;
|
||||
if (anyOpen) { section.classList.remove('sec-open'); body.style.display = 'none'; }
|
||||
else { section.classList.add('sec-open'); body.style.display = ''; }
|
||||
});
|
||||
updateSectionSummaries();
|
||||
updateToggleAllBtn();
|
||||
}
|
||||
function updateToggleAllBtn() {
|
||||
const anyOpen = _allSecIds.some(id => document.getElementById(id)?.classList.contains('sec-open'));
|
||||
const btn = document.getElementById('toggleAllBtn');
|
||||
if (btn) btn.textContent = anyOpen ? 'Collapse All' : 'Expand All';
|
||||
}
|
||||
|
||||
// ── stepCount(inputId, delta, event) ─────────────────────────────
|
||||
// Increments or decrements a num-input by delta (±1) from the
|
||||
// collapsed section counter. Stops propagation so the click doesn't
|
||||
// bubble up and toggle the section open/closed.
|
||||
function stepCount(inputId, delta, event) {
|
||||
if (event) event.stopPropagation();
|
||||
const el = document.getElementById(inputId);
|
||||
if (!el) return;
|
||||
const min = parseInt(el.min) >= 0 ? parseInt(el.min) : 0;
|
||||
el.value = Math.max(min, (parseInt(el.value) || 0) + delta);
|
||||
update();
|
||||
}
|
||||
|
||||
// ── updateSectionSummaries(q) ────────────────────────────────────
|
||||
@@ -574,6 +695,7 @@ function updateSavings(q) {
|
||||
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.style.color = '';
|
||||
} 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.style.color = 'var(--amber)';
|
||||
@@ -688,78 +810,6 @@ function restoreState() {
|
||||
} catch(e) { return false; }
|
||||
}
|
||||
|
||||
// ── exportQuote() ─────────────────────────────────────────────────
|
||||
// Builds a plain-text quote summary from calcQuote() and triggers
|
||||
// a browser file download: SVS_MSP_Quote_<ClientName>.txt
|
||||
function exportQuote() {
|
||||
const q = calcQuote();
|
||||
const lines = [
|
||||
'═══════════════════════════════════════════════════════',
|
||||
' SVS MSP — Managed Services Quote',
|
||||
'═══════════════════════════════════════════════════════',
|
||||
` Client: ${q.clientName || '(not specified)'}`,
|
||||
` Ref: ${document.getElementById('quoteRef')?.textContent || ''}`,
|
||||
` Date: ${document.getElementById('headerDate')?.textContent || ''}`,
|
||||
'───────────────────────────────────────────────────────',
|
||||
'',
|
||||
' SERVICES SUMMARY',
|
||||
'',
|
||||
];
|
||||
if (q.users > 0) {
|
||||
lines.push(` User Package (${q.users} users @ ${fmt(q.totalUserRate)}/user)`);
|
||||
lines.push(` Base (${q.byol ? 'BYOL' : 'M365 Incl.'}): ${fmt(q.userBase)}/mo`);
|
||||
if (q.userExt) lines.push(` Extended Hours: ${fmt(q.userExt)}/mo`);
|
||||
if (q.userPWM) lines.push(` 1Password: ${fmt(q.userPWM)}/mo`);
|
||||
if (q.userINKY) lines.push(` INKY Pro: ${fmt(q.userINKY)}/mo`);
|
||||
if (q.userZT) lines.push(` Zero Trust: ${fmt(q.userZT)}/mo`);
|
||||
lines.push(` Subtotal: ${fmt(q.userTotal)}/mo`);
|
||||
lines.push('');
|
||||
}
|
||||
if (q.endpoints > 0 || q.servers > 0) {
|
||||
lines.push(` Endpoint Package (${q.endpoints} endpoints @ $35)`);
|
||||
if (q.endpointBMB) lines.push(` + Bare Metal Backup: ${fmt(q.endpointBMB)}/mo`);
|
||||
if (q.endpointUSB) lines.push(` + USB Blocking: ${fmt(q.endpointUSB)}/mo`);
|
||||
if (q.servers > 0) lines.push(` + ${q.servers} Server(s) @ $120: ${fmt(q.serverBase)}/mo`);
|
||||
lines.push(` Subtotal: ${fmt(q.endpointTotal)}/mo`);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push(` Site Admin Fee: ${fmt(q.adminFeeNet)}/mo`);
|
||||
lines.push('');
|
||||
if (q.ztNetTotal > 0) {
|
||||
lines.push(` Zero Trust Networking: ${fmt(q.ztNetTotal)}/mo`);
|
||||
lines.push('');
|
||||
}
|
||||
if (q.voipTotal > 0) {
|
||||
lines.push(` VoIP / UCaaS (${q.voipSeats} seats, ${q.voipTier}): ${fmt(q.voipTotal)}/mo`);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('───────────────────────────────────────────────────────');
|
||||
if (q.discountPct > 0) {
|
||||
const termLabel = q.contractTerm === '12mo' ? '12-Month' : '24-Month';
|
||||
lines.push(` BASE MRR: ${fmt(q.MRR)}`);
|
||||
lines.push(` ${termLabel} DISCOUNT (${Math.round(q.discountPct*100)}%): −${fmt(q.discountAmt)}`);
|
||||
}
|
||||
lines.push(` MONTHLY RECURRING (MRR): ${fmt(q.effectiveMrr)}`);
|
||||
if (q.hstEnabled) lines.push(` + HST (13%): ${fmt(q.hstAmt)}`);
|
||||
lines.push(` ANNUAL PROJECTION: ${fmt(q.effectiveAnnual)}`);
|
||||
if (q.oneTimeFee > 0) lines.push(` ONBOARDING FEE: ${fmt(q.oneTimeFee)} (one-time, not recurring)`);
|
||||
if (q.users > 0) lines.push(` AVG. COST PER USER: ${fmt(q.effectiveMrr / q.users)}/user/mo (all services)`);
|
||||
lines.push(` HST: ${q.hstEnabled ? 'Included in figures above' : 'Not included — HST applies at 13% on invoice'}`);
|
||||
lines.push('───────────────────────────────────────────────────────');
|
||||
lines.push('');
|
||||
lines.push('Prepared by SVS MSP | This quote is valid for 30 days from date of issue.');
|
||||
lines.push('Questions? Contact your SVS MSP account representative.');
|
||||
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const client = (q.clientName || 'Quote').replace(/[^a-z0-9]/gi,'_');
|
||||
a.href = url;
|
||||
a.download = `SVS_MSP_Quote_${client}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// ── printInvoice() ────────────────────────────────────────────────
|
||||
// Generates a clean invoice-style HTML document in a new window
|
||||
// and triggers the browser print dialog (Save as PDF works perfectly).
|
||||
@@ -803,7 +853,7 @@ function printInvoice() {
|
||||
}
|
||||
if (q.voipTotal > 0) {
|
||||
const tier = {basic:'Basic',standard:'Standard',premium:'Premium'}[q.voipTier] || 'Basic';
|
||||
row(`VoIP / UCaaS — ${tier}`, `${q.voipSeats} seat${q.voipSeats!==1?'s':''} × $${VOIP_RATES[q.voipTier]}/mo`, fmt(q.voipSeatsAmt));
|
||||
row(`VoIP / UCaaS — ${tier}`, `${q.voipSeats} seat${q.voipSeats!==1?'s':''} × $${q.voipSeatRate}/mo`, fmt(q.voipSeatsAmt));
|
||||
if (q.voipPhoneAmt > 0) row('↳ Desk Phone HaaS (+$15/seat)', '', fmt(q.voipPhoneAmt), true);
|
||||
if (q.voipFaxAmt > 0) row('↳ Virtual Fax (+$10/mo)', '', fmt(q.voipFaxAmt), true);
|
||||
}
|
||||
@@ -1042,7 +1092,8 @@ function initTheme() {
|
||||
// Entry point. Sets quote ref (SVS-YYYYMMDD-XXXX) and date,
|
||||
// restores saved state, then calls update() for initial render.
|
||||
// Called once at bottom of script.
|
||||
function initQuote() {
|
||||
async function initQuote() {
|
||||
await loadPricing();
|
||||
const now = new Date();
|
||||
const months = ['JANUARY','FEBRUARY','MARCH','APRIL','MAY','JUNE','JULY','AUGUST','SEPTEMBER','OCTOBER','NOVEMBER','DECEMBER'];
|
||||
const year = now.getFullYear();
|
||||
@@ -1135,7 +1186,6 @@ initQuote();
|
||||
syncEl('annualDisplay');
|
||||
syncEl('perUserDisplay');
|
||||
syncEl('perUserBreakdown');
|
||||
syncEl('adminPct');
|
||||
syncEl('m365SaveAmt');
|
||||
syncEl('sl-discount-val');
|
||||
syncEl('sl-base-mrr-val');
|
||||
@@ -1162,7 +1212,6 @@ initQuote();
|
||||
syncClass('sl-zt');
|
||||
syncClass('sl-voip');
|
||||
syncClass('sl-admin');
|
||||
syncClass('sideNote-admin');
|
||||
syncClass('sideNote-m365');
|
||||
syncClass('sideNote-byol');
|
||||
syncClass('vsComparison');
|
||||
@@ -1173,9 +1222,6 @@ initQuote();
|
||||
syncClass('sl-hst-row');
|
||||
syncClass('sl-hst-total-row');
|
||||
syncClass('sl-otf-row');
|
||||
syncStyle('sl-discount-row');
|
||||
syncStyle('sl-hst-row');
|
||||
syncStyle('sl-otf-row');
|
||||
syncClass('vs-1man-save-row');
|
||||
syncClass('vs-1man-save');
|
||||
syncClass('vs-1man-save-lbl');
|
||||
|
||||
Reference in New Issue
Block a user