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 JSONfetchCatalogServices()— 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()queriesrepairOrdersandquotesby VIN, triggered whenactiveTab === 'history' - Financial Audit Trail:
loadFinancialAudit()queriesroFinancialAuditbyroId, triggered whenactiveTab === 'timeline'
Financial Tab (implemented)
See references/financial-summary-modal.md for full design rationale and v1 history.
State
finCpTotal,finCpCost,finWarrTotal,finWarrCost,finShopCharge— string inputsfinSaving,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 ChargewarrGrossProfit=Warr Total - Warr CostoverallGrossProfit=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'sfinancialJSON blob) - Renders
TimelineEntrycomponents for each event type (created, status, clock_start, financial, etc.)
Financial Audit Trail (added Phase 4.3)
- Queries
roFinancialAuditPocketBase collection filtered by current ROroId, sorted newest-first (-at) - Each entry shows: field name (camelCase → Title Case via
formatFieldName),from → toJSON values in monospace, "by" attribution, truncated hash with full hash ontitlehover - Only renders when entries exist (hidden otherwise)
- Shows
Loader2spinner while loading
Adding a New Data Section to a Tab
The pattern:
- Add state:
const [data, setData] = useState<Type[]>([])+const [loading, setLoading] = useState(false) - Add
useCallbackload function querying PocketBase - Add
useEffecthooked toactiveTabandro - Add conditional render in the tab's content JSX (loading spinner + data display + empty state)
- Reuse existing imports (Loader2, formatDateTime, etc.)
Types
ROFinancialAuditEntryinsrc/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 componentsrc/components/ROForm.tsx— shared create/edit form (now includes catalog search; seereferences/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, formatElapsedsrc/lib/totals.ts— computeROTotals, roServiceTotalsrc/lib/technicianData.ts— parseRepairOrderServices, syncAssignmentsForROServicessrc/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 inremindersPocketBase collection - Dashboard widget: See
src/pages/Dashboard.tsx— displays active reminders at top of dashboard