4.5 KiB
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:
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:
// 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:
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):
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:
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.