diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/DashboardSummaryCards.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/DashboardSummaryCards.tsx index 425b1ee..439b54a 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/DashboardSummaryCards.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/DashboardSummaryCards.tsx @@ -9,23 +9,29 @@ import { } from "@/app/shadcn_components/ui/card"; import { motion } from "framer-motion"; import { Home, Zap, Leaf, LineChart, FileQuestionIcon } from "lucide-react"; -import { formatNumber } from "@/app/utils"; +import { formatNumber, sapToEpc } from "@/app/utils"; import type { AverageMetrics, EstimatedCounts, TotalMetrics, ScenarioOverlayMetrics, + MetricKey, } from "./types"; -import type { MetricKey } from "./types"; -import { sapToEpc } from "@/app/utils"; -const cardStyles = { +/* ───────────────────────────────────────────── */ +/* Style maps */ +/* ───────────────────────────────────────────── */ + +const cardStyles: Record< + MetricKey, + { icon: React.ComponentType; color: string } +> = { totalHomes: { icon: Home, color: "text-purple-600" }, avgSap: { icon: LineChart, color: "text-blue-600" }, avgCarbon: { icon: Leaf, color: "text-emerald-600" }, avgBills: { icon: Zap, color: "text-amber-600" }, missingEpc: { icon: FileQuestionIcon, color: "text-red-600" }, -} as Record; color: string }>; +}; const epcColors: Record = { A: "text-epc_a", @@ -38,24 +44,38 @@ const epcColors: Record = { Unknown: "text-gray-400", }; +/* ───────────────────────────────────────────── */ +/* Helpers */ +/* ───────────────────────────────────────────── */ + function hasOverlay( - overlay: ScenarioOverlayMetrics | undefined + overlay: ScenarioOverlayMetrics | undefined, ): overlay is ScenarioOverlayMetrics { return overlay !== undefined; } +function Skeleton({ className = "" }: { className?: string }) { + return
; +} + +/* ───────────────────────────────────────────── */ +/* Component */ +/* ───────────────────────────────────────────── */ + export function DashboardSummaryCards({ total, totals, averages, estimatedCounts, scenarioOverlay, + loading = false, }: { total: number; totals: TotalMetrics; averages: AverageMetrics; estimatedCounts: EstimatedCounts; scenarioOverlay?: ScenarioOverlayMetrics | null; + loading?: boolean; }) { const missingEpcCount = estimatedCounts.estimated; const missingEpcPercent = total > 0 ? (missingEpcCount / total) * 100 : 0; @@ -66,10 +86,7 @@ export function DashboardSummaryCards({ const hasScenario = hasOverlay(overlay); function deltaLabel(baseline: number, scenario: number) { - const b = Number(baseline); - const s = Number(scenario); - const diff = s - b; - + const diff = scenario - baseline; if (!isFinite(diff) || diff === 0) return null; const sign = diff > 0 ? "▲" : "▼"; @@ -87,10 +104,6 @@ export function DashboardSummaryCards({ key: "totalHomes", title: "Number of Homes", baseline: total, - scenario: null, - baselineTotal: undefined, - scenarioTotal: undefined, - units: "", subtitle: "Total properties in this portfolio.", }, { @@ -100,8 +113,6 @@ export function DashboardSummaryCards({ scenario: overlay?.avgSap && `${sapToEpc(overlay.avgSap.scenario)} (${overlay.avgSap.scenario} pts)`, - baselineTotal: undefined, - scenarioTotal: undefined, subtitle: "Current SAP rating across all properties.", isEpc: true, }, @@ -144,92 +155,110 @@ export function DashboardSummaryCards({ return ( + {/* Header */} - - - - - {c.title} - + {loading ? ( + <> + + + + ) : ( + <> + + + + + {c.title} + + + )} + {/* Content */} - {/* BASELINE + SCENARIO ROW */}
- {/* BASELINE COLUMN */} + {/* Baseline */}
Baseline -
- - {c.key === "avgBills" ? `£${c.baseline}` : c.baseline} - - - {/* units next to baseline average */} - {c.units && ( - {c.units} - )} -
- - {/* Baseline total */} - {c.baselineTotal !== undefined && ( - - Total:{" "} - {c.key === "avgBills" - ? `£${formatNumber(c.baselineTotal)}` - : `${formatNumber(c.baselineTotal)} tCO₂e`} - + {loading ? ( + + ) : ( +
+ + {c.key === "avgBills" ? `£${c.baseline}` : c.baseline} + + {c.units && ( + {c.units} + )} +
)} + + {c.baselineTotal !== undefined && + (loading ? ( + + ) : ( + + Total:{" "} + {c.key === "avgBills" + ? `£${formatNumber(c.baselineTotal)}` + : `${formatNumber(c.baselineTotal)} tCO₂e`} + + ))}
- {/* SCENARIO COLUMN */} + {/* Scenario */} {hasScenario && c.scenario && (
Scenario - {/* average + delta + units row */} -
- - {c.key === "avgBills" ? `£${c.scenario}` : c.scenario} - - - {c.delta && {c.delta}} -
- - {/* Scenario total */} - {c.scenarioTotal !== undefined && ( - - Total:{" "} - {c.key === "avgBills" - ? `£${formatNumber(c.scenarioTotal)}` - : `${formatNumber(c.scenarioTotal)} tCO₂e`} - + {loading ? ( + + ) : ( +
+ + {c.key === "avgBills" ? `£${c.scenario}` : c.scenario} + + {c.delta && {c.delta}} +
)} + + {c.scenarioTotal !== undefined && + (loading ? ( + + ) : ( + + Total:{" "} + {c.key === "avgBills" + ? `£${formatNumber(c.scenarioTotal)}` + : `${formatNumber(c.scenarioTotal)} tCO₂e`} + + ))}
)}
@@ -246,7 +275,11 @@ export function DashboardSummaryCards({
-

{c.subtitle}

+ {loading ? ( + + ) : ( +

{c.subtitle}

+ )}
); diff --git a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx index 6ca578b..96867f3 100644 --- a/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/reporting/ReportingClientArea.tsx @@ -127,8 +127,6 @@ export function ReportingClientArea({ keepPreviousData: true, }); - const scenarioLoading = isLoading && !!selectedScenarioId; - // ---------------------------------------- // Build overlay for Dashboard Summary cards // ---------------------------------------- @@ -198,6 +196,8 @@ export function ReportingClientArea({ // Baseline stays baseline const activeMetrics = baseline; + const scenarioBusy = !!selectedScenarioId && (isLoading || isFetching); + return ( <>
@@ -215,22 +215,22 @@ export function ReportingClientArea({ {/* Show measures */} { setAppliedHideNonCompliant(value); }} @@ -244,11 +244,11 @@ export function ReportingClientArea({ "_blank", ); }} - disabled={scenarioLoading} + disabled={scenarioBusy} className={` rounded-md border px-3 py-2 text-sm font-medium transition ${ - scenarioLoading + scenarioBusy ? "border-gray-200 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50" } @@ -279,7 +279,7 @@ export function ReportingClientArea({
@@ -289,6 +289,7 @@ export function ReportingClientArea({ averages={activeMetrics.averages} estimatedCounts={activeMetrics.estimatedCounts} scenarioOverlay={scenarioOverlay} + loading={scenarioBusy} />