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

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

// 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:

  • 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 configwaiting_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)