From 5e7b7aa0711d37fd5ffac7c90c2ffa5a7b7ca956 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 19 Jul 2026 15:34:44 +0100 Subject: [PATCH] feat(reporting): explain what each scenario cost figure covers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users couldn't tell what the cost values include or exclude — e.g. does "gross per home" cover contingency, is the principal contractor's price in there. Add plain-language "what's included / excluded" disclosure across the scenario overlay, sourced from one canonical copy module (costHelp.ts) so every surface says it the same way and matches the domain glossary. - New shared `InfoDot` primitive (the one ⓘ affordance). - Investment ledger: ⓘ on Net cost, gross/home, Construction works, Project delivery, Contingency, Bill savings, Carbon saved, and the two value-for-money ratios — each stating what it does/doesn't cover (e.g. construction = the principal contractor's works, excludes delivery + contingency; gross = works + delivery + contingency). - KPI band gains an optional `tip`; the "Gross cost /home" KPI uses it. - "Where the money goes" now notes it's construction works only, so it reconciles to the Construction works line, not gross cost. Copy only — no figures or queries changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../reporting/ReportingClientArea.tsx | 2 + .../reporting/components/InvestmentLedger.tsx | 56 ++++++------------- .../reporting/components/KpiBand.tsx | 7 ++- .../components/MeasureAllocation.tsx | 11 +++- .../reporting/components/costHelp.ts | 48 ++++++++++++++++ .../reporting/components/primitives.tsx | 33 +++++++++++ 6 files changed, 116 insertions(+), 41 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/reporting/components/costHelp.ts diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx index 8b0edd91..f93356b3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx @@ -31,6 +31,7 @@ import { type LedgerView, } from "./components/InvestmentLedger"; import { FilterChips, type ScenarioFilters } from "./components/FilterChips"; +import { COST_HELP } from "./components/costHelp"; import { DrillDownShelf, type DrillTarget } from "./components/DrillDownShelf"; import { StockBreakdown } from "./components/StockBreakdown"; import { MeasureAllocation } from "./components/MeasureAllocation"; @@ -491,6 +492,7 @@ function MetricsBody({ label: "Gross cost /home", value: `£${formatNumber(scenarioData.gross_per_unit ?? 0)}`, sub: `${scenarioData.n_units_upgraded} homes`, + tip: COST_HELP.grossPerHome, }, ] : []; diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/components/InvestmentLedger.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/components/InvestmentLedger.tsx index 1d34b13f..063b1b2b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/components/InvestmentLedger.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/components/InvestmentLedger.tsx @@ -1,13 +1,6 @@ "use client"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/app/shadcn_components/ui/tooltip"; -import { - Info, Wallet, Coins, Landmark, @@ -19,8 +12,9 @@ import { Cloud, type LucideIcon, } from "lucide-react"; -import { moneyFull } from "./primitives"; -import { carsOffTheRoad, PROJECT_DELIVERY_RATE } from "@/lib/reporting/model"; +import { InfoDot, moneyFull } from "./primitives"; +import { COST_HELP } from "./costHelp"; +import { carsOffTheRoad } from "@/lib/reporting/model"; export interface LedgerView { constructionCost: number; @@ -37,8 +31,6 @@ export interface LedgerView { costPerCarbon: number; } -const deliveryPct = Math.round(PROJECT_DELIVERY_RATE * 100); - // The cost-composition palette echoes the data-quality evidence bar: quiet // navy → gold → grey, so the reporting surfaces read as one system. const COST_COLORS = { @@ -47,27 +39,6 @@ const COST_COLORS = { contingency: "#c5cad8", } as const; -function InfoDot({ label, tip }: { label: string; tip: string }) { - return ( - - - - - - - {tip} - - - - ); -} - function GroupLabel({ icon: Icon, children, @@ -89,12 +60,14 @@ function IconRow({ label, value, note, + tip, tone = "neutral", }: { icon: LucideIcon; label: string; value: string; note?: string; + tip?: string; tone?: "neutral" | "benefit"; }) { const chip = @@ -111,6 +84,7 @@ function IconRow({ {label} + {tip && } Net cost - + - - {moneyFull(v.grossPerHome)}{" "} + + {moneyFull(v.grossPerHome)} gross/home +
@@ -200,6 +175,7 @@ export function InvestmentLedger({ v }: { v: LedgerView }) { style={{ background: p.color }} /> {p.label} + {moneyFull(p.value)} @@ -225,6 +201,7 @@ export function InvestmentLedger({ v }: { v: LedgerView }) { label="Bill savings" value={moneyFull(v.billSavings)} note={`${moneyFull(v.billSavingsPerHome)}/home`} + tip={COST_HELP.billSavings} tone="benefit" />
@@ -243,11 +221,13 @@ export function InvestmentLedger({ v }: { v: LedgerView }) { icon={Gauge} label="Cost per SAP point" value={moneyFull(v.costPerSap)} + tip={COST_HELP.costPerSap} /> diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/components/KpiBand.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/components/KpiBand.tsx index 426ce912..bc3ef0b1 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/components/KpiBand.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/components/KpiBand.tsx @@ -1,6 +1,6 @@ "use client"; -import { EpcChip, DeltaPill } from "./primitives"; +import { EpcChip, DeltaPill, InfoDot } from "./primitives"; export interface Kpi { label: string; @@ -8,6 +8,8 @@ export interface Kpi { unit?: string; sub?: React.ReactNode; delta?: { text: string; improved: boolean }; + /** Optional "what this includes/excludes" note, shown as an ⓘ by the label. */ + tip?: string; } /** @@ -22,8 +24,9 @@ export function KpiBand({ kpis }: { kpis: Kpi[] }) { key={k.label} className={`px-5 py-4 ${i > 0 ? "border-l border-gray-100" : ""}`} > -
+
{k.label} + {k.tip && }
{k.value} diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/components/MeasureAllocation.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/components/MeasureAllocation.tsx index 6fe7a581..265b6111 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/components/MeasureAllocation.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/components/MeasureAllocation.tsx @@ -8,7 +8,8 @@ import { type Measure, type MeasureGroup, } from "@/lib/reporting/measures"; -import { moneyCompact, moneyFull } from "./primitives"; +import { InfoDot, moneyCompact, moneyFull } from "./primitives"; +import { COST_HELP } from "./costHelp"; import { loadScenarioMeasures } from "../actions"; import type { DrillTarget } from "./DrillDownShelf"; import type { TagFilter } from "@/lib/reporting/viewState"; @@ -171,6 +172,14 @@ export function MeasureAllocation({ ))}
+

+ Construction works only — excludes project delivery and + contingency. + +

)} diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/components/costHelp.ts b/src/app/portfolio/[slug]/(portfolio)/reporting/components/costHelp.ts new file mode 100644 index 00000000..8284692c --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/components/costHelp.ts @@ -0,0 +1,48 @@ +import { PROJECT_DELIVERY_RATE } from "@/lib/reporting/model"; + +const deliveryPct = Math.round(PROJECT_DELIVERY_RATE * 100); + +/** + * One canonical set of plain-language definitions for the scenario cost + * figures — what each one includes and excludes — so the KPI band, the + * Investment ledger and the "where the money goes" breakdown all explain them + * the same way. Wording follows the domain glossary (CONTEXT.md → Reporting): + * Gross cost = construction works + project delivery + contingency; Net cost = + * gross − secured funding. + */ +export const COST_HELP = { + grossPerHome: + "Gross cost ÷ the homes upgraded in this scenario. Gross is all-in — " + + "construction works + project delivery + contingency — before any funding. " + + "Excludes VAT and internal staff time.", + net: + "Gross cost (construction works + project delivery + contingency) minus " + + "secured funding — the figure a board commits to. Excludes VAT and internal " + + "staff time.", + construction: + "The principal contractor's price for the physical works — the measures " + + "installed. Excludes project delivery and contingency.", + projectDelivery: + "Professional and management costs of delivering the works (design, project " + + "management, overheads), separate from the construction works. Currently " + + `estimated at ${deliveryPct}% of construction works.`, + contingency: + "A risk allowance for unforeseen costs, on top of construction works and " + + "project delivery. Part of gross cost (and so of net).", + billSavings: + "Modelled reduction in residents' annual energy bills once the works are " + + "in — this scenario versus current stock.", + carbonSaved: + "Modelled annual carbon saved once the works are in — this scenario versus " + + "current stock.", + costPerSap: + "Capital cost (construction works + project delivery) ÷ total SAP points " + + "gained. Excludes contingency and funding. Lower is better.", + costPerCarbon: + "Capital cost (construction works + project delivery) ÷ tonnes of CO₂ saved " + + "a year. Excludes contingency and funding. Lower is better.", + whereMoneyGoes: + "The construction works only — the cost of the measures installed. Excludes " + + 'project delivery and contingency, so this totals the "Construction works" ' + + "line in the ledger, not gross cost.", +} as const; diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/components/primitives.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/components/primitives.tsx index b95695f9..e3531042 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/components/primitives.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/components/primitives.tsx @@ -6,8 +6,41 @@ * green/red; everything else is quiet navy-on-white. */ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/app/shadcn_components/ui/tooltip"; +import { Info } from "lucide-react"; import { getEpcColorClass } from "@/app/utils"; +/** + * A small "ⓘ" affordance that reveals an explanation on hover/focus. The one + * way the reporting surfaces disclose what a figure includes or excludes, so + * the KPI band, ledger and spend breakdown all read the same. + */ +export function InfoDot({ label, tip }: { label: string; tip: string }) { + return ( + + + + + + + {tip} + + + + ); +} + const EPC_TEXT_ON: Record = { A: "text-white", B: "text-white",