# 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
