4.9 KiB
SPQ v2 — Key Patterns & Pitfalls
Project at /mnt/seagate8tb/Websites/ShopProQuote.backup-20260626-2058/spq-v2/.
Vite + React + TypeScript SPA, PocketBase backend.
Dev Proxy — DeepSeek AI
The app calls DeepSeek API at /deepseek/v1/chat/completions (OpenAI-compatible). The API key lives in nginx (/etc/nginx/sites-enabled/shopproquote), not in the Vite project.
Vite dev proxy MUST route through localhost nginx, not direct to api.deepseek.com or Ollama:
'/deepseek': {
target: 'https://localhost',
changeOrigin: true,
secure: false, // localhost self-signed cert
},
The old approach (pointing to Ollama :11434) fails because Ollama has no deepseek-v4-flash.
Quote Service Defaults
Services added to a quote default to Pending status (yellow), NOT Approved:
addService({ ...service, selected: true, approved: false, customerDecision: 'pending', priority: undefined, applyShopCharge: true });
This applies to all three code paths in QuoteGenerator.tsx:
handleAdd(catalog search picker, ~line 147)- RO import → quote pre-fill (~lines 777-778)
handleAddSuggestion(AI suggestions, ~lines 959-960)
The per-service "Shop charge" checkbox in the expanded row lets users override. The QuoteSummary only shows the shop charge line when shopCharge > 0.
AI Write — Tech Notes
handleAiWrite in QuoteGenerator.tsx passes technicianNotes: svc.technicianNotes to aiWriteExplanation. The AI prompt in ai.ts already reads technicianNotes and includes them as Technician's internal notes: "..." in the context. Always include this field when calling aiWriteExplanation.
RO Number & Top-Level Fields
handleSave writes customer info BOTH as nested customerInfo JSON AND as top-level fields (customerName, vehicleInfo, roNumber). The Dashboard queries top-level fields. The edit-load logic merges both sources:
const rawCustomerInfo = data.customerInfo
? { ...data, ...(typeof data.customerInfo === 'string' ? JSON.parse(data.customerInfo) : data.customerInfo) }
: data;
PDF → PNG Export
downloadPdfAsImages(doc | Blob, customerName) in src/lib/pdf-to-images.ts converts every PDF page to a PNG and triggers downloads. Accepts either a jsPDF instance or a Blob (from generateQuotePDF()). Uses pdfjs-dist with worker configured in main.tsx via Vite static-asset import.
Repair Orders Dashboard
See references/v2-repair-orders-dashboard.md for the complete time-management system:
- 6-status model with live countdown timers
- Multi-tier sorting (waiting parts → waiters → drop-off → due time)
- Urgency styling (warning ≤30min, past due = red row)
- Interactive Waiter/Drop-Off toggle with confirmation
- "+ Add Time" feature via prompt
- Hours + Minutes duration input in create/edit modal
Dashboard Dropdown
The Dashboard "Recent Quotes" section uses a custom dropdown with search instead of a native <select>. State: jumpOpen + jumpQuery. Click-outside closes via useRef + mousedown listener. Search filters by customer name, RO#, and vehicle. Quotes sorted by -id (PocketBase time-sortable).
ROForm — Shared Edit Form Component
src/components/ROForm.tsx — reusable form component extracted from the ROModal (~600 lines → standalone). Used by both:
- ROModal (create/edit popup) — wraps ROForm in a modal overlay with header
- RODetailsPanel (inline details tab) — renders ROForm directly in-page
Props: editRO (null for create), existingRoNumbers, settings, onSave, onCancel, submitLabel.
Contains all form state (customer info, vehicle, advisor, status, warranty, services CRUD, discount, notes, totals preview). The ServiceRow sub-component also lives here — do NOT duplicate it.
RO Details Tab
Clicking an RO row navigates to the "Details" tab (not inline expand). Architecture:
TabIdincludes'details'selectedROIdstate +selectedROmemoized lookuphandleSelectRO(id)setsselectedROId+ switches to'details'tab- Early-return guard renders
RODetailsPanelwhenactiveTab === 'details' && selectedRO handleInlineUpdate(roId, data)handles PB update + refresh (separate from modal'shandleSavewhich uses module-leveleditROstate)- Back button in RODetailsPanel resets to
'active'tab
JSX Refactoring — Fragment Wrapping Pitfall
When wrapping existing JSX in a conditional fragment, prefer early return over inline {cond && (<>...</>)} wrapping. The inline approach causes indentation/closure mismatches when the wrapped content has its own fragment nesting. Pattern:
// ✅ DO — early return
if (someCondition) {
return <SpecialView />;
}
const mainContent = (
<>...existing content untouched...</>
);
// ❌ AVOID — fragment wrapping
const mainContent = (
<>
{cond && (
<SpecialView />
)}
{!cond && (
<>...existing content...</> // nesting hell
)}
</>
);