initial commit
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user