# QuoteService dual-field trap: `approved` vs `customerDecision` The `QuoteService` type has TWO fields that represent approval state: ```typescript export interface QuoteService extends Service { approved: boolean; customerDecision: 'approved' | 'declined' | 'pending'; // ... } ``` These MUST stay in sync. When they diverge, the UI summary and the PDF produce different totals. ## The invariant Every code path that sets one MUST set the other consistently: | State | `approved` | `customerDecision` | |-------|-----------|-------------------| | Customer approved | `true` | `'approved'` | | Customer declined | `false` | `'declined'` | | Pending decision | `false` | `'pending'` | ## Where each filter is used ### ALL totals (src/lib/totals.ts, src/store/quote.ts, src/lib/pdf.ts) All three now use `customerDecision !== 'declined'` — both approved AND pending services count. Only declined services are excluded from subtotal, discount base, shop charge, and tax. Both the on-screen Quote Summary sidebar AND the generated PDF/Print use the same filter. ### UI grouping (src/pages/QuoteGenerator.tsx ServicesTable) Three sections by `customerDecision`: Approved (green), Pending (gray), Declined (red). ## Three-state toggle system Two store actions handle the three states, both keeping fields in sync: **`toggleApproved(id)`** — toggles between approved ↔ pending: - If approved → `{ approved: false, customerDecision: 'pending' }` - If not approved → `{ approved: true, customerDecision: 'approved' }` **`toggleDeclined(id)`** — toggles between declined ↔ pending: - If declined → `{ approved: false, customerDecision: 'pending' }` - If not declined → `{ approved: false, customerDecision: 'declined' }` Both are wired to adjacent checkboxes in the ServiceRow: green ✓ (toggleApproved) and red ✕ (toggleDeclined). The expanded Customer Decision buttons (Approve / Decline / Pending) are the alternative UI path — they use `onUpdate()` to set both fields atomically. ## Pitfall: all three must use the same filter All three places that compute totals must use the SAME filter — `customerDecision !== 'declined'`: - **`computeQuoteTotals()`** in `src/lib/totals.ts` - **Store `subtotal()` / `approvedSubtotal()`** in `src/store/quote.ts` - **PDF `servicesTotal` + shop charge** in `src/lib/pdf.ts` If any one of these diverges, the on-screen summary and the generated PDF will show different amounts, or the PDF will show $0.00 when all services are pending. **Prevention:** when changing the filter rule, grep all three files and change them together.