# 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.
