# Customer Decision System (v2 Quote Generator)

## Fields

Each `QuoteService` has two related fields:

| Field | Type | Purpose |
|---|---|---|
| `approved` | boolean | Visual toggle state for the green checkbox |
| `customerDecision` | `'approved'` / `'pending'` / `'declined'` | Actual decision state |

## How They Stay in Sync

- **Approve button** (green ✓): sets `approved: true, customerDecision: 'approved'`
- **Decline button** (red ✕): sets `approved: false, customerDecision: 'declined'`
- **Pending button**: sets `approved: false, customerDecision: 'pending'`
- **toggleApproved** (checkbox click): toggles `approved`, sets `customerDecision` to `'pending'` (when unapproving) or `'approved'` (when approving)
- **toggleDeclined** (X checkbox click): sets `approved: false`, toggles `customerDecision` between `'declined'` and `'pending'`

Both are set together in every action path — they never drift.

## Service Grouping in ServicesTable

The `ServicesTable` component groups services by `customerDecision` into three sections:
- **Approved** (green header): `customerDecision === 'approved'`
- **Pending** (gray header): `customerDecision === 'pending'`
- **Declined** (red header): `customerDecision === 'declined'`

An "Approve All" button sits above the groups when non-approved services exist.

## Totals Calculation: `billableServices()`

Defined in `src/lib/totals.ts`. This is the single shared heuristic used by the UI Quote Summary, the store's `subtotal()`/`approvedSubtotal()`, and the PDF generator.

```typescript
export function billableServices(services: { customerDecision: string }[]) {
  const nonDeclined = services.filter(s => s.customerDecision !== 'declined');
  const hasApproved = nonDeclined.some(s => s.customerDecision === 'approved');
  const hasPending = nonDeclined.some(s => s.customerDecision === 'pending');
  return (hasApproved && hasPending)
    ? nonDeclined.filter(s => s.customerDecision === 'approved')  // mixed → only approved
    : nonDeclined;  // unanimous → all
}
```

**Rules:**
- All services approved → count all (full total)
- All services pending → count all (full total)
- Mixed approved + pending → count only approved (partial total)
- Declined services → always excluded

This makes totals dynamic: they adjust as the advisor approves/declines individual services. The full total only appears when the decision state is unanimous.

## PDF Generation

In `src/lib/pdf.ts`, the `billableServices()` result is pre-computed as a `Set` of service IDs:
```typescript
const billableIds = new Set(billableServices(services).map(s => s.id));
```
Used in two places within the generator:
- Service total accumulation: `if (billableIds.has(svc.id)) servicesTotal += price`
- Shop charge base: `(billableIds.has(s.id) && s.applyShopCharge)`

## Store Actions

- `toggleApproved(id)` — toggles approve checkbox state
- `toggleDeclined(id)` — toggles decline (X) checkbox state
- `updateService(id, updates)` — partial update (used by Approve/Decline/Pending buttons)

## Dual Checkbox UI

ServiceRow renders two checkboxes side by side:

| Checkbox | Icon | Active color | Calls |
|---|---|---|---|
| Approve | ✓ (always visible, gray when off) | Green | `onToggleApproved` |
| Decline | ✕ (always visible, gray when off) | Red | `onToggleDeclined` |

The icons are always rendered — grayed-out (`text-gray-300`) when inactive, white on colored background when active. Clicking either toggles its state.

## Recent Quotes Status Display

The Recent Quotes panel (`src/pages/QuoteGenerator.tsx`) derives a display status from service decisions, NOT from the raw PocketBase `status` field. When all services are decided (no pending), the status is computed: all-approved → "Approved", all-declined → "Declined". The PocketBase `status` field only tracks lifecycle ('draft' on save, 'sent' on share, 'approved'/'declined' on customer submission via QuoteApprove) — it does NOT reflect service-level decisions made in the QuoteGenerator.

See `references/recent-quotes-status-derivation.md` for the full fix and rationale.
