Added in the fundamental scenario reporting

This commit is contained in:
Khalim Conn-Kowlessar 2025-12-08 11:48:42 +00:00
parent 5dce22c687
commit eaeaa13d0f
4 changed files with 112 additions and 71 deletions

View file

@ -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<string, number> = {
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,
});
}

View file

@ -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<string, number>;
}) {
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 (
<Card className="border border-gray-100 bg-white">
@ -68,7 +93,7 @@ export function BreakdownChart({
<Select value={selected} onValueChange={setSelected}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Select breakdown" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="epc">EPC Band</SelectItem>
@ -77,13 +102,6 @@ export function BreakdownChart({
</SelectContent>
</Select>
</div>
{selected === "epc" && (
<p className="text-xs text-gray-500 mt-1">
Where an EPC is missing, the EPC band shown here is a modelled
estimate.
</p>
)}
</CardHeader>
<CardContent>
@ -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"
/>
</CardContent>
</Card>
);
}
function MyTooltip({ payload }: CustomTooltipProps) {
if (!payload || payload.length === 0) return null;
return (
<div className="rounded-md bg-white shadow-lg border border-gray-200 px-3 py-2 text-xs text-gray-700">
{payload.map((p) => (
<div
key={p.dataKey}
className="flex justify-between gap-4 items-center"
>
<span>{p.dataKey}:</span>
<span className="font-medium">{p.value}</span>
</div>
))}
</div>
);
}

View file

@ -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}
/>
<div className="lg:col-span-2">

View file

@ -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.");
}