143 lines
7.0 KiB
Markdown
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 |