diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx index 6f586e6..6ca578b 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { ScenarioSelectorWrapper } from "./scenarioSelectorWrapper"; import { DashboardSummaryCards } from "./DashboardSummaryCards"; @@ -124,6 +124,7 @@ export function ReportingClientArea({ scenarioId: selectedScenarioId!, }), enabled: measuresOpen && !!selectedScenarioId, + keepPreviousData: true, }); const scenarioLoading = isLoading && !!selectedScenarioId; @@ -160,37 +161,40 @@ export function ReportingClientArea({ // Scenario specific metrics that appear in the drawer (from API) and cannot be overlayed on baseline // ---------------------------------------- - const scenarioSpecific = scenarioData - ? { - constructionCost: scenarioData.construction_cost, - pcCost: scenarioData.pc_cost, - contingency: scenarioData.contingency, - funding: scenarioData.total_funding, - costPerSap: - scenarioData.total_sap_uplift && scenarioData.total_sap_uplift > 0 - ? (scenarioData.construction_cost + scenarioData.pc_cost) / - scenarioData.total_sap_uplift - : 0, - costPerCo2: - scenarioData.construction_cost > 0 - ? (scenarioData.construction_cost + scenarioData.pc_cost) / - ((baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon) - : 0, - netCost: scenarioData.net_cost, - grossPerUnit: scenarioData.gross_per_unit, - nUnits: scenarioData.n_units_upgraded, - totalCarbonSaved: - (baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon, - totalBillsSaved: - (baseline.totals.total_bills ?? 0) - scenarioData.total_bills, - averageCaribonSaved: - ((baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon) / - scenarioData.n_units_upgraded, - averageBillsSaved: - ((baseline.totals.total_bills ?? 0) - scenarioData.total_bills) / - scenarioData.n_units_upgraded, - } - : null; + const scenarioSpecific = useMemo(() => { + if (!scenarioData) return null; + + return { + constructionCost: scenarioData.construction_cost, + pcCost: scenarioData.pc_cost, + contingency: scenarioData.contingency, + funding: scenarioData.total_funding, + costPerSap: + scenarioData.total_sap_uplift && scenarioData.total_sap_uplift > 0 + ? (scenarioData.construction_cost + scenarioData.pc_cost) / + scenarioData.total_sap_uplift + : 0, + costPerCo2: + scenarioData.construction_cost > 0 + ? (scenarioData.construction_cost + scenarioData.pc_cost) / + ((baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon) + : 0, + netCost: scenarioData.net_cost, + grossPerUnit: scenarioData.gross_per_unit, + nUnits: scenarioData.n_units_upgraded, + totalCarbonSaved: + (baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon, + totalBillsSaved: + (baseline.totals.total_bills ?? 0) - scenarioData.total_bills, + averageCaribonSaved: + ((baseline.totals.total_carbon ?? 0) - scenarioData.total_carbon) / + scenarioData.n_units_upgraded, + averageBillsSaved: + ((baseline.totals.total_bills ?? 0) - scenarioData.total_bills) / + scenarioData.n_units_upgraded, + }; + }, [scenarioData, baseline]); + // Baseline stays baseline const activeMetrics = baseline; @@ -272,7 +276,11 @@ export function ReportingClientArea({ subtitle="High-level insights on performance, energy, and EPC quality." /> - +
{children}
@@ -75,6 +76,7 @@ function Metric({ color, gradient, variant = "green", + loading = false, }: { label: string; value: string | number; @@ -82,15 +84,38 @@ function Metric({ color: string; gradient: string; variant?: "green" | "blue" | "purple"; + loading?: boolean; }) { + if (loading || !value) { + return ( + +
+
+
+
+ + ); + } return (
- - {value} - - {label} - + {loading ? ( +
+
+
+
+
+ ) : ( + <> + + + {value} + + + {label} + + + )}
); @@ -108,6 +133,7 @@ function PairedMetric({ gradient, iconClassName = "text-gray-700", variant = "green", + loading = false, }: { title: string; icon: React.ComponentType>; @@ -116,30 +142,62 @@ function PairedMetric({ gradient: string; iconClassName?: string; variant?: "green" | "blue" | "purple"; + loading?: boolean; }) { + if (loading || !primary.value || !secondary.value) { + return ( + +
+
+
+
+ + ); + } + return (
-
- - {title} -
- -
-
-

{primary.label}

-

- {primary.value} -

+ {loading ? ( +
+
+
+
+
+
+
+
+
+
+
+
+ ) : ( + <> +
+ + + {title} + +
-
-

{secondary.label}

-

- {secondary.value} -

-
-
+
+
+

{primary.label}

+

+ {primary.value} +

+
+ +
+

{secondary.label}

+

+ {secondary.value} +

+
+
+ + )}
); @@ -172,7 +230,7 @@ function Section({
@@ -191,6 +249,22 @@ function Section({ ); } +/* ───────────────────────────────────────────── */ +/* Loading Skeleton for dashboard cards */ +/* ───────────────────────────────────────────── */ + +function LoadingOverlay() { + return ( +
+
+ {Array.from({ length: 9 }).map((_, i) => ( +
+ ))} +
+
+ ); +} + /* ───────────────────────────────────────────── */ /* Main Drawer */ /* ───────────────────────────────────────────── */ @@ -198,10 +272,11 @@ function Section({ export function ScenarioFinancialDrawer({ open, metrics, + loading = false, }: ScenarioFinancialDrawerProps) { return ( - {open && metrics && ( + {open && ( @@ -278,32 +362,37 @@ export function ScenarioFinancialDrawer({ iconClassName="text-blue-600" primary={{ label: "Construction works", - value: `£${formatNumber(metrics.constructionCost)}`, + value: metrics + ? `£${formatNumber(metrics.constructionCost)}` + : "", }} secondary={{ label: "Project delivery", - value: `£${formatNumber(metrics.pcCost)}`, + value: metrics ? `£${formatNumber(metrics.pcCost)}` : "", }} gradient={gradients.blue} variant="blue" + loading={loading} /> @@ -321,14 +410,15 @@ export function ScenarioFinancialDrawer({ iconClassName="text-purple-700" primary={{ label: "£ per SAP point", - value: `£${formatNumber(metrics.costPerSap)}`, + value: metrics ? `£${formatNumber(metrics.costPerSap)}` : "", }} secondary={{ label: "£ per tonne CO₂", - value: `£${formatNumber(metrics.costPerCo2)}`, + value: metrics ? `£${formatNumber(metrics.costPerCo2)}` : "", }} gradient={gradients.purple} variant="purple" + loading={loading} />