initial commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# 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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user