125 lines
4.4 KiB
Markdown
125 lines
4.4 KiB
Markdown
# SPQ PocketBase Field Mapping — Frontend ↔ DB
|
|
|
|
## Critical: Frontend Field Names ≠ PB Column Names
|
|
|
|
The `repairOrders` collection schema has different field names than what the TypeScript types use. Saving to the wrong name causes **silent data loss** — PB accepts unknown fields through the API but does NOT persist them.
|
|
|
|
### Frontend → PB Translation Table
|
|
|
|
| TypeScript (types.ts) | PB Column | PB Type | Translation |
|
|
|---|---|---|---|
|
|
| `estimatedDuration: number` (minutes) | `estimatedTime` | TEXT | `(estimatedDuration / 60).toFixed(1)` → stores hours string like `"1.5"` |
|
|
| `customerType: 'waiter' \| 'drop-off'` | N/A — no column | — | Store in `financial` JSON blob: `JSON.stringify({ ...existingFinancial, customerType })` |
|
|
| `writeupTime: string` (ISO) | `writeupTime` | TEXT | Direct match ✓ |
|
|
| `promisedTime: string` (ISO) | `promisedTime` | TEXT | Direct match ✓ |
|
|
|
|
### Normalize on Load
|
|
|
|
In `fetchOrders` normalization, convert PB fields back to frontend types:
|
|
|
|
```typescript
|
|
// PB returns estimatedTime as hours string → convert to minutes number
|
|
let estimatedDuration: number | undefined;
|
|
if (item.estimatedTime && item.estimatedTime !== '') {
|
|
const hours = parseFloat(item.estimatedTime);
|
|
if (!isNaN(hours)) estimatedDuration = Math.round(hours * 60);
|
|
}
|
|
|
|
// PB returns financial JSON blob → extract customerType
|
|
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 {}
|
|
}
|
|
|
|
return { ...item, services, estimatedDuration, customerType };
|
|
```
|
|
|
|
### Translate on Save
|
|
|
|
In `handleSave`, transform before sending to PB:
|
|
|
|
```typescript
|
|
const { estimatedDuration, customerType, ...rest } = data;
|
|
const payload = {
|
|
...rest,
|
|
userId,
|
|
services: JSON.parse(JSON.stringify(data.services || [])),
|
|
estimatedTime: estimatedDuration != null ? (estimatedDuration / 60).toFixed(1) : '',
|
|
};
|
|
|
|
// Store customerType in financial JSON
|
|
if (customerType) {
|
|
try {
|
|
const existing = typeof rest.financial === 'string'
|
|
? JSON.parse(rest.financial) : (rest.financial || {});
|
|
payload.financial = JSON.stringify({ ...existing, customerType });
|
|
} catch {
|
|
payload.financial = JSON.stringify({ customerType });
|
|
}
|
|
}
|
|
```
|
|
|
|
### For Add-Time / Inline Saves
|
|
|
|
When updating just the estimated duration inline (like the "+ Add Time" feature), save directly to `estimatedTime`:
|
|
|
|
```typescript
|
|
await pb.collection('repairOrders').update(id, {
|
|
estimatedTime: (newDuration / 60).toFixed(1),
|
|
});
|
|
```
|
|
|
|
### For Customer-Type Toggle Inline
|
|
|
|
When toggling waiter/drop-off inline, update the `financial` JSON blob:
|
|
|
|
```typescript
|
|
const ro = orders.find((o) => o.id === id);
|
|
let financial: Record<string, any> = {};
|
|
if (ro?.financial) {
|
|
try { financial = typeof ro.financial === 'string' ? JSON.parse(ro.financial) : ro.financial; } catch {}
|
|
}
|
|
financial.customerType = newType;
|
|
await pb.collection('repairOrders').update(id, {
|
|
financial: JSON.stringify(financial),
|
|
});
|
|
```
|
|
|
|
## RO Status Values (React App)
|
|
|
|
| TS Value | PB Value | Display | Badge |
|
|
|---|---|---|---|
|
|
| `active` | `active` | Active | Blue |
|
|
| `in_progress` | `in_progress` | In Progress | Yellow |
|
|
| `waiting_parts` | `waiting_parts` | Waiting Parts | Orange |
|
|
| `waiting_pickup` | `waiting_pickup` | Waiting Pick-up | Teal |
|
|
| `completed` | `completed` | Completed | Green |
|
|
| `delivered` | `delivered` | Delivered | Gray |
|
|
|
|
## Appointment to Repair Order (Check-In)
|
|
|
|
When converting an appointment into an RO, the `handleCheckInSubmit` maps fields like this:
|
|
|
|
| Appointment field | RO field |
|
|
|---|---|
|
|
| `customerName` | `customerName` |
|
|
| `customerPhone` | `customerPhone` |
|
|
| `customerEmail` | `customerEmail` |
|
|
| `vehicleInfo` | `vehicleInfo` |
|
|
| `advisorName` | `advisorName` |
|
|
| `notes` | `notes` (mapped from "customer concerns" input) |
|
|
| — | `vin` (prompted in check-in form) |
|
|
| — | `mileage` (prompted in check-in form) |
|
|
| — | `writeupTime` = `new Date().toISOString()` (auto-stamped) |
|
|
| — | `estimatedTime` = `(estimatedDuration / 60).toFixed(1)` (from hours+minutes input) |
|
|
| — | `financial` = `{ customerType }` (from radio toggle) |
|
|
| — | `status` = `'active'` (initial) |
|
|
| — | `roNumber` = auto-generated (e.g., `RO-20260629-0842`) |
|
|
|
|
After RO creation, appointment status changes to `checked_in`.
|