137 lines
7.6 KiB
Markdown
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.
|