Files
2026-07-12 10:17:17 -04:00

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:

  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:

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 + rotating ChevronDown icon
  • Panel: absolutely positioned <div> below trigger, z-50, max-h-72 with scroll
  • Close: click-outside handler via useRef + mousedown listener in useEffect
  • 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.