# 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