# 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 `<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.
