5.1 KiB
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:
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
fetchOrdersnormalization): 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
// 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:
// 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:
TabIdincludes'details'alongside'active' | 'all' | 'completed'selectedROIdstate +selectedROmemoized lookup fromordershandleSelectRO(id)setsselectedROId+ switches tab to'details'- Early-return guard when
activeTab === 'details'rendersRODetailsPanel handleInlineUpdate(roId, data)updates PB directly (separate from modal'shandleSave)
Time helpers
calcDueTime(ro)— computes due timestamp fromwriteupTime(falls back toro.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 timeformatDateTime(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_pickupuses teal colors
Multi-tier sort (sortOrders(orders))
- Waiting For Parts → pinned to bottom, sorted by due time ascending
- Waiters → above Drop-Offs
- 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)