Files
2026-07-12 10:17:17 -04:00

62 lines
2.6 KiB
Markdown

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