82 lines
3.8 KiB
Markdown
82 lines
3.8 KiB
Markdown
# RO-Quote Sync Mechanism
|
|
|
|
## Overview
|
|
|
|
When a quote is generated from a Repair Order (RO), approved services on the quote sync back to the RO's services array on Save. This is the bidirectional RO ↔ Quote link.
|
|
|
|
## Key Files
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `src/components/quoteGenerator/QuoteSummary.tsx` | Sync logic in `handleSave` (lines 146-194) and `ensureShareToken` (lines ~270-303) |
|
|
| `src/pages/QuoteGenerator.tsx` | Sets `repairOriginId` from `data.repairOrderId` on edit load (line ~524) and from `fromRO` URL param (line ~103) |
|
|
|
|
## Data Flow
|
|
|
|
### 1. New Quote from RO
|
|
|
|
- User clicks "Generate Quote" from RO → navigates to `/quote?fromRO=RO_ID&name=...`
|
|
- `fromRO` effect in QuoteGenerator.tsx loads the RO, sets `repairOriginId = ro.id`
|
|
- User adds/approves services, clicks Save
|
|
- `handleSave` in QuoteSummary.tsx creates the quote record with `repairOrderId: repairOriginId` (line 125)
|
|
- Sync block runs: filters `services.filter(s => s.approved)`, loads current RO services, merges (by service name — avoids duplicates), updates RO with `JSON.stringify(roServices)`
|
|
- Toast: `"Synced N approved service(s) to RO"`
|
|
|
|
### 2. Editing Existing Quote from RO
|
|
|
|
- Quote has `repairOrderId` set in PocketBase
|
|
- Edit-load effect restores `setRepairOriginId(data.repairOrderId)` (QuoteGenerator.tsx line ~524)
|
|
- User approves more services, clicks Save
|
|
- Sync block runs same as above
|
|
|
|
### 3. Share with Customer (ensureShareToken)
|
|
|
|
- **New quote**: Creates the quote record, syncs approved services to RO, then creates share token. Same merge logic as handleSave.
|
|
- **Editing existing quote** (editId set): The `if (!quoteId)` block is SKIPPED — no sync on share of existing quotes. The quote is read from PB, share token created/returned. **This is a known gap.**
|
|
|
|
## Critical Conditions
|
|
|
|
The sync block only runs when BOTH are true:
|
|
1. `repairOriginId` is set (non-null)
|
|
2. `services.filter(s => s.approved).length > 0`
|
|
|
|
## Why Sync Might Not Run
|
|
|
|
| Cause | Symptom |
|
|
|-------|---------|
|
|
| Quote's `repairOrderId` is null in PB | `repairOriginId` stays null; entire sync block skipped silently |
|
|
| No services approved | Sync block skipped; no toast shown (by design) |
|
|
| RO load fails (permissions/network) | Falls to catch block → error toast: `"Sync to RO failed: ..."` |
|
|
| Schema validation fails before sync | Error toast from Zod; sync never reached |
|
|
|
|
**Most common cause**: The quote was created before the `repairOrderId` field existed. Check the quote in PocketBase admin — if `repairOrderId` is empty, the quote has no RO link. Generate a new quote from the RO to get the field populated.
|
|
|
|
## PocketBase Fields
|
|
|
|
- `quotes.repairOrderId` — relation field linking quote to its originating RO. Set at quote creation (line 125 in handleSave).
|
|
- `repairOrders.services` — stored as `JSON.stringify(array)` (plain JSON text field, NOT a PocketBase relation).
|
|
|
|
## Sync Merge Logic
|
|
|
|
Services are merged by **name** (not ID):
|
|
- If RO already has a service with the same name → update it (preserving status if already set)
|
|
- If name not found → append new service
|
|
|
|
Each synced service gets: `id: 'qs-{quoteService.id}'`, `name`, `description`, `laborHours` (from price/rate), `laborRate`, `partsCost: 0`, `total: price`, `status: 'pending'`
|
|
|
|
## Testing the Sync
|
|
|
|
1. Create an RO owned by the current user
|
|
2. Generate a quote from it (`/quote?fromRO=RO_ID`)
|
|
3. Add and approve services
|
|
4. Click Save
|
|
5. Check the RO in PocketBase — `services` should contain the approved services
|
|
6. Verify via API: `GET /api/collections/repairOrders/records/RO_ID`
|
|
|
|
## Previous Investigation
|
|
|
|
- The sync code was verified working end-to-end (2026-07-08)
|
|
- All debug `[SPQ-DEBUG]` console.logs have been removed
|
|
- The sync runs in both `handleSave` and `ensureShareToken` (new-quote path only)
|
|
- RO `services` field is written as `JSON.stringify(roServices)` — always produces valid JSON arrays
|