62 lines
2.6 KiB
Markdown
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.
|