Files
2026-07-12 10:17:17 -04:00

114 lines
4.9 KiB
Markdown

# 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:
```typescript
'/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:
```typescript
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:
```typescript
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:
- `TabId` includes `'details'`
- `selectedROId` state + `selectedRO` memoized lookup
- `handleSelectRO(id)` sets `selectedROId` + switches to `'details'` tab
- Early-return guard renders `RODetailsPanel` when `activeTab === 'details' && selectedRO`
- `handleInlineUpdate(roId, data)` handles PB update + refresh (separate from modal's `handleSave` which uses module-level `editRO` state)
- 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:
```tsx
// ✅ DO — early return
if (someCondition) {
return <SpecialView />;
}
const mainContent = (
<>...existing content untouched...</>
);
// ❌ AVOID — fragment wrapping
const mainContent = (
<>
{cond && (
<SpecialView />
)}
{!cond && (
<>...existing content...</> // nesting hell
)}
</>
);
```