/** * AI Quick Quote widget — floating panel for generating quick phone/walk-in estimates. * Follows the same IIFE + sessionStorage pattern as ai-help-widget.js. */ (function () { 'use strict'; const SESSION_KEY = 'qqWidgetState'; const el = { widget: document.getElementById('qq-widget'), btn: document.getElementById('qq-btn'), panel: document.getElementById('qq-panel'), closeBtn: document.getElementById('qq-close'), token: document.getElementById('qq-token'), // Step 1 stepInput: document.getElementById('qq-step-input'), description: document.getElementById('qq-description'), qty: document.getElementById('qq-qty'), coats: document.getElementById('qq-coats'), analyzeBtn: document.getElementById('qq-analyze-btn'), inputError: document.getElementById('qq-input-error'), // Step 2 stepResults: document.getElementById('qq-step-results'), resDesc: document.getElementById('qq-res-description'), resSqft: document.getElementById('qq-res-sqft'), resComplexity:document.getElementById('qq-res-complexity'), resMinutes: document.getElementById('qq-res-minutes'), resPrice: document.getElementById('qq-res-price'), resOven: document.getElementById('qq-res-oven'), resConfidence:document.getElementById('qq-res-confidence'), resReasoning: document.getElementById('qq-res-reasoning'), powderSection:document.getElementById('qq-powder-section'), powderList: document.getElementById('qq-powder-list'), reference: document.getElementById('qq-reference'), backBtn: document.getElementById('qq-back-btn'), saveBtn: document.getElementById('qq-save-btn'), saveError: document.getElementById('qq-save-error'), // Shared loading: document.getElementById('qq-loading'), }; if (!el.widget) return; // partial not rendered (unauthenticated) // ── State ──────────────────────────────────────────────────────────────── let isOpen = false; let lastResult = null; // AiQuickQuoteResult from last successful Analyze call function saveState() { try { sessionStorage.setItem(SESSION_KEY, JSON.stringify({ isOpen })); } catch (_) { /* private browsing */ } } function restoreState() { try { const raw = sessionStorage.getItem(SESSION_KEY); if (!raw) return; const state = JSON.parse(raw); if (state.isOpen) openPanel(false); } catch (_) { /* corrupt state */ } } // ── Panel open/close ───────────────────────────────────────────────────── function openPanel(animate) { isOpen = true; el.panel.removeAttribute('hidden'); el.btn.setAttribute('aria-expanded', 'true'); if (animate) el.panel.style.animation = 'none'; // instant on restore saveState(); } function closePanel() { isOpen = false; el.panel.setAttribute('hidden', ''); el.btn.setAttribute('aria-expanded', 'false'); saveState(); } el.btn.addEventListener('click', () => isOpen ? closePanel() : openPanel(true)); el.closeBtn.addEventListener('click', closePanel); // Close on outside click document.addEventListener('mousedown', function (e) { if (isOpen && !el.widget.contains(e.target)) closePanel(); }); // ── Step navigation ────────────────────────────────────────────────────── function showStep(step) { el.stepInput.classList.toggle('d-none', step !== 'input'); el.stepResults.classList.toggle('d-none', step !== 'results'); el.loading.classList.add('d-none'); } el.backBtn.addEventListener('click', () => { clearErrors(); showStep('input'); }); // ── Analyze ────────────────────────────────────────────────────────────── el.analyzeBtn.addEventListener('click', runAnalysis); el.description.addEventListener('keydown', function (e) { if (e.key === 'Enter' && e.ctrlKey) runAnalysis(); }); async function runAnalysis() { clearErrors(); const description = el.description.value.trim(); if (!description) { showInputError('Please describe what the customer needs.'); return; } setLoading(true); try { const response = await post('/AiQuickQuote/Analyze', { description, quantity: parseInt(el.qty.value, 10) || 1, coatCount: parseInt(el.coats.value, 10) || 1 }); if (!response.success) { showInputError(response.errorMessage || 'Analysis failed. Please try again.'); return; } lastResult = response; populateResults(response); showStep('results'); } catch (err) { showInputError('Could not reach the server. Please try again.'); console.error('[QuickQuote] Analyze error:', err); } finally { setLoading(false); } } function populateResults(r) { el.resDesc.textContent = r.description || '—'; el.resSqft.textContent = r.surfaceAreaSqFt ? r.surfaceAreaSqFt.toFixed(1) + ' sqft' : '—'; el.resComplexity.textContent = r.complexity || '—'; el.resMinutes.textContent = r.estimatedMinutes ? r.estimatedMinutes + ' min' : '—'; el.resPrice.textContent = formatCurrency(r.estimatedTotal || r.estimatedUnitPrice); const ovenCost = r.breakdown?.ovenCost; const ovenMin = r.breakdown?.ovenCycleMinutes; if (ovenCost && ovenCost > 0) { el.resOven.textContent = `incl. oven 1 batch ${ovenMin ? ovenMin + ' min' : ''}: ${formatCurrency(ovenCost)}`; } else { el.resOven.textContent = ''; } el.resReasoning.textContent = r.reasoning || ''; // Confidence badge const conf = (r.confidence || 'Medium').toLowerCase(); el.resConfidence.textContent = r.confidence || 'Medium'; el.resConfidence.className = 'badge ' + ( conf === 'high' ? 'bg-success' : conf === 'medium' ? 'bg-warning text-dark' : 'bg-danger' ); // Powder stock if (r.powderMatches && r.powderMatches.length > 0) { el.powderList.innerHTML = r.powderMatches.map(buildPowderBadge).join(''); el.powderSection.classList.remove('d-none'); } else { el.powderSection.classList.add('d-none'); } } function buildPowderBadge(match) { if (match.hasInventoryMatch) { const icon = match.isInStock ? '✓' : '✗'; const cls = match.isInStock ? 'text-success border-success' : 'text-danger border-danger'; const label = match.isInStock ? `In stock — ${match.quantityOnHand.toFixed(1)} lbs` : 'Not in stock'; const name = match.inventoryItemName || match.detectedColorName; return ` ${icon} ${escHtml(name)} ${escHtml(label)} `; } return ` ? ${escHtml(match.detectedColorName)} Not in inventory `; } // ── Save ───────────────────────────────────────────────────────────────── el.saveBtn.addEventListener('click', runSave); async function runSave() { clearErrors(); if (!lastResult) return; const reference = el.reference.value.trim(); if (!reference) { showSaveError('Enter a reference (caller name or memo) before saving.'); return; } el.saveBtn.disabled = true; el.saveBtn.innerHTML = ' Saving…'; try { const body = { reference, originalDescription: el.description.value.trim(), aiDescription: lastResult.description, surfaceAreaSqFt: lastResult.surfaceAreaSqFt, complexity: lastResult.complexity, estimatedMinutes: lastResult.estimatedMinutes, requiresPreheat: lastResult.requiresPreheat, preheatMinutes: lastResult.preheatMinutes, quantity: parseInt(el.qty.value, 10) || 1, coatCount: parseInt(el.coats.value, 10) || 1, estimatedUnitPrice: lastResult.estimatedUnitPrice, materialCost: lastResult.breakdown?.materialCost ?? 0, laborCost: lastResult.breakdown?.laborCost ?? 0, ovenBatchCost: lastResult.breakdown?.ovenCost ?? 0, ovenCycleMinutes: lastResult.breakdown?.ovenCycleMinutes ?? 50 }; const response = await post('/AiQuickQuote/Save', body); if (response.success && response.redirectUrl) { closePanel(); window.location.href = response.redirectUrl; } else { showSaveError(response.errorMessage || 'Save failed. Please try again.'); } } catch (err) { showSaveError('Could not reach the server. Please try again.'); console.error('[QuickQuote] Save error:', err); } finally { el.saveBtn.disabled = false; el.saveBtn.innerHTML = ' Save as Draft Quote'; } } // ── Utilities ──────────────────────────────────────────────────────────── async function post(url, data) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': el.token.value }, body: JSON.stringify(data) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } function setLoading(on) { el.loading.classList.toggle('d-none', !on); el.analyzeBtn.disabled = on; el.analyzeBtn.innerHTML = on ? ' Analyzing…' : ' Get Estimate'; } function showInputError(msg) { el.inputError.textContent = msg; el.inputError.classList.remove('d-none'); } function showSaveError(msg) { el.saveError.textContent = msg; el.saveError.classList.remove('d-none'); } function clearErrors() { el.inputError.classList.add('d-none'); el.saveError.classList.add('d-none'); } function formatCurrency(value) { if (!value && value !== 0) return '—'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value); } function escHtml(str) { return (str || '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // ── Init ───────────────────────────────────────────────────────────────── showStep('input'); restoreState(); })();