# 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
    )}
  </>
);
```
