2.6 KiB
QuoteService dual-field trap: approved vs customerDecision
The QuoteService type has TWO fields that represent approval state:
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()insrc/lib/totals.ts- Store
subtotal()/approvedSubtotal()insrc/store/quote.ts - PDF
servicesTotal+ shop charge insrc/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.