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