diff --git a/src/app/api/portfolio/[portfolioId]/scenario/[scenarioId]/metrics/route.ts b/src/app/api/portfolio/[portfolioId]/scenario/[scenarioId]/metrics/route.ts index e2a4b3d..4f9471b 100644 --- a/src/app/api/portfolio/[portfolioId]/scenario/[scenarioId]/metrics/route.ts +++ b/src/app/api/portfolio/[portfolioId]/scenario/[scenarioId]/metrics/route.ts @@ -1,6 +1,7 @@ import { db } from "@/app/db/db"; import { sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; +import { sapToEpc } from "@/app/utils"; type PlanRow = { id: bigint; @@ -25,11 +26,7 @@ export async function GET( const pid = BigInt(portfolioId); const sid = BigInt(scenarioId); - // - // -------------------------------------------------------------- - // 1️⃣ Fetch all plans for this portfolio + scenario (FAST) - // -------------------------------------------------------------- - // + // Fetch all plans const planRows = await db.execute(sql` SELECT id, @@ -58,33 +55,21 @@ export async function GET( const planIds = plans.map((p) => p.id); - // - // -------------------------------------------------------------- - // 2️⃣ Fetch total funding for all planIds - // funding = SUM(project_funding + total_uplift) - // -------------------------------------------------------------- - // - + // Total funding const planIdArray = sql`ARRAY[${sql.join(planIds, sql`, `)}]::bigint[]`; const fundingRows = await db.execute(sql` - SELECT - SUM(COALESCE(project_funding, 0) + COALESCE(total_uplift, 0))::float AS total_funding - FROM funding_package - WHERE plan_id = ANY(${planIdArray}); -`); + SELECT + SUM(COALESCE(project_funding, 0) + COALESCE(total_uplift, 0))::float AS total_funding + FROM funding_package + WHERE plan_id = ANY(${planIdArray}); + `); - // - // -------------------------------------------------------------- - // 3️⃣ Aggregate scenario metrics (SAP, carbon, bills) - // -------------------------------------------------------------- - // + // Averages + totals const n = plans.length; const avg_sap = plans.reduce((s, r) => s + (r.post_sap_points ?? 0), 0) / n; - const avg_carbon = plans.reduce((s, r) => s + (r.post_co2_emissions ?? 0), 0) / n; - const avg_bills = plans.reduce((s, r) => s + (r.post_energy_bill ?? 0), 0) / n; @@ -92,18 +77,30 @@ export async function GET( (s, r) => s + (r.post_co2_emissions ?? 0), 0 ); - const total_bills = plans.reduce((s, r) => s + (r.post_energy_bill ?? 0), 0); - // - // -------------------------------------------------------------- - // 4️⃣ Financial Metrics (based on real funding) - // -------------------------------------------------------------- - // - const totalCost = 0; // you will add "plan.cost" later - const contingency = 0; // also update when you have it + // Financial + const totalCost = 0; + const contingency = 0; const funding = Number(fundingRows.rows[0]?.total_funding ?? 0); - const netCost = Number(totalCost) - funding; + const netCost = totalCost - funding; + + // NEW — scenario EPC band counts + const scenario_epc_counts: Record = { + A: 0, + B: 0, + C: 0, + D: 0, + E: 0, + F: 0, + G: 0, + Unknown: 0, + }; + + for (const p of plans) { + const band = sapToEpc(p.post_sap_points); + scenario_epc_counts[band] += 1; + } return NextResponse.json({ avg_sap: avg_sap.toFixed(1), @@ -117,5 +114,8 @@ export async function GET( net_cost: netCost, net_cost_per_unit: n > 0 ? netCost / n : 0, n_units: n, + + // NEW DATA for Overlay + scenario_epc_counts, }); } diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/BreakdownChart.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/BreakdownChart.tsx index 93e3824..ea0c5da 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/BreakdownChart.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/BreakdownChart.tsx @@ -8,6 +8,8 @@ import { CardContent, } from "@/app/shadcn_components/ui/card"; import { BarChart } from "@tremor/react"; +import type { CustomTooltipProps } from "@tremor/react"; + import { Select, SelectTrigger, @@ -15,48 +17,71 @@ import { SelectItem, SelectValue, } from "@/app/shadcn_components/ui/select"; + import type { EpcBandCount, AgeBandCount, PropertyTypeCount } from "./types"; export function BreakdownChart({ epcBands, ageBands, propertyTypes, + scenarioEpcBands, }: { epcBands: EpcBandCount[]; ageBands: AgeBandCount[]; propertyTypes: PropertyTypeCount[]; + scenarioEpcBands?: Record; }) { const [selected, setSelected] = useState("epc"); - const chartData = useMemo(() => { - switch (selected) { - case "epc": - return epcBands.map((d) => ({ - label: d.epc ?? "Unknown", - actual: d.actual ?? 0, - estimated: d.estimated ?? 0, - })); - case "age": - return ageBands.map((d) => ({ - label: d.age_band, - count: d.count, - })); - case "type": - return propertyTypes.map((d) => ({ - label: d.type ?? "Unknown", - count: d.count, - })); - default: - return []; - } - }, [selected, epcBands, ageBands, propertyTypes]); + const friendlyKeys = { + actual: "Actual EPCs", + estimated: "Estimated EPCs", + scenario: "Scenario result", + }; - const categories = selected === "epc" ? ["actual", "estimated"] : ["count"]; + const chartData = useMemo(() => { + if (selected !== "epc") { + return selected === "age" + ? ageBands.map((d) => ({ label: d.age_band, Count: d.count })) + : propertyTypes.map((d) => ({ + label: d.type ?? "Unknown", + Count: d.count, + })); + } + + const rows: any[] = []; + + for (const d of epcBands) { + const epc = d.epc ?? "Unknown"; + const scenarioValue = scenarioEpcBands?.[epc] ?? 0; + + // Baseline (stacked) + rows.push({ + label: `${epc} (baseline)`, + [friendlyKeys.actual]: d.actual ?? 0, + [friendlyKeys.estimated]: d.estimated ?? 0, + [friendlyKeys.scenario]: 0, + }); + + // Scenario (single bar) + rows.push({ + label: `${epc} (scenario)`, + [friendlyKeys.actual]: 0, + [friendlyKeys.estimated]: 0, + [friendlyKeys.scenario]: scenarioValue, + }); + } + + return rows; + }, [selected, epcBands, ageBands, propertyTypes, scenarioEpcBands]); + + const categories = + selected === "epc" + ? [friendlyKeys.actual, friendlyKeys.estimated, friendlyKeys.scenario] + : ["Count"]; const colors = - selected === "epc" - ? ["#14163d", "#3943b7"] // brand colours: actual + estimated - : ["#2d348f"]; // your existing colour + selected === "epc" ? ["#14163d", "#3943b7", "emerald"] : ["#2d348f"]; return ( @@ -68,7 +93,7 @@ export function BreakdownChart({ - - {selected === "epc" && ( -

- Where an EPC is missing, the EPC band shown here is a modelled - estimate. -

- )} @@ -93,10 +111,29 @@ export function BreakdownChart({ categories={categories} colors={colors} valueFormatter={(v) => v.toString()} - stack={selected === "epc"} // enable stacked bars only for EPC mode + stack={selected === "epc"} + customTooltip={MyTooltip} className="h-64" />
); } + +function MyTooltip({ payload }: CustomTooltipProps) { + if (!payload || payload.length === 0) return null; + + return ( +
+ {payload.map((p) => ( +
+ {p.dataKey}: + {p.value} +
+ ))} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx index 23f4d4c..1b30527 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx @@ -100,11 +100,10 @@ export function ReportingClientArea({ scenarioTotal: scenarioData.total_bills, }, valuation: { baseline: null, scenario: null }, + scenarioEpcBands: scenarioData.scenario_epc_counts, } : null; - console.log("Scenario Data:", scenarioData); - // ---------------------------------------- // Financial drawer values (from API) // ---------------------------------------- @@ -170,6 +169,7 @@ export function ReportingClientArea({ epcBands={activeMetrics.epcBands} ageBands={activeMetrics.ageBands} propertyTypes={propertyTypes} + scenarioEpcBands={scenarioOverlay?.scenarioEpcBands} />
diff --git a/src/app/utils.ts b/src/app/utils.ts index d3c5890..036f55d 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -92,7 +92,11 @@ export const serializeBigInt = (_: any, value: any): string | any => { return typeof value === "bigint" ? value.toString() : value; }; -export function sapToEpc(sapPoints: number): string { +export function sapToEpc(sapPoints: number | null): string { + if (sapPoints === null) { + return "Unknown"; + } + if (sapPoints < 0) { throw new Error("SAP points should be above 0."); }