# Financial Summary — v1 modal & v2 Financial tab ## v1: The editable modal (ShopProQuote original) **HTML**: `/mnt/seagate8tb/Websites/ShopProQuote/repair-orders.html:857-933` - Container `#financial-summary-modal` (hidden, unhidden on "Complete Order") - **Customer Pay** card (green): inputs `financial-cp-total`, `financial-cp-cost`, live display `financial-cp-profit` - **Warranty** card (blue): inputs `financial-wh-total`, `financial-wh-cost`, live display `financial-wh-profit` - **Gross Profit** bar: `financial-gross-profit` = (cpTotal + whTotal) - (cpCost + whCost), color-coded - Validation message `financial-validation-message` (at least one payment type must have values) - Cancel / "Complete Order" buttons **JS**: `/mnt/seagate8tb/Websites/ShopProQuote/repair-orders.js` - `openFinancialSummaryModal()` ~line 2016 — seeds CP total/cost from RO's existing totals - `calculateFinancialSummary()` line 2140 — recomputes profits on every input change - `applyWarrantyDeduction()` line 2180 — **automatic**: warranty amounts are deducted from CP amounts (stores `originalTotalAmount/Cost` and adjusts CP input fields down by whTotal/whCost) - `saveFinancialSummary()` ~line 2276 — writes the financial blob **Saved `financial` blob shape** (written to PocketBase/Firebase `repairOrders.financial`): ```json { "cpTotal": 0, "cpCost": 0, "whTotal": 0, "whCost": 0, "cpProfit": 0, "whProfit": 0, "totalAmount": 0, "totalCost": 0, "grossProfit": 0, "completedAt": "ISO timestamp" } ``` **Shop charge is NOT a field in the v1 modal.** Shop charge is configured globally (settings `shopChargeRate` % + `shopChargeCap` $) and applied per-line on quotes/ROs. The RO card summary's `#shop-charge-amount` (repair-orders.html:685) is a display of the computed shop charge, not a financial-modal input. If a `shopCharge` value is present in the financial blob, v2's dashboard will pick it up, but v1 never wrote one here. **Inline preview** (read-only) at `repair-orders.js:3273` — renders on each RO card, splits into "mixed" (two-column CP/Warranty cards) vs "single" (3-col Revenue/Cost/Profit) based on `hasBoth`. ## v2: Read-only reader (FinancialDashboard.tsx) **File**: `src/pages/FinancialDashboard.tsx` - `finData(o)` parses `o.financial` (string or object) - `finVal(f, newKey, legacyKey)` reads new schema field with v1 legacy fallback: - `grossTotal` falls back to `cpTotal` (top-level fallback `o.grossTotal`) - `grossCost` falls back to `cpCost` - `warrTotal` falls back to `whTotal` - `warrCost` falls back to `whCost` - `shopCharge` falls back to `shopCharge` (top-level `o.shopCharge` or `o.shopCharges`) - `customerPayGpOf(o)` = `max(0, grossTotal - warrTotal - shopCharge) - max(0, grossCost - warrCost)` - `warrantyGpOf(o)` = `warrTotal - warrCost` - Aggregated into `totalCustomerPayGP` + `totalWarrantyGP` = `overallGrossProfit` The FinancialDashboard is read-only. The editable Financial tab lives in `RODetailModal.tsx` (see below). ## PocketBase schema context `repairOrders.financial` is a JSON blob (see `references/pocketbase-schema.md`). Migration `1739999000003_promote_financial_fields.js` added top-level numeric columns `grossTotal`, `grossCost`, `warrTotal`, `warrCost`, `shopCharge` and backfilled from the blob — both the blob and the columns coexist; `FinancialDashboard.tsx` checks the blob first, then top-level. ## v2 Financial tab — IMPLEMENTED Ray confirmed the design and pre-seed behavior. Implementation is in `src/pages/RODetailModal.tsx` as a 5th tab ("Financial") in the TABS array. The infrastructure was already wired before implementation: the `roFinancialAudit` collection + PB hook (M25) auto-diffs the blob on every update, the `'financial'` ROEvent type is in `types.ts` and rendered in the timeline, and the Timeline tab already shows the audit trail. ### Placement New 5th tab "Financial" inside `RODetailModal.tsx` — NOT a separate backgroundLocation modal. Added to the `TABS` array (now: details, services, financial, history, timeline). The `TabId` type union was extended with `'financial'`. ### Inputs (advisor enters) - **CP Total** — the full RO billed amount (includes the warranty portion) - **CP Cost** — the full cost of the job (includes the warranty portion) - **Warranty Total** — the warranty-attributable revenue - **Warranty Cost** — the warranty-attributable cost - **Shop Charge** — consumables/supplies charge (NEW — v1 did not have this as an input in the financial modal) ### Computed live (display-only) - **CP Net Total** = CP Total - Warranty Total - **CP Net Cost** = CP Cost - Warranty Cost - **CP Gross Profit** = (CP Net Total) - (CP Net Cost) - Shop Charge - **Warranty Gross Profit** = Warranty Total - Warranty Cost (shop charge does NOT touch this — warranty is manufacturer-reimbursed at flat rates) - **Overall Gross Profit** = CP GP + Warranty GP This exactly matches `customerPayGpOf()` + `warrantyGpOf()` already in `FinancialDashboard.tsx:141-161`, so dashboard totals pick up the entered values with zero dashboard-side changes. ### Key design difference from v1 v1's `applyWarrantyDeduction()` auto-adjusted the CP **input fields** down when warranty was entered (mutating the inputs the advisor sees). Ray's v2 spec treats it as a **computed display** — the advisor enters the full CP Total/Cost, the tab computes and shows CP Net Total/Cost, and the advisor sees the deduction as a live readout, not as mutated inputs. This is intentional and was preserved in the implementation. ### Pre-seed (CONFIRMED by Ray) Ray confirmed pre-seed is desired. On first opening the Financial tab (`activeTab === 'financial'` and `!finSeeded`), inputs are pre-seeded from the RO's existing `financial` blob. If no existing values: - CP Total falls back to `computeROTotals().total` (the RO's computed grand total) - Shop Charge falls back to `computeROTotals().shopCharge` - CP Cost, Warranty Total, Warranty Cost default to empty The `finSeeded` flag prevents reseeding on subsequent tab switches. If the RO changes (different RO loaded), `finSeeded` should be reset — the effect depends on `[activeTab, ro, finSeeded, settings]`. ### Storage on save `handleSaveFinancial()` writes to `repairOrders.financial` JSON blob using the keys v2 already reads: - `grossTotal` ← CP Total - `grossCost` ← CP Cost - `warrTotal` - `warrCost` - `shopCharge` - `cpProfit` ← CP GP (computed) - `whProfit` ← Warranty GP (computed) - `grossProfit` ← Overall GP (computed) - `totalAmount` ← CP Total + Warr Total - `totalCost` ← CP Cost + Warr Cost - `completedAt` ← existing or new ISO timestamp Existing blob keys (e.g. `customerType`) are preserved via spread. An `'financial'` ROEvent is appended for the timeline. The PB `onRecordAfterUpdateRequest` hook (M25 migration) auto-creates hash-chained `roFinancialAudit` entries. ### Completion-flow lever (NOT yet implemented) The "Complete" button in RODetailModal Details tab still sets status to `'completed'` without switching to the Financial tab. A natural follow-up (Ray has not confirmed) is to switch to the Financial tab on completion-press, forcing financial entry on completion — matching v1's "Complete Repair Order" modal behavior. This remains a TBD. ### Implementation patch sequence (for reference) The Financial tab was added via 4 patches to `RODetailModal.tsx`: 1. Extended `TabId` union and `TABS` array with `{ id: 'financial', label: 'Financial', icon: DollarSign }` 2. Added financial input state (`finCpTotal`, `finCpCost`, `finWarrTotal`, `finWarrCost`, `finShopCharge`, `finSaving`, `finSaved`, `finSeeded`) 3. Added pre-seed `useEffect` (triggered when `activeTab === 'financial'` and `!finSeeded`) 4. Added live computed values and `handleSaveFinancial()` handler 5. Added `financialContent` JSX (sticky save bar, CP/Warranty/ShopCharge cards, Gross Profit summary) 6. Wired `case 'financial': return financialContent;` into the `activeTabContent` switch **Pitfall**: The `patch` tool requires `path`, `old_string`, and `new_string` all together. Omitting `path` (even when calling under a default-path context) causes a hard-fail after 6 retries. Always include the `path` parameter explicitly on every `patch` call.