@@ -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",