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

127 lines
5.1 KiB
Markdown

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