# spq-v2 React Frontend

The v2 rewrite lives at `/mnt/seagate8tb/Websites/ShopProQuote.backup-20260626-2058/spq-v2/`. Served on port 443 via nginx from its `dist/` directory.

## Build

Use `node ./node_modules/.bin/vite build` — NOT `npm run build`. The latter runs `tsc -b` first, which fails on pre-existing type errors in other files (Appointments, Customers, FinancialDashboard, etc.). Vite's esbuild handles TS more leniently and builds fine directly.

`npm run dev` starts the Vite dev server (port 5173) for live development.

## PocketBase Field Normalization (Critical)

The React frontend field names often differ from the PB collection column names. **This is the #1 source of "save doesn't persist" bugs.**

### How to diagnose

Query the actual PB SQLite schema:
```bash
sqlite3 /home/ray/docker/pocketbase/pb_data/data.db "PRAGMA table_info(repairOrders)"
```

The TypeScript type definitions (`src/types.ts`) may be aspirational — the DB is the source of truth.

### The normalization pattern

When field names differ, transform in **both directions**:

- **On load** (in `fetchOrders` normalization): map PB column → frontend field
- **On save** (in `handleSave`, `handleAddTime`): map frontend field → PB column

### Known mismatches

| Frontend field | Frontend type | PB column | PB type |
|---|---|---|---|
| `estimatedDuration` | number (minutes) | `estimatedTime` | TEXT (hours decimal string, e.g. "1.5") |
| `customerType` | 'waiter' \| 'drop-off' | `financial.customerType` | stored in `financial` JSON blob |

### Example: estimatedTime ↔ estimatedDuration

```typescript
// On load
estimatedDuration = Math.round(parseFloat(item.estimatedTime) * 60);

// On save
estimatedTime: (estimatedDuration / 60).toFixed(1);
```

### Custom fields in financial JSON

Fields without a dedicated PB column (like `customerType`) must be stored/loaded from the `financial` JSON column:

```typescript
// On load — extract from financial blob
let customerType: CustomerType | undefined;
if (item.financial) {
  try {
    const fin = typeof item.financial === 'string' ? JSON.parse(item.financial) : item.financial;
    if (fin && (fin.customerType === 'waiter' || fin.customerType === 'drop-off')) {
      customerType = fin.customerType;
    }
  } catch {}
}

// On save — store in financial blob
let financial: Record<string, any> = {};
if (ro?.financial) {
  try { financial = typeof ro.financial === 'string' ? JSON.parse(ro.financial) : ro.financial; } catch {}
}
financial.customerType = newType;
pb.collection('repairOrders').update(id, { financial: JSON.stringify(financial) });
```

## Repair Orders Dashboard

The active Repair Orders page (`src/pages/RepairOrders.tsx`) has several key components:

### ROForm — Shared Edit Component

`src/components/ROForm.tsx` — reusable form extracted from ROModal. Used by:
- **ROModal** — wraps ROForm in a modal overlay for create/edit popups
- **RODetailsPanel** — renders ROForm inline in the Details tab

Props: `editRO` (null = create mode), `existingRoNumbers`, `settings`, `onSave`, `onCancel`, `submitLabel`. Contains all form state including services CRUD (add/remove/edit service rows), totals preview, discount, notes. The `ServiceRow` sub-component lives here — do not duplicate.

### RO Details Tab

Clicking an RO row navigates to a "Details" tab:
- `TabId` includes `'details'` alongside `'active' | 'all' | 'completed'`
- `selectedROId` state + `selectedRO` memoized lookup from `orders`
- `handleSelectRO(id)` sets `selectedROId` + switches tab to `'details'`
- Early-return guard when `activeTab === 'details'` renders `RODetailsPanel`
- `handleInlineUpdate(roId, data)` updates PB directly (separate from modal's `handleSave`)

### Time helpers

- `calcDueTime(ro)` — computes due timestamp from `writeupTime` (falls back to `ro.created`) + `estimatedDuration` (defaults to 60min)
- `formatTimeRemaining(ro, now)` — returns "1h 15m", "45m", "-10m" (returns "—" for completed/delivered)
- `getUrgencyClass(ro, now)` — returns '' | 'warning' (≤30min remaining) | 'urgent' (past due)
- `formatDueDateTime(ro)` — formatted date/time string of the computed due time
- `formatDateTime(iso)` — "Jun 29, 3:45 PM" format

### UI components

- **CustomerTypeBadge** — clickable Waiter/Drop-Off badge with `window.confirm()` toggle
- **TimeRemainingCell** — live countdown display with urgency coloring + alert icon
- **StatusBadge** / **Status config** — `waiting_pickup` uses teal colors

### Multi-tier sort (`sortOrders(orders)`)

1. Waiting For Parts → pinned to bottom, sorted by due time ascending
2. Waiters → above Drop-Offs
3. Within each group, due time ascending (most urgent first)

### Urgency styling

- **Warning** (≤30min to due): amber text on countdown, no row highlight
- **Urgent** (past due): red row background (`bg-red-50`), red text, alert triangle icon
- **Completed/delivered**: no styling, countdown shows "—"

### Add Time

`handleAddTime(id, extraMinutes)` — triggered from a "+ Add Time" button in the expanded detail action bar. Shows a prompt accepting flexible formats:
- `"30m"` → 30 minutes
- `"1h"` → 60 minutes
- `"1h 30m"` → 90 minutes
- `"90"` → 90 minutes (plain number = minutes)
