mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Added in the fundamental scenario reporting
This commit is contained in:
parent
5dce22c687
commit
eaeaa13d0f
4 changed files with 112 additions and 71 deletions
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue