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

143 lines
7.0 KiB
Markdown

# 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<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`:
```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