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

137 lines
7.6 KiB
Markdown

# PocketBase Timestamp Compatibility
The `pocketbase.js` adapter's `Timestamp` class wraps dates for Firebase compatibility (`toDate()`, `toMillis()`, etc.).
## Missing Static Methods (Fixed)
Code in `appointments.js` calls `Timestamp.fromDate()` and `Timestamp.now()` — these are Firebase APIs that did NOT exist on the adapter's `Timestamp` class.
**Fix** — added to `pocketbase.js`:
```js
class Timestamp {
// ... existing methods ...
static fromDate(date) { return new Timestamp(date); }
static now() { return new Timestamp(new Date()); }
}
```
**Symptom when missing**: Edit appointment appears to work (no JS error in console for the `Timestamp` reference itself) but the `updateDoc` call receives malformed data and the save silently fails. Look for `TypeError: Timestamp.fromDate is not a function` in dev tools.
## convertToPB Does NOT Handle Timestamp Objects (CRITICAL)
The `convertToPB()` function checks `instanceof Date` to convert dates to ISO strings. But `Timestamp` is a plain class — NOT a `Date` subclass. So `Timestamp` objects fall through to the catch-all `JSON.stringify(value)` branch and get serialized as garbage:
```json
{"_date":"2026-06-14T14:30:00.000Z"}
```
This silently poisons PocketBase records. The fix is to ADD `Timestamp` handling to `convertToPB`:
```js
} else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Timestamp)) {
result[key] = JSON.stringify(value);
} else if (value instanceof Timestamp) {
result[key] = value.toISOString();
```
**Better approach**: Stop using `Timestamp` wrapper for saves entirely. Use plain `new Date()``convertToPB` handles `Date` natively with `value.toISOString()`. Only use `Timestamp` when READING from PocketBase (it's created by `convertFromPB`).
## `new Date(timestampObj)` Crashes Silently
Code like `new Date(appointment.appointmentDateTime)` produces `Invalid Date` when `appointmentDateTime` is a `Timestamp` object (because `Date()` constructor doesn't know about the internal `_date` property). This causes empty date/time fields in the edit modal.
**Fix**: Use the `Timestamp.toDate()` method:
```js
const date = appointmentDateTime?.toDate
? appointmentDateTime.toDate()
: new Date(appointmentDateTime);
```
**Locations fixed**: `appointments.js` lines 173 (date grouping) and 814 (modal populating).
## Round-Trip
- `convertToPB()`: `Date` instances → ISO strings; `Timestamp` instances → ISO strings (after fix above)
- `convertFromPB()`: ISO string fields → `Timestamp` objects (for app use)
`Timestamp` fields recognized by the converter: `writeupTime`, `createdAt`, `updatedAt`, `lastUpdated`, `appointmentDateTime`, `completionTime`, `lastActivity`, `timestamp`, `dateCreated`, `startDate`, `endDate`, `deadline`, `dueDateTime`, `promisedTime`.
## Silent Field Drop — PocketBase Schema Mismatch
When a field name is NOT in the PocketBase collection schema, the write **silently drops the field** — no error, no warning. The symptom surfaces on READ: the field is `undefined`, and any fallback (e.g., `let dueDateTime = new Date()`) produces wrong data.
**Example**: Tasks showed overdue with today's date regardless of the selected due date because `dueDateTime` was not in the tasks collection schema. PocketBase dropped it on write; the retrieval code fell back to `new Date()`.
**Fix checklist when adding a new field to any collection**:
1. Add the field to the PocketBase collection schema (via API PATCH or SQLite)
2. If it's a date/datetime field, add it to `TIMESTAMP_FIELDS` in `pocketbase.js`
3. Restart PocketBase container
See `references/pocketbase-schema-patch.md` for the SQLite direct-patch pattern when API auth is unavailable.
## Nested try/catch Gap in handleEditAppointment
The validation code in `handleEditAppointment()` was OUTSIDE any try/catch — any error (null element, `log()` throwing) would silently reject the async promise with no visible error to the user. Fix: wrap the entire function body in a single `try/catch` with an `alert()` fallback so errors are ALWAYS visible.
## Create RO from Appointment — Required Fields
The `promptRepairOrderDetails()` modal in `appointments.js` must include ALL fields the quote generator has. Current (fixed) set:
| Field | ID | Pre-fill source |
|---|---|---|
| Customer Name | `ro-customer-name` | `appointment.customerName` |
| Customer Phone | `ro-customer-phone` | `appointment.customerPhone` |
| Vehicle Info | `ro-vehicle-info` | `appointment.vehicleInfo` |
| Service Advisor | `ro-service-advisor` | `window.appSettings?.serviceAdvisor` |
| RO Number | `ro-number-input` | Auto (timestamp last 6 digits) |
| Mileage | `mileage-input` | `appointment.mileage` (if present) |
| VIN | `vin-input` | `appointment.vin` |
| Estimated Hours | `estimated-hours-input` | Default 2.0 |
| Service Type | `service-type` radio | Waiter/Drop Off |
Function signature: `promptRepairOrderDetails(appointment)` — takes the full appointment object, not just the VIN.
## Quote Auto-Conversion
In `saveQuote()` (`quote-tab-manager.js`): when every service on a quote has `customerDecision` set to `'approved'` or `'declined'` (no pending), set `quoteData.status = 'converted'` automatically. This drops the quote from the dashboard Daily Briefing.
## Daily Briefing Dollar Breakdown
In `generateDailyBriefing()` (`dashboard.js`): iterate active quotes and sum service prices by `customerDecision` to produce `approvedDollars` and `declinedDollars`. Feed these to the LLM data string:
```
Total quote value: $X — Approved: $Y, Declined: $Z
```
The system prompt instructs the LLM to mention the breakdown when relevant.
## Auth Redirect Loop — Login ↔ Dashboard
When two static HTML pages use mutual `onAuthStateChanged` redirects (login → dashboard, dashboard → login), a stale PocketBase auth token in localStorage causes an infinite bounce loop.
**How it happens**: PocketBase's `authStore.onChange` fires `callback(null)` after the SDK's first API call returns 401 (expired/invalidated token). But the old token persists in localStorage, so the next page load reads it again — `currentUser` returns a user, fires `callback(user)`, redirects back, and the cycle repeats.
**Symptoms**: Browser flickers between login and dashboard pages, constantly refreshing.
**The fix — merge login into root**: Make the login page the site root (`index.html`) and move the dashboard to a separate page (`dashboard.html`). This eliminates the mutual redirect:
| Before | After |
|--------|-------|
| `index.html` = dashboard | `dashboard.html` = dashboard |
| `login.html` = login page | `index.html` = login page |
| Visit `/` → dashboard → redirect to login | Visit `/` → login page directly (no redirect) |
**Files to update (13 total across SPQ)**:
- Auth redirects: `'login.html'``'index.html'` (7 JS files)
- Login success: `'index.html'``'dashboard.html'` (login.js + index.html inline scripts)
- Nav links in HTML: `href="index.html"``href="dashboard.html"` (4 HTML files)
- Keyboard shortcut: dashboard.js case 'q' → `'dashboard.html'`
- Quote links: customers.js `'index.html?quoteId='``'dashboard.html?quoteId='`
**Verification**: Visit root with no auth → login form shows directly. Visit root with valid token → one clean redirect to dashboard.html. Visit dashboard.html with no auth → one clean redirect to index.html.
## PocketBase SDK Auto-Cancellation
See `references/pocketbase-auto-cancellation.md` for the full guide. PocketBase SDK auto-cancels duplicate `create()`/`update()` calls by default. Fix: pass `{ requestKey: null }` on all mutating calls and add double-submission guards to form handlers.