# Customer Decision System (v2 Quote Generator) ## Fields Each `QuoteService` has two related fields: | Field | Type | Purpose | |---|---|---| | `approved` | boolean | Visual toggle state for the green checkbox | | `customerDecision` | `'approved'` / `'pending'` / `'declined'` | Actual decision state | ## How They Stay in Sync - **Approve button** (green ✓): sets `approved: true, customerDecision: 'approved'` - **Decline button** (red ✕): sets `approved: false, customerDecision: 'declined'` - **Pending button**: sets `approved: false, customerDecision: 'pending'` - **toggleApproved** (checkbox click): toggles `approved`, sets `customerDecision` to `'pending'` (when unapproving) or `'approved'` (when approving) - **toggleDeclined** (X checkbox click): sets `approved: false`, toggles `customerDecision` between `'declined'` and `'pending'` Both are set together in every action path — they never drift. ## Service Grouping in ServicesTable The `ServicesTable` component groups services by `customerDecision` into three sections: - **Approved** (green header): `customerDecision === 'approved'` - **Pending** (gray header): `customerDecision === 'pending'` - **Declined** (red header): `customerDecision === 'declined'` An "Approve All" button sits above the groups when non-approved services exist. ## Totals Calculation: `billableServices()` Defined in `src/lib/totals.ts`. This is the single shared heuristic used by the UI Quote Summary, the store's `subtotal()`/`approvedSubtotal()`, and the PDF generator. ```typescript export function billableServices(services: { customerDecision: string }[]) { const nonDeclined = services.filter(s => s.customerDecision !== 'declined'); const hasApproved = nonDeclined.some(s => s.customerDecision === 'approved'); const hasPending = nonDeclined.some(s => s.customerDecision === 'pending'); return (hasApproved && hasPending) ? nonDeclined.filter(s => s.customerDecision === 'approved') // mixed → only approved : nonDeclined; // unanimous → all } ``` **Rules:** - All services approved → count all (full total) - All services pending → count all (full total) - Mixed approved + pending → count only approved (partial total) - Declined services → always excluded This makes totals dynamic: they adjust as the advisor approves/declines individual services. The full total only appears when the decision state is unanimous. ## PDF Generation In `src/lib/pdf.ts`, the `billableServices()` result is pre-computed as a `Set` of service IDs: ```typescript const billableIds = new Set(billableServices(services).map(s => s.id)); ``` Used in two places within the generator: - Service total accumulation: `if (billableIds.has(svc.id)) servicesTotal += price` - Shop charge base: `(billableIds.has(s.id) && s.applyShopCharge)` ## Store Actions - `toggleApproved(id)` — toggles approve checkbox state - `toggleDeclined(id)` — toggles decline (X) checkbox state - `updateService(id, updates)` — partial update (used by Approve/Decline/Pending buttons) ## Dual Checkbox UI ServiceRow renders two checkboxes side by side: | Checkbox | Icon | Active color | Calls | |---|---|---|---| | Approve | ✓ (always visible, gray when off) | Green | `onToggleApproved` | | Decline | ✕ (always visible, gray when off) | Red | `onToggleDeclined` | The icons are always rendered — grayed-out (`text-gray-300`) when inactive, white on colored background when active. Clicking either toggles its state. ## Recent Quotes Status Display The Recent Quotes panel (`src/pages/QuoteGenerator.tsx`) derives a display status from service decisions, NOT from the raw PocketBase `status` field. When all services are decided (no pending), the status is computed: all-approved → "Approved", all-declined → "Declined". The PocketBase `status` field only tracks lifecycle ('draft' on save, 'sent' on share, 'approved'/'declined' on customer submission via QuoteApprove) — it does NOT reflect service-level decisions made in the QuoteGenerator. See `references/recent-quotes-status-derivation.md` for the full fix and rationale.