127 lines
5.1 KiB
Markdown
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)
|