8.1 KiB
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 displayfinancial-cp-profit - Warranty card (blue): inputs
financial-wh-total,financial-wh-cost, live displayfinancial-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 totalscalculateFinancialSummary()line 2140 — recomputes profits on every input changeapplyWarrantyDeduction()line 2180 — automatic: warranty amounts are deducted from CP amounts (storesoriginalTotalAmount/Costand 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):
{
"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)parseso.financial(string or object)finVal(f, newKey, legacyKey)reads new schema field with v1 legacy fallback:grossTotalfalls back tocpTotal(top-level fallbacko.grossTotal)grossCostfalls back tocpCostwarrTotalfalls back towhTotalwarrCostfalls back towhCostshopChargefalls back toshopCharge(top-levelo.shopChargeoro.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 TotalgrossCost← CP CostwarrTotalwarrCostshopChargecpProfit← CP GP (computed)whProfit← Warranty GP (computed)grossProfit← Overall GP (computed)totalAmount← CP Total + Warr TotaltotalCost← CP Cost + Warr CostcompletedAt← 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:
- Extended
TabIdunion andTABSarray with{ id: 'financial', label: 'Financial', icon: DollarSign } - Added financial input state (
finCpTotal,finCpCost,finWarrTotal,finWarrCost,finShopCharge,finSaving,finSaved,finSeeded) - Added pre-seed
useEffect(triggered whenactiveTab === 'financial'and!finSeeded) - Added live computed values and
handleSaveFinancial()handler - Added
financialContentJSX (sticky save bar, CP/Warranty/ShopCharge cards, Gross Profit summary) - Wired
case 'financial': return financialContent;into theactiveTabContentswitch
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.