Files
hermes-config/skills/self-hosting/shop-pro-quote/references/quote-save-flow.md
T
2026-07-12 10:17:17 -04:00

184 lines
8.6 KiB
Markdown

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