Files
hermes-config/skills/self-hosting/shop-pro-quote/references/financial-summary-modal.md
T
2026-07-12 10:17:17 -04:00

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 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):

{
  "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.