My very First own bigboy config

This commit is contained in:
2026-03-11 17:46:45 -04:00
parent 1232b12961
commit bbe2b374a9
5 changed files with 484 additions and 193 deletions

View File

@@ -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');