# 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: ```tsx 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([])` + `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`: ```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