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

7.0 KiB

RODetailModal Architecture

Overview

src/pages/RODetailModal.tsx (~1600 lines) — the full-featured modal viewer for a single Repair Order. Supports both modal (overlay via navigate(-1)) and standalone page rendering. Contains 5 tabs: Details, Services, Financial, History, and Timeline.

Tabs

Tab Key Content
details Status management, info bar, services summary, promised-time override, clock, void, satisfaction rating
services Catalog search + add, manual service rows, new-service creation, totals
financial CP/Warranty Total/Cost inputs, Shop Charge input, live Gross Profit display. Writes to financial blob keys v2 already reads. See references/financial-summary-modal.md.
history VIN-based vehicle history (past ROs + quotes for the same VIN)
timeline Audit timeline from roEvents.ts + Financial Audit Trail from roFinancialAudit collection

Component Structure

RODetailModal
├── Modal wrapper (fixed overlay) or standalone full page
├── Tab bar (Details, Services, Financial, History, Timeline)
├── Tab content (one at a time via activeTab state)
│   ├── detailsContent
│   ├── servicesContent
│   ├── financialContent
│   ├── historyContent
│   └── timelineContent
└── AddTimeModal

Data-Fetching Patterns

Core RO Fetch

  • fetchRO() — called on mount, fetches the RO from PocketBase, normalizes services JSON
  • fetchCatalogServices() — fetches all catalog services for the Services tab

Tab-Triggered Fetches

Each tab that needs its own data follows this pattern:

const loadX = useCallback(async () => {
  if (!ro) return;
  setXLoading(true);
  try {
    const records = await pb.collection('x').getFullList({ filter: ..., sort: ... });
    setXData(records.map(...));
  } catch { setXData([]); }
  finally { setXLoading(false); }
}, [ro]);

useEffect(() => {
  if (activeTab === 'tabname') loadX();
}, [activeTab, loadX]);

Examples:

  • History tab: loadHistory() queries repairOrders and quotes by VIN, triggered when activeTab === 'history'
  • Financial Audit Trail: loadFinancialAudit() queries roFinancialAudit by roId, triggered when activeTab === 'timeline'

Financial Tab (implemented)

See references/financial-summary-modal.md for full design rationale and v1 history.

State

  • finCpTotal, finCpCost, finWarrTotal, finWarrCost, finShopCharge — string inputs
  • finSaving, finSaved, finSeeded — booleans for save state and pre-seed gate

Pre-seed

On first opening the Financial tab (activeTab === 'financial' and !finSeeded), inputs are pre-seeded from the RO's existing financial blob, falling back to computeROTotals() values (CP Total falls back to totals.total, Shop Charge falls back to totals.shopCharge). The finSeeded flag prevents reseeding on subsequent tab switches. Ray confirmed pre-seed is desired.

Live computed values

  • cpNetTotal = max(0, CP Total - Warr Total)
  • cpNetCost = max(0, CP Cost - Warr Cost)
  • cpGrossProfit = cpNetTotal - cpNetCost - Shop Charge
  • warrGrossProfit = Warr Total - Warr Cost
  • overallGrossProfit = cpGrossProfit + warrGrossProfit

These match customerPayGpOf() and warrantyGpOf() in FinancialDashboard.tsx, so dashboard totals pick up the entered values with zero dashboard-side changes.

Save

handleSaveFinancial() preserves existing blob keys (e.g. customerType), writes grossTotal/grossCost/warrTotal/warrCost/shopCharge/cpProfit/whProfit/grossProfit/totalAmount/totalCost/completedAt to the financial JSON blob, appends a 'financial' ROEvent for the timeline, and relies on the PB M25 hook for audit-trail creation.

Layout

  • Sticky save bar at top (Save Financial button) — per the modal standard, this is the only save button
  • Helper text explaining the deduction rules
  • Customer Pay card (green): CP Total input, CP Cost input, CP Net Total display, CP Net Cost display, CP Gross Profit display
  • Warranty card (blue): Warranty Total input, Warranty Cost input, Warranty GP display
  • Shop Charge card (amber): Shop Charge input, CP GP after Shop Charge display
  • Gross Profit summary bar: Overall Gross Profit (large, color-coded green/red), CP GP + Warranty GP breakdown

Timeline Tab

Audit Timeline (existing)

  • Reads events from readEvents(ro) (stored in the RO's financial JSON blob)
  • Renders TimelineEntry components for each event type (created, status, clock_start, financial, etc.)

Financial Audit Trail (added Phase 4.3)

  • Queries roFinancialAudit PocketBase collection filtered by current RO roId, sorted newest-first (-at)
  • Each entry shows: field name (camelCase → Title Case via formatFieldName), from → to JSON values in monospace, "by" attribution, truncated hash with full hash on title hover
  • Only renders when entries exist (hidden otherwise)
  • Shows Loader2 spinner while loading

Adding a New Data Section to a Tab

The pattern:

  1. Add state: const [data, setData] = useState<Type[]>([]) + const [loading, setLoading] = useState(false)
  2. Add useCallback load function querying PocketBase
  3. Add useEffect hooked to activeTab and ro
  4. Add conditional render in the tab's content JSX (loading spinner + data display + empty state)
  5. Reuse existing imports (Loader2, formatDateTime, etc.)

Types

  • ROFinancialAuditEntry in src/types.ts:
    interface ROFinancialAuditEntry {
      id: string; roId: string; field: string;
      from: unknown; to: unknown; by: string; at: string;
      prevHash: string; hash: string;
    }
    

Associated Files

  • src/pages/RODetailModal.tsx — main component
  • src/components/ROForm.tsx — shared create/edit form (now includes catalog search; see references/roform-catalog-search.md)
  • src/types.ts — type definitions (ROFinancialAuditEntry, ROEvent, RepairOrder, etc.)
  • src/lib/roEvents.ts — event reading/writing for timeline (readEvents, appendROEvent, clock*, void*)
  • src/lib/format.ts — formatCurrency, formatDateTime, formatElapsed
  • src/lib/totals.ts — computeROTotals, roServiceTotal
  • src/lib/technicianData.ts — parseRepairOrderServices, syncAssignmentsForROServices
  • src/store/settings.ts — useSettings hook for shop charge/tax rates

Maintenance Reminders (Phase 4.6)

The Details tab header includes a "Set Reminder" button (BellRing icon) that opens an inline form to create maintenance reminders. See references/reminders.md for full details on the reminders feature.

Quick Reference

  • Button location: Header button group, after Generate Quote
  • Form: Inline below header, blue-themed card with mileage/interval/notes fields
  • State: showReminderForm, reminderMileage, reminderInterval, reminderNotes, savingReminder
  • Handler: handleSaveReminder() creates record in reminders PocketBase collection
  • Dashboard widget: See src/pages/Dashboard.tsx — displays active reminders at top of dashboard