# Quote Save Flow & Data Model ## Save destination PocketBase `quotes` collection, via `pocketbase.js` Firebase-compatible adapter. No external APIs. ``` Save Quote button click → saveQuote() [quote-tab-manager.js:2496] → import pocketbase.js (Firebase-compat API) → pocketbase.js connects to window.location.origin (same server) → addDoc(collection(db, 'quotes'), quoteData) // new → setDoc(doc(db, 'quotes', id), {...}, {merge}) // update ``` ## Quote data shape ```javascript const quoteData = { userId: currentUser.uid, customerName: string, customerPhone: string, serviceAdvisor: string, repairOrderNumber: string, vehicleInfo: string, // e.g., "2019 Honda Pilot" mileage: string, vin: string, services: [{ ...serviceObject, // spread of the service fields from PocketBase price: number, partsNotInStock: boolean, aftermarketAvailable: boolean, aftermarketPartsList: string|null, partsDeliveryTime: string|null }], discountValue: number, // parsed from DOM input discountType: 'dollar'|'percent', total: number, // parsed from #total-amount lastUpdated: Date|Timestamp, status: 'converted'|undefined // auto-set when all services decided }; ``` ## Auto-conversion on full decision When `saveQuote()` builds `quoteData`, it checks if every service has a `customerDecision` of `'approved'` or `'declined'` (none `'pending'`). If so, it sets `quoteData.status = 'converted'` before saving. This causes the quote to drop out of the Daily Briefing's active-quote list immediately. Implementation in `quote-tab-manager.js` ~line 2995: ```javascript const allDecided = selectedServices.length > 0 && selectedServices.every(s => s.customerDecision === 'approved' || s.customerDecision === 'declined'); if (allDecided) { quoteData.status = 'converted'; } ``` ## Load flow When Quote Generator tab opens: 1. `initializeQuoteTab()` → `loadSavedQuotesEnhanced()` 2. Queries `quotes` collection filtered by `userId`, ordered by `lastUpdated desc` 3. Renders into element `#saved-quotes-list` (sidebar card, up to 5 quotes) 4. If >0 quotes, shows "See All N Quotes" button → opens `#quotes-modal` with full searchable/sortable list ## Delete flow `deleteQuote(quoteId)` in `quote-tab-manager.js`: 1. `confirm('Delete this quote permanently?')` — user confirmation 2. `deleteDoc(doc(db, 'quotes', quoteId))` via pocketbase.js adapter 3. Remove from `allQuotesData` cache for immediate modal update 4. `await loadSavedQuotesEnhanced()` — refresh sidebar list from DB 5. If `#quotes-modal` is open, `populateAndRenderQuotesModal(allQuotesData)` to refresh it **Delete buttons**: trash can SVG icon (heroicons outline) appears on hover via `opacity-0 group-hover:opacity-100`. Uses `e.stopPropagation()` so clicking trash doesn't also trigger load. Pattern applied in both the sidebar cards and the modal list. ## UI Elements (both were missing — now exist) ### v2 Save Flow (React/Vite) The v2 QuoteGenerator has a distinct save flow from the legacy v1. Key differences: ### handleSave — top-level field sync The v2 `handleSave()` writes customer info both as a nested `customerInfo` JSON blob AND as individual top-level fields: ```tsx const data = { userId: pb.authStore.model?.id, customerName: customerInfo.name, vehicleInfo: customerInfo.vehicleInfo, roNumber: customerInfo.roNumber, customerInfo: JSON.parse(JSON.stringify(customerInfo)), services: JSON.parse(JSON.stringify(services)), discount: JSON.parse(JSON.stringify(discount)), total: grandTotal, status: 'draft', createdAt: new Date().toISOString(), }; ``` The top-level fields (`customerName`, `vehicleInfo`, `roNumber`) serve the Dashboard's list queries (`fields: 'id,customerName,vehicleInfo,roNumber,total,status,createdAt,lastUpdated'`). Without them, the Dashboard shows empty values for records created by the new app. **Pitfall (fixed 2026-06-28)**: The original v2 `handleSave` only saved `customerInfo` as a JSON blob. Dashboard queries for `customerName` and `vehicleInfo` returned empty. Fix: add the three top-level fields alongside the nested blob. ### Load logic — merged fallback pattern When loading a quote via `?edit=ID`, the v2 code merges top-level fields with nested JSON to handle both old (individual top-level fields) and new (nested `customerInfo` JSON) record formats: ```tsx const rawCustomerInfo = data.customerInfo ? { ...data, ...(typeof data.customerInfo === 'string' ? JSON.parse(data.customerInfo) : data.customerInfo) } : data; const ci = normalizeCustomerInfo(rawCustomerInfo); ``` The merge `{ ...data, ...customerInfo }` means nested `customerInfo` fields take priority, but top-level fields fill in any gaps (e.g., old records that stored `roNumber` as a flat field but not inside `customerInfo`). ### applyShopCharge default In v2, `applyShopCharge` must default to `true` for new services added via `handleAdd()`. The store default (`undefined`) is falsy, producing $0 shop charge and hiding the row in Quote Summary. See `v2-quote-pdf-architecture.md` for the fix. ### Dashboard Quote Jump dropdown (v2) Replaced the native `