Glas Theme Coming Next
This commit is contained in:
@@ -351,7 +351,7 @@ function update() {
|
||||
const subParts = [`${users} × ${fmt(baseUserRate)}/user (${byol ? 'BYOL' : 'M365 Incl.'})`];
|
||||
if (addExtHours) subParts.push(`+ ${fmt(userExt)}/mo ext. hrs`);
|
||||
if (addPWM) subParts.push(`+ ${fmt(userPWM)}/mo 1Password`);
|
||||
if (addINKY) subParts.push(`+ ${fmt(userINKY)}/mo INKY`);
|
||||
if (addINKY) subParts.push(`+ ${fmt(userINKY)}/mo INKY Pro upgrade`);
|
||||
if (addZT) subParts.push(`+ ${fmt(userZT)}/mo Zero Trust`);
|
||||
sub.innerHTML = subParts.join('<br>');
|
||||
}
|
||||
@@ -376,6 +376,7 @@ function update() {
|
||||
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 =
|
||||
@@ -384,6 +385,13 @@ function update() {
|
||||
slAdminEl?.classList.remove('sl-admin-waived');
|
||||
if (slAdminValEl) slAdminValEl.textContent = fmt(adminFeeNet);
|
||||
}
|
||||
if (slAdminSubEl) {
|
||||
const adminParts = [`Base ${fmt(siteAdminBase)}/mo`];
|
||||
if (ztActive) adminParts.push(`+ ${fmt(ADMIN_FEE_ZT)}/mo Zero Trust supplement`);
|
||||
if (addPWM && admin1PWM > 0) adminParts.push(`+ ${fmt(admin1PWM)}/mo 1Password admin`);
|
||||
slAdminSubEl.classList.remove('hidden');
|
||||
slAdminSubEl.innerHTML = adminParts.join('<br>');
|
||||
}
|
||||
|
||||
// MRR + totals — show effective MRR (after term discount) as the headline number
|
||||
getEl('mrrDisplay').textContent = fmt(effectiveMrr);
|
||||
@@ -546,7 +554,6 @@ function onWaiveToggle() {
|
||||
// Calls updateSectionSummaries() to show/hide summary badges.
|
||||
// Map: section ID → collapsible IDs that should auto-expand when section opens
|
||||
const _sectionCollapsibles = {
|
||||
'sec-01': ['adminCovered'],
|
||||
'sec-02': ['userIncluded', 'addonsA'],
|
||||
'sec-03': ['endpointIncluded', 'addonsB'],
|
||||
'sec-04': ['serverIncluded'],
|
||||
@@ -849,6 +856,7 @@ function stepInput(id, delta) {
|
||||
|
||||
// --- AUTO-SAVE / RESTORE ---
|
||||
const SAVE_KEY = 'svs-msp-quote-v1';
|
||||
const QUOTE_REF_KEY = 'svs-msp-quote-ref';
|
||||
|
||||
function saveState() {
|
||||
try {
|
||||
@@ -886,6 +894,50 @@ function debouncedSave() {
|
||||
_saveTimer = setTimeout(saveState, 400);
|
||||
}
|
||||
|
||||
function syncBodyScrollLock() {
|
||||
const panelOpen = document.getElementById('mobileQuotePanel')?.classList.contains('open');
|
||||
const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open');
|
||||
document.body.style.overflow = (panelOpen || modalOpen) ? 'hidden' : '';
|
||||
}
|
||||
|
||||
function openResetConfirm() {
|
||||
const modal = document.getElementById('resetConfirmModal');
|
||||
if (!modal) return;
|
||||
modal.classList.add('open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
syncBodyScrollLock();
|
||||
document.getElementById('resetConfirmCancel')?.focus();
|
||||
}
|
||||
|
||||
function closeResetConfirm() {
|
||||
const modal = document.getElementById('resetConfirmModal');
|
||||
if (!modal) return;
|
||||
modal.classList.remove('open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
syncBodyScrollLock();
|
||||
}
|
||||
|
||||
function confirmResetQuote() {
|
||||
clearTimeout(_saveTimer);
|
||||
try {
|
||||
localStorage.removeItem(SAVE_KEY);
|
||||
localStorage.removeItem(QUOTE_REF_KEY);
|
||||
} catch (e) {
|
||||
console.warn('confirmResetQuote: failed to clear saved quote state', e);
|
||||
}
|
||||
closeResetConfirm();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open');
|
||||
if (e.key === 'Escape' && modalOpen) {
|
||||
closeResetConfirm();
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
// ── restoreState() ───────────────────────────────────────────────
|
||||
// Restores form state from localStorage on page load.
|
||||
// SAVE_KEY = 'svs-msp-quote-v1'.
|
||||
@@ -966,7 +1018,7 @@ function printInvoice() {
|
||||
row(`User Package — ${pkg}`, `${q.users} user${q.users!==1?'s':''} × ${fmt(q.baseUserRate)}/mo`, fmt(q.userBase));
|
||||
if (q.userExt > 0) row(`↳ Extended Hours (+${fmt(ADDON_EXT_HOURS)}/user)`, '', fmt(q.userExt), true);
|
||||
if (q.userPWM > 0) row(`↳ 1Password Business (+${fmt(ADDON_1PASSWORD)}/user)`, '', fmt(q.userPWM), true);
|
||||
if (q.userINKY > 0) row(`↳ Inky Email Security (+${fmt(ADDON_INKY)}/user)`, '', fmt(q.userINKY), true);
|
||||
if (q.userINKY > 0) row(`↳ INKY Pro Upgrade (+${fmt(ADDON_INKY)}/user)`, '', fmt(q.userINKY), true);
|
||||
if (q.userZT > 0) row(`↳ Zero Trust User (+${fmt(ADDON_ZERO_TRUST_USER)}/user)`, '', fmt(q.userZT), true);
|
||||
}
|
||||
if (q.endpoints > 0) {
|
||||
@@ -1014,7 +1066,7 @@ function printInvoice() {
|
||||
feat('Licensing Model', true, q.byol ? 'BYOL — Bring Your Own License' : 'M365 Premium Included');
|
||||
feat('Extended Help Desk Hours', q.addExtHours, q.addExtHours ? `+${fmt(ADDON_EXT_HOURS)}/user/mo` : '');
|
||||
feat('1Password Business', q.addPWM, q.addPWM ? `+${fmt(ADDON_1PASSWORD)}/user/mo` : '');
|
||||
feat('INKY Pro Email Security', q.addINKY, q.addINKY ? `+${fmt(ADDON_INKY)}/user/mo` : '');
|
||||
feat('INKY Pro Upgrade', q.addINKY, q.addINKY ? `+${fmt(ADDON_INKY)}/user/mo` : '');
|
||||
feat('Zero Trust User Access', q.addZT, q.addZT ? `+${fmt(ADDON_ZERO_TRUST_USER)}/user/mo` : '');
|
||||
feat('USB Device Blocking', q.addUSB, q.addUSB ? `+${fmt(ADDON_USB_BLOCKING)}/endpoint/mo` : '');
|
||||
feat('Bare Metal Backup', q.addBMB, q.addBMB ? `+${fmt(ADDON_BARE_METAL_BACKUP)}/endpoint/mo` : '');
|
||||
@@ -1280,22 +1332,22 @@ async function initQuote() {
|
||||
const year = now.getFullYear();
|
||||
const month = months[now.getMonth()];
|
||||
const dateStr = `${year}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
|
||||
const savedRef = localStorage.getItem('svs-msp-quote-ref');
|
||||
const savedRef = localStorage.getItem(QUOTE_REF_KEY);
|
||||
let quoteRef;
|
||||
if (savedRef) {
|
||||
// Regenerate if the baked-in date is older than 30 days
|
||||
const m = savedRef.match(/^SVS-(\d{4})(\d{2})(\d{2})-/);
|
||||
const refDate = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
|
||||
const ageMs = refDate ? now - refDate : Infinity;
|
||||
if (ageMs > 30 * 24 * 60 * 60 * 1000) {
|
||||
quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`;
|
||||
localStorage.setItem('svs-msp-quote-ref', quoteRef);
|
||||
} else {
|
||||
quoteRef = savedRef;
|
||||
}
|
||||
const refDate = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
|
||||
const ageMs = refDate ? now - refDate : Infinity;
|
||||
if (ageMs > 30 * 24 * 60 * 60 * 1000) {
|
||||
quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`;
|
||||
localStorage.setItem(QUOTE_REF_KEY, quoteRef);
|
||||
} else {
|
||||
quoteRef = savedRef;
|
||||
}
|
||||
} else {
|
||||
quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random()*9000)+1000)}`;
|
||||
localStorage.setItem('svs-msp-quote-ref', quoteRef);
|
||||
localStorage.setItem(QUOTE_REF_KEY, quoteRef);
|
||||
}
|
||||
const quoteRefEl = document.getElementById('quoteRef');
|
||||
if (quoteRefEl) quoteRefEl.textContent = quoteRef;
|
||||
@@ -1341,7 +1393,7 @@ initQuote();
|
||||
var panel = document.getElementById('mobileQuotePanel');
|
||||
if (panel) {
|
||||
panel.classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
syncBodyScrollLock();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1349,7 +1401,7 @@ initQuote();
|
||||
var panel = document.getElementById('mobileQuotePanel');
|
||||
if (panel) {
|
||||
panel.classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
syncBodyScrollLock();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1423,10 +1475,12 @@ initQuote();
|
||||
syncEl('nudgeCounter');
|
||||
syncEl('sl-users-sub');
|
||||
syncEl('sl-endpoints-sub');
|
||||
syncEl('sl-admin-sub');
|
||||
syncClass('sl-users');
|
||||
syncClass('sl-users-sub');
|
||||
syncClass('sl-endpoints');
|
||||
syncClass('sl-endpoints-sub');
|
||||
syncClass('sl-admin-sub');
|
||||
syncClass('sl-servers');
|
||||
syncClass('sl-zt');
|
||||
syncClass('sl-voip');
|
||||
@@ -1452,6 +1506,7 @@ initQuote();
|
||||
syncEl('adminWaivedAmt');
|
||||
syncStyle('sl-users-sub');
|
||||
syncStyle('sl-endpoints-sub');
|
||||
syncStyle('sl-admin-sub');
|
||||
syncStyle('perUserRow');
|
||||
syncChecked('hstToggle');
|
||||
// Pill MRR — show effective MRR with label
|
||||
|
||||
Reference in New Issue
Block a user