# 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 = {}; 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)