// Pricing defaults and JSON loading live in quote-pricing.js. // This file intentionally consumes the pricing globals exposed there // so Phase 1 can stay low-risk and preserve the current runtime API. // Rendering helpers and nudge state live in quote-render.js. // Persistence, export/print, theme, and mobile sync live in dedicated // modules so this file can stay focused on quote calculation orchestration. // --- CALC --- // ── calcQuote() ───────────────────────────────────────────────── // Compatibility wrapper around the pure Phase 2 quote engine. // Reads live form state from the DOM, then delegates pricing math to // calculateQuote(state, pricing) without changing the rest of the app. function calcQuote() { return calculateQuote(readFormState(), getPricingConfig()); } // --- MAIN UPDATE --- // ── update() ───────────────────────────────────────────────────── // Master DOM update function. Called on every input change. // Sequence: calcQuote → update all displays → renderNudge → // updateSavings → updateVsComparison → updateSectionSummaries // Also wrapped by the mobile IIFE below to sync _m elements. // Do not call renderNudge() directly from other functions; // always go through update() to keep _m panel in sync. function update() { const q = calcQuote(); const pricing = getPricingConfig(); const bestM365Rate = Math.max(pricing.RATE_M365_M2M || 0, pricing.RATE_M365 || 0); const m365BundleSavings = Math.max(0, bestM365Rate - pricing.RATE_BYOL); const render = window.SVSQuoteRender; // ── Onboarding fee logic ── // m2m: auto = 50% MRR, manual override allowed, waive toggle available // 12-month: auto = 50% off onboarding (25% of MRR), manual override allowed // 24-month: complimentary (fully waived), input disabled const waivedEl = document.getElementById('onboardingWaived'); const feeEl = document.getElementById('oneTimeFee'); const fullOnboarding = Math.round(q.MRR / 2); if (waivedEl && q.contractTerm === '24mo') { // 24-month: fully complimentary waivedEl.checked = true; waivedEl.disabled = true; waivedEl.dataset.autoWaived = '1'; // Preserve any manual override so it survives the round-trip back to m2m/12mo if (feeEl && feeEl.dataset.manual) { feeEl.dataset.manualValue = feeEl.value; delete feeEl.dataset.manual; } } else if (waivedEl && q.contractTerm === '12mo') { // 12-month: 50% off onboarding, not waived — clear any auto-waive state waivedEl.disabled = false; if (waivedEl.dataset.autoWaived) { waivedEl.checked = false; delete waivedEl.dataset.autoWaived; } // Restore manual override if user had one before switching to 24mo if (feeEl && !feeEl.dataset.manual && feeEl.dataset.manualValue) { feeEl.dataset.manual = '1'; feeEl.value = feeEl.dataset.manualValue; delete feeEl.dataset.manualValue; } } else if (waivedEl) { // m2m: full manual control waivedEl.disabled = false; if (waivedEl.dataset.autoWaived) { waivedEl.checked = false; delete waivedEl.dataset.autoWaived; } // Restore manual override if user had one before switching to 24mo if (feeEl && !feeEl.dataset.manual && feeEl.dataset.manualValue) { feeEl.dataset.manual = '1'; feeEl.value = feeEl.dataset.manualValue; delete feeEl.dataset.manualValue; } } const waived = waivedEl?.checked || false; let oneTimeFee; if (waived) { oneTimeFee = 0; if (feeEl) { feeEl.value = ''; feeEl.disabled = true; feeEl.placeholder = 'Complimentary'; } } else if (q.contractTerm === '12mo') { // 12-month: 50% off the standard onboarding fee if (feeEl) { feeEl.disabled = false; feeEl.placeholder = '50% off'; } if (feeEl && !feeEl.dataset.manual) { oneTimeFee = Math.round(fullOnboarding / 2); feeEl.value = oneTimeFee > 0 ? oneTimeFee : ''; } else { oneTimeFee = parseFloat(feeEl?.value) || 0; } } else { // m2m: standard auto-calc if (feeEl) { feeEl.disabled = false; feeEl.placeholder = 'auto'; } if (feeEl && !feeEl.dataset.manual) { oneTimeFee = fullOnboarding; feeEl.value = oneTimeFee > 0 ? oneTimeFee : ''; } else { oneTimeFee = parseFloat(feeEl?.value) || 0; } } q.oneTimeFee = oneTimeFee; const renderOptions = { m365BundleSavings, oneTimeFee, onboardingWaived: waived, onboardingWouldBe: fullOnboarding, onboardingHalfOff: q.contractTerm === '12mo' && !waived }; render.renderQuoteUi(q, renderOptions); render.renderSidebar(q, renderOptions); render.setNudges(render.buildNudges(q, renderOptions)); renderNudge(); updateSavings(q); updateVsComparison(q); updateSectionSummaries(q); debouncedSave(); } // ── onWaiveToggle() ────────────────────────────────────────────── // Called from onchange on #onboardingWaived checkbox. // Clears the manual override flag on the fee input so auto-calc resumes, // then runs update(). Extracted from inline HTML attribute for clarity. function onWaiveToggle() { const feeInput = document.getElementById('oneTimeFee'); if (feeInput) feeInput.removeAttribute('data-manual'); update(); } // ── toggleSection(id) ──────────────────────────────────────────── // Collapses/expands a numbered section card. // Adds/removes .sec-open on the section element. // .sec-open → chevron rotates 180deg (CSS), body shown (JS display). // Calls updateSectionSummaries() to show/hide summary badges. // Map: section ID → collapsible IDs that should auto-expand when section opens const _sectionCollapsibles = {}; function finishSectionAnimation(body, isOpen) { body.style.transition = ''; body.style.overflow = ''; body.style.height = ''; body.style.opacity = ''; body.style.display = isOpen ? '' : 'none'; } function animateSectionBody(body, open) { if (!body) return; if (body._sectionAnimationCleanup) { body._sectionAnimationCleanup(); body._sectionAnimationCleanup = null; } body.style.overflow = 'hidden'; body.style.transition = 'height 0.34s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.22s ease'; if (open) { body.style.display = ''; body.style.height = '0px'; body.style.opacity = '0'; body.getBoundingClientRect(); const targetHeight = body.scrollHeight; requestAnimationFrame(() => { body.style.height = targetHeight + 'px'; body.style.opacity = '1'; }); } else { const startHeight = body.scrollHeight || body.offsetHeight; body.style.display = ''; body.style.height = startHeight + 'px'; body.style.opacity = '1'; body.getBoundingClientRect(); requestAnimationFrame(() => { body.style.height = '0px'; body.style.opacity = '0'; }); } const onEnd = (event) => { if (event.target !== body || event.propertyName !== 'height') return; body.removeEventListener('transitionend', onEnd); body._sectionAnimationCleanup = null; finishSectionAnimation(body, open); }; body._sectionAnimationCleanup = () => { body.removeEventListener('transitionend', onEnd); finishSectionAnimation(body, open); }; body.addEventListener('transitionend', onEnd); } function toggleSection(id) { const section = document.getElementById(id); const body = document.getElementById(id + '-body'); if (!section || !body) return; const isOpen = section.classList.toggle('sec-open'); // Sync aria-expanded on the section-toggle header const header = section.querySelector('.section-toggle'); if (header) header.setAttribute('aria-expanded', String(isOpen)); animateSectionBody(body, isOpen); // Auto-expand inner collapsibles when section opens if (isOpen && _sectionCollapsibles[id]) { _sectionCollapsibles[id].forEach(cid => { const cBody = document.getElementById(cid); const cIcon = document.getElementById(cid + '-icon'); const cPreview = document.getElementById(cid + '-preview'); if (cBody && !cBody.classList.contains('open')) { cBody.classList.add('open'); if (cIcon) cIcon.classList.add('open'); if (cPreview) cPreview.style.display = 'none'; } }); } updateSectionSummaries(calcQuote()); updateToggleAllBtn(); } // ── toggleAllSections() / updateToggleAllBtn() ──────────────────── // Collapse all if any are open; expand all if all are closed. // Button label reflects current state. const _allSecIds = ['sec-02','sec-03','sec-01','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'); animateSectionBody(body, false); } else { section.classList.add('sec-open'); animateSectionBody(body, true); // Auto-expand inner collapsibles if (_sectionCollapsibles[id]) { _sectionCollapsibles[id].forEach(cid => { const cBody = document.getElementById(cid); const cIcon = document.getElementById(cid + '-icon'); const cPreview = document.getElementById(cid + '-preview'); if (cBody && !cBody.classList.contains('open')) { cBody.classList.add('open'); if (cIcon) cIcon.classList.add('open'); if (cPreview) cPreview.style.display = 'none'; } }); } } }); updateSectionSummaries(calcQuote()); updateToggleAllBtn(); } function updateToggleAllBtn() { const anyOpen = _allSecIds.some(id => document.getElementById(id)?.classList.contains('sec-open')); const btn = document.getElementById('toggleAllBtn'); if (!btn) return; const collapseSpan = btn.querySelector('.toggle-all-collapse-icon'); const expandSpan = btn.querySelector('.toggle-all-expand-icon'); const textSpan = btn.querySelector('.toggle-all-label'); if (collapseSpan) collapseSpan.style.display = anyOpen ? '' : 'none'; if (expandSpan) expandSpan.style.display = anyOpen ? 'none' : ''; if (textSpan) textSpan.textContent = anyOpen ? 'Collapse All' : 'Expand All'; } // ── toggleCollapsible(id) ───────────────────────────────────────── // Collapses/expands inner content panels (What's Included, Add-Ons). // Separate from section-level toggle. Toggles .open on .collapsible-body. // Also toggles preview pills (shown when collapsed, hidden when open). function toggleCollapsible(id) { const body = document.getElementById(id); const icon = document.getElementById(id + '-icon'); const preview = document.getElementById(id + '-preview'); if (!body) return; const open = body.classList.toggle('open'); if (icon) icon.classList.toggle('open', open); if (preview) preview.style.display = open ? 'none' : 'flex'; // Sync aria-expanded on the collapsible header that controls this body const header = body.previousElementSibling; if (header && header.classList.contains('collapsible-header')) { header.setAttribute('aria-expanded', String(open)); } } // ── toggleAddon(checkId, rowId) ───────────────────────────────── // Flips the hidden checkbox + toggles .selected on the visible row. // Called via onclick on the