Files
svsmspcalc/quote-persistence.js
2026-03-15 18:24:36 -04:00

241 lines
8.6 KiB
JavaScript

(function(global) {
'use strict';
const SAVE_KEY = 'svs-msp-quote-v1';
const QUOTE_REF_KEY = 'svs-msp-quote-ref';
let saveTimer;
function saveState() {
try {
const state = {
clientName: document.getElementById('clientName')?.value || '',
users: parseInt(document.getElementById('userCount')?.value, 10) || 0,
endpoints: parseInt(document.getElementById('endpointCount')?.value, 10) || 0,
servers: parseInt(document.getElementById('serverCount')?.value, 10) || 0,
byol: document.getElementById('rateBYOL')?.checked || false,
addExtHours: document.getElementById('addExtHours')?.checked || false,
addPWM: document.getElementById('addPWM')?.checked || false,
addINKY: document.getElementById('addINKY')?.checked || false,
addZT: document.getElementById('addZT')?.checked || false,
addUSB: document.getElementById('addUSB')?.checked || false,
addBMB: document.getElementById('addBMB')?.checked || false,
ztSeats: parseInt(document.getElementById('ztNetSeats')?.value, 10) || 0,
ztRouters: parseInt(document.getElementById('ztNetRouters')?.value, 10) || 0,
voipTier: (document.querySelector('input[name="voipTier"]:checked') || {}).value || 'basic',
voipSeats: parseInt(document.getElementById('voipSeats')?.value, 10) || 0,
addVoipPhone: document.getElementById('addVoipPhone')?.checked || false,
addVoipFax: document.getElementById('addVoipFax')?.checked || false,
phoneBill: parseFloat(document.getElementById('currentPhoneBill')?.value) || 0,
contractTerm: (document.querySelector('input[name="contractTerm"]:checked') || {}).value || 'm2m',
hstEnabled: document.getElementById('hstToggle')?.checked || false,
oneTimeFee: parseFloat(document.getElementById('oneTimeFee')?.value) || 0,
adminWaived: document.getElementById('adminWaived')?.checked || false,
onboardingWaived: document.getElementById('onboardingWaived')?.checked || false,
onboardingManual: document.getElementById('oneTimeFee')?.dataset.manual === '1',
repName: document.getElementById('repName')?.value || '',
quoteNotes: document.getElementById('quoteNotes')?.value || ''
};
localStorage.setItem(SAVE_KEY, JSON.stringify(state));
} catch (e) {
console.warn('saveState: failed to persist quote', e);
}
}
function debouncedSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(saveState, 400);
}
function syncBodyScrollLock() {
const panelOpen = document.getElementById('mobileQuotePanel')?.classList.contains('open');
const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open');
const sidebarFocusOpen = document.body.classList.contains('sidebar-focus-open');
document.body.style.overflow = (panelOpen || modalOpen || sidebarFocusOpen) ? '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();
}
function trapFocusInModal(e) {
var modal = document.getElementById('resetConfirmModal');
if (!modal || !modal.classList.contains('open')) return;
var focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener('keydown', function(e) {
const modalOpen = document.getElementById('resetConfirmModal')?.classList.contains('open');
if (e.key === 'Escape' && modalOpen) {
closeResetConfirm();
e.preventDefault();
e.stopImmediatePropagation();
return;
}
if (e.key === 'Tab' && modalOpen) {
trapFocusInModal(e);
}
});
function restoreState() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw) return false;
const s = JSON.parse(raw);
const set = (id, val) => {
const el = document.getElementById(id);
if (el) el.value = val;
};
const check = (id, val) => {
const el = document.getElementById(id);
if (el) el.checked = !!val;
};
set('clientName', s.clientName);
set('userCount', s.users);
set('endpointCount', s.endpoints);
set('serverCount', s.servers);
check('rateBYOL', s.byol);
check('rateM365', !s.byol);
check('addExtHours', s.addExtHours);
check('addPWM', s.addPWM);
check('addINKY', s.addINKY);
check('addZT', s.addZT);
check('addUSB', s.addUSB);
check('addBMB', s.addBMB);
set('ztNetSeats', s.ztSeats);
set('ztNetRouters', s.ztRouters);
const tierEl = document.querySelector(`input[name="voipTier"][value="${s.voipTier}"]`);
if (tierEl) tierEl.checked = true;
set('voipSeats', s.voipSeats);
check('addVoipPhone', s.addVoipPhone);
check('addVoipFax', s.addVoipFax);
set('currentPhoneBill', s.phoneBill);
const termEl = document.querySelector(`input[name="contractTerm"][value="${s.contractTerm || 'm2m'}"]`);
if (termEl) termEl.checked = true;
check('hstToggle', s.hstEnabled);
check('adminWaived', s.adminWaived);
check('onboardingWaived', s.onboardingWaived);
if (s.onboardingManual && !s.onboardingWaived) {
set('oneTimeFee', s.oneTimeFee || 0);
const feeEl = document.getElementById('oneTimeFee');
if (feeEl) feeEl.dataset.manual = '1';
}
set('repName', s.repName || '');
const notesEl = document.getElementById('quoteNotes');
if (notesEl) notesEl.value = s.quoteNotes || '';
const rowMap = {
addExtHours: 'row-ext',
addPWM: 'row-pwm',
addINKY: 'row-inky',
addZT: 'row-zt',
addBMB: 'row-bmb',
addUSB: 'row-usb',
addVoipPhone: 'row-vphone',
addVoipFax: 'row-vfax'
};
['addExtHours', 'addPWM', 'addINKY', 'addZT', 'addBMB', 'addUSB', 'addVoipPhone', 'addVoipFax'].forEach(id => {
const cb = document.getElementById(id);
if (cb?.checked) {
const row = document.getElementById(rowMap[id]);
if (row) row.classList.add('selected');
}
});
return true;
} catch (e) {
return false;
}
}
function getOrCreateQuoteRef(now) {
const currentDate = now || new Date();
const dateStr = `${currentDate.getFullYear()}${String(currentDate.getMonth() + 1).padStart(2, '0')}${String(currentDate.getDate()).padStart(2, '0')}`;
const savedRef = localStorage.getItem(QUOTE_REF_KEY);
if (savedRef) {
const match = savedRef.match(/^SVS-(\d{4})(\d{2})(\d{2})-/);
const refDate = match ? new Date(+match[1], +match[2] - 1, +match[3]) : null;
const ageMs = refDate ? currentDate - refDate : Infinity;
if (ageMs <= 30 * 24 * 60 * 60 * 1000) {
return savedRef;
}
}
const quoteRef = `SVS-${dateStr}-${String(Math.floor(Math.random() * 9000) + 1000)}`;
localStorage.setItem(QUOTE_REF_KEY, quoteRef);
return quoteRef;
}
global.SVSQuotePersistence = {
SAVE_KEY,
QUOTE_REF_KEY,
saveState,
debouncedSave,
syncBodyScrollLock,
openResetConfirm,
closeResetConfirm,
confirmResetQuote,
restoreState,
getOrCreateQuoteRef
};
global.SAVE_KEY = SAVE_KEY;
global.QUOTE_REF_KEY = QUOTE_REF_KEY;
global.saveState = saveState;
global.debouncedSave = debouncedSave;
global.syncBodyScrollLock = syncBodyScrollLock;
global.openResetConfirm = openResetConfirm;
global.closeResetConfirm = closeResetConfirm;
global.confirmResetQuote = confirmResetQuote;
global.restoreState = restoreState;
global.getOrCreateQuoteRef = getOrCreateQuoteRef;
})(window);