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

124 lines
8.1 KiB
Markdown

# 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.