8.6 KiB
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
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:
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:
initializeQuoteTab()→loadSavedQuotesEnhanced()- Queries
quotescollection filtered byuserId, ordered bylastUpdated desc - Renders into element
#saved-quotes-list(sidebar card, up to 5 quotes) - If >0 quotes, shows "See All N Quotes" button → opens
#quotes-modalwith full searchable/sortable list
Delete flow
deleteQuote(quoteId) in quote-tab-manager.js:
confirm('Delete this quote permanently?')— user confirmationdeleteDoc(doc(db, 'quotes', quoteId))via pocketbase.js adapter- Remove from
allQuotesDatacache for immediate modal update await loadSavedQuotesEnhanced()— refresh sidebar list from DB- If
#quotes-modalis 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:
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:
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 <select> with a custom React dropdown in the Recent Quotes section. Pattern:
- Trigger: styled
<button>with placeholder text + rotatingChevronDownicon - Panel: absolutely positioned
<div>below trigger,z-50,max-h-72with scroll - Close: click-outside handler via
useRef+mousedownlistener inuseEffect - Items: Each shows customer name + RO# on top line, vehicle + date on second line (left), total + status badge (right)
- Navigation: Selecting an item calls
navigate()and closes the panel
See references/ui-pitfalls.md for the React custom dropdown pattern details.
v1 Saved Quotes UI (legacy)
#saved-quotes-list (sidebar card)
Inside #generate-quote-content, below the Summary + Actions row. A "Saved Quotes" card with header (clipboard icon, title, subtitle) and body containing the list. Shows up to 5 recent quotes. Each quote card shows customer name, vehicle, date, and total. Trash icon appears on hover top-right. Click card to load quote.
#quotes-modal (full modal)
Before <script> tags in repair-orders.html. Modal with:
- Header: "All Saved Quotes" title + close button (X)
- Search bar:
#quotes-modal-search-main— filters by customer, vehicle, or ID - Sort dropdown:
#quotes-modal-sort-main— newest first, oldest first, customer name, highest total, lowest total - Count display:
#quotes-count-display— shows "Showing X of Y total quotes" - List:
#modal-quotes-list— each row has customer/vehicle on left, total/date on right, trash icon on hover - Empty state:
#quotes-empty-state— shown when no quotes match filter - Footer: Close button
#quotes-modal-close-btn-2-main
Both close buttons (#quotes-modal-close-btn and #quotes-modal-close-btn-2-main) call closeQuotesModal(). Click-outside-to-close works via the global modal handler.
PocketBase quotes collection schema
Full field list (26 fields as of 2026-06-07):
| Field | Type | Required | Notes |
|---|---|---|---|
id |
text | ✓ | PocketBase auto-generated |
userId |
text | ✓ | PB user ID |
customerName |
text | ||
customerPhone |
text | ||
customerEmail |
text | legacy, unused by saveQuote | |
serviceAdvisor |
text | ||
repairOrderNumber |
text | ||
vehicleInfo |
text | e.g. "2019 Honda Pilot" | |
mileage |
text | ||
vin |
text | ||
services |
json | array of service objects | |
discountValue |
number | ||
discountType |
text | "dollar" or "percent" | |
subtotal |
number | legacy, unused by saveQuote | |
tax |
number | legacy, unused by saveQuote | |
total |
number | ||
lastUpdated |
date | ISO 8601 string | |
createdAt |
text | legacy | |
updatedAt |
text | legacy | |
deviceModel |
text | legacy | |
deviceType |
text | legacy | |
issue |
text | legacy | |
status |
text | legacy | |
notes |
text | legacy | |
customerId |
text | legacy | |
repairOrderId |
text | legacy |
Schema mismatch is the #1 cause of "Error saving quote". The collection had leftover fields from a previous use (device repair tracking). The 9 fields needed by saveQuote (serviceAdvisor, repairOrderNumber, vehicleInfo, mileage, vin, services, discountValue, discountType, lastUpdated) were all missing. Fix: PATCH /api/collections/quotes with the complete fields array. See references/pocketbase-schema-patch.md for the Python script pattern.