97 lines
4.5 KiB
Markdown
97 lines
4.5 KiB
Markdown
# ShopProQuote Critical Pitfalls
|
|
|
|
## Redirect loop: stale PocketBase auth token
|
|
|
|
**Symptom:** Browser enters an infinite redirect loop between `index.html` ↔ `dashboard.html` (or `login.html` if browser cache has old code).
|
|
|
|
**Root cause:** `index.html` line 131 auto-redirects to `dashboard.html` based on a truthy check of `localStorage.getItem('pocketbase_auth')`. When the PocketBase token is stale (expired/invalid), the PocketBase SDK's `onAuthStateChanged` fires with `null`, and the JS redirects back to `index.html`. The stale token is NOT cleared from localStorage before redirecting, so `index.html` sees it again → infinite loop.
|
|
|
|
**Fix:** In every auth-failure handler (`onAuthStateChanged` else block), always clear before redirecting:
|
|
```javascript
|
|
localStorage.removeItem('pocketbase_auth');
|
|
window.location.href = 'index.html';
|
|
```
|
|
Affected files: `dashboard.js`, `repair-orders.js`, `appointments.js`, `shared/header-functionality.js`.
|
|
|
|
Also check: `index.html` line 131 only does a truthy check. Never add logic that redirects based solely on localStorage presence without verifying token validity.
|
|
|
|
## PocketBase SDK auto-cancellation
|
|
|
|
**Symptom:** "auto-cancelled" error or "request was autocancelled" when creating/updating records, especially on double-click.
|
|
|
|
**Root cause:** PocketBase JS SDK auto-cancels requests that share the same `requestKey`. By default, all `create()` and `update()` calls to the same collection share a generated key based on method + URL. A rapid second call cancels the first.
|
|
|
|
**Fix:** Always pass `{ requestKey: null }` to disable auto-cancellation in `pocketbase.js`:
|
|
```javascript
|
|
// addDoc
|
|
pb.collection(collName).create(pbData, { requestKey: null });
|
|
|
|
// updateDoc
|
|
pb.collection(collName).update(docId, pbData, { requestKey: null });
|
|
|
|
// setDoc (both paths)
|
|
pb.collection(collName).update(records[0].id, pbData, { requestKey: null });
|
|
pb.collection(collName).create(pbData, { requestKey: null });
|
|
```
|
|
|
|
## Double-submission guard
|
|
|
|
**Symptom:** Form creates duplicate records when user double-clicks submit button.
|
|
|
|
**Fix:** Guard all create/update form handlers against re-entry:
|
|
```javascript
|
|
function handleSubmit() {
|
|
const submitBtn = document.getElementById('submit-btn');
|
|
if (submitBtn && submitBtn.disabled) return; // guard
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = 'Creating...';
|
|
}
|
|
try {
|
|
// ... do work ...
|
|
} finally {
|
|
// Always re-enable — success and error paths both need this
|
|
if (submitBtn) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = 'Create Repair Order';
|
|
}
|
|
}
|
|
}
|
|
```
|
|
Primary file: `repair-orders.js` `handleCreateROSubmit()`.
|
|
|
|
## scoping bug (ReferenceError silently crashes handler)
|
|
|
|
**Symptom:** Button click does nothing — no error, no notification, no action.
|
|
|
|
**Common cause:** Variable declared with `const`/`let` inside a `try` block, then referenced outside the block. JavaScript throws a silent ReferenceError that crashes async handlers with no visible feedback.
|
|
|
|
**Example (broken):**
|
|
```javascript
|
|
try {
|
|
const roData = getROData(roId);
|
|
// ...
|
|
} catch (e) { /* ... */ }
|
|
// BUG: roData is block-scoped to try, ReferenceError here
|
|
if (roData) { /* ... */ }
|
|
```
|
|
|
|
**Fix:** Declare the variable before the try block:
|
|
```javascript
|
|
const roData = repairOrders.find(ro => ro.id === roId);
|
|
try {
|
|
// use roData...
|
|
} catch (e) { /* ... */ }
|
|
// roData still accessible here
|
|
```
|
|
|
|
## Quote service approved vs customerDecision mismatch (totals show $0 in PDF)
|
|
|
|
**Symptom:** On-screen quote summary shows correct totals, but generated PDF or Print view shows $0.00 for all totals even though services are present.
|
|
|
|
**Root cause:** The `QuoteService` type has two fields (`approved: boolean` and `customerDecision: string`) that must stay in sync. When they diverge — e.g. a service has `approved: true` but `customerDecision: 'pending'` — the UI total (which used `s.approved`) counts it but the PDF (which uses `svc.customerDecision === 'approved'`) excludes it. If all services are in this broken state, the PDF shows $0.
|
|
|
|
**Fix:** All code paths that compute totals must use `customerDecision === 'approved'` as the single source of truth. See `references/quote-service-dual-fields.md` for the invariant table and the three-state toggle system.
|
|
|
|
**Verification:** Check that `computeQuoteTotals` (src/lib/totals.ts), store `subtotal()`/`total()`, and `generateQuotePDF` (src/lib/pdf.ts) all filter by `customerDecision === 'approved'`. The `approved` boolean should only drive the checkbox UI, never business logic.
|