Files
hermes-config/skills/self-hosting/shop-pro-quote/references/v2-patterns.md
T
2026-07-12 10:17:17 -04:00

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:

  • 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:

// ✅ DO — early return
if (someCondition) {
  return <SpecialView />;
}
const mainContent = (
  <>...existing content untouched...</>
);

// ❌ AVOID — fragment wrapping
const mainContent = (
  <>
    {cond && (
      <SpecialView />
    )}
    {!cond && (
      <>...existing content...</>  // nesting hell
    )}
  </>
);