mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
Merge pull request #120 from Hestia-Homes/eco-project-data
Improving UI for project plan
This commit is contained in:
commit
14697771a4
9 changed files with 367 additions and 255 deletions
|
|
@ -55,7 +55,7 @@ export function Toolbar({ portfolioId, scenarios }: ToolbarProps) {
|
|||
icon: RocketLaunchIcon,
|
||||
match: (p: string) =>
|
||||
p.startsWith(`/portfolio/${portfolioId}/your-projects`),
|
||||
href: `/portfolio/${portfolioId}/your-projects/proposal`,
|
||||
href: `/portfolio/${portfolioId}/your-projects/plan`,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ export default async function Layout({
|
|||
return (
|
||||
<section>
|
||||
<div className="flex justify-center border-b mb-6 max-w-8xl mx-auto">
|
||||
<TabLink href={`/portfolio/${slug}/your-projects/proposal`}>
|
||||
Proposal
|
||||
</TabLink>
|
||||
<TabLink href={`/portfolio/${slug}/your-projects/plan`}>Plan</TabLink>
|
||||
<TabLink href={`/portfolio/${slug}/your-projects/live`}>
|
||||
Live Reporting
|
||||
</TabLink>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default async function LiveReportingPage(props: {
|
|||
}
|
||||
|
||||
// 🏢 Fetch the company
|
||||
|
||||
const [company] = await surveyDB
|
||||
.select()
|
||||
.from(hubspotCompanyData)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,360 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardDescription,
|
||||
} from "@/app/shadcn_components/ui/card";
|
||||
import { BarChart, DonutChart } from "@tremor/react";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import {
|
||||
PoundSterling,
|
||||
Leaf,
|
||||
Zap,
|
||||
Home,
|
||||
CarIcon,
|
||||
LineChart,
|
||||
CoinsIcon,
|
||||
} from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const mappedTitles: Record<string, string> = {
|
||||
solar_eco4: "Solar ECO4 project metrics",
|
||||
solar_hhrsh_eco4: "Solar & HHRSH ECO4 project metrics",
|
||||
empty_cavity_eco: "Empty Cavity Insulation metrics",
|
||||
partial_cavity_eco: "Partial Cavity Insulation metrics",
|
||||
extraction_eco: "Cavity Extraction & Refill project metrics",
|
||||
default: "Select a work type to view metrics",
|
||||
};
|
||||
|
||||
const displayNames: Record<string, string> = {
|
||||
solar_eco4: "Solar ECO4",
|
||||
solar_hhrsh_eco4: "Solar & HHRSH ECO4",
|
||||
empty_cavity_eco: "Empty Cavity Insulation",
|
||||
partial_cavity_eco: "Partial Cavity Insulation",
|
||||
extraction_eco: "Cavity Extraction & Refill",
|
||||
};
|
||||
|
||||
const cardStyles: Record<string, { color: string; icon: React.ElementType }> = {
|
||||
"Number of Homes": { color: "text-purple-600", icon: Home },
|
||||
"Estimated Capex Investment": {
|
||||
color: "text-green-600",
|
||||
icon: PoundSterling,
|
||||
},
|
||||
"Total Funding": { color: "text-brandbrown", icon: CoinsIcon },
|
||||
"Bill Savings": { color: "text-amber-600", icon: Zap },
|
||||
"Average post SAP": { color: "text-blue-600", icon: LineChart },
|
||||
"Carbon Savings": { color: "text-emerald-600", icon: Leaf },
|
||||
};
|
||||
|
||||
export function ProjectProposal({ plans }: { plans: any[] }) {
|
||||
const grouped = useMemo(() => {
|
||||
const map: Record<string, any[]> = {};
|
||||
for (const plan of plans) {
|
||||
if (!plan.planType) continue;
|
||||
if (!map[plan.planType]) map[plan.planType] = [];
|
||||
map[plan.planType].push(plan);
|
||||
}
|
||||
|
||||
return Object.entries(map).map(([type, list]) => {
|
||||
const totalFunding = list.reduce(
|
||||
(sum, p) => sum + (p.totalFunding ?? 0),
|
||||
0
|
||||
);
|
||||
const totalClientContribution = list.reduce(
|
||||
(sum, p) => sum + (p.clientContribution ?? 0),
|
||||
0
|
||||
);
|
||||
const totalCarbon = list.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings ?? 0),
|
||||
0
|
||||
);
|
||||
const totalBills = list.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings ?? 0),
|
||||
0
|
||||
);
|
||||
const avgSapPost =
|
||||
list.reduce((sum, p) => sum + (p.averageSapPost ?? 0), 0) / list.length;
|
||||
|
||||
return {
|
||||
planType: type,
|
||||
label: displayNames[type] || type, // 👈 new human-friendly label
|
||||
count: list.length,
|
||||
avgClientContribution: totalClientContribution / list.length,
|
||||
totalClientContribution,
|
||||
totalFunding,
|
||||
totalCarbon,
|
||||
totalBills,
|
||||
avgSapPost,
|
||||
};
|
||||
});
|
||||
}, [plans]);
|
||||
|
||||
const [selectedType, setSelectedType] = useState<string | null>(
|
||||
grouped[0]?.planType ?? null
|
||||
);
|
||||
|
||||
// Derive the selected data safely
|
||||
const selectedData = useMemo(() => {
|
||||
if (!selectedType || !grouped.length) return null;
|
||||
return grouped.find((d) => d.planType === selectedType) ?? grouped[0];
|
||||
}, [grouped, selectedType]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 grid-cols-4 gap-4">
|
||||
{/* Chart */}
|
||||
<Card className="col-span-2 border border-gray-100 bg-gradient-to-br from-white to-gray-50/40 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-brandblue">
|
||||
Homes by Work Type
|
||||
</CardTitle>
|
||||
{grouped.length > 1 && (
|
||||
<CardDescription className="text-sm text-gray-500">
|
||||
Click on the bars to view metrics for each work type.
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{grouped.length > 1 ? (
|
||||
<BarChart
|
||||
data={grouped}
|
||||
index="label" // 👈 use label instead of planType
|
||||
categories={["count"]}
|
||||
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
|
||||
valueFormatter={(v) => v.toString()}
|
||||
onValueChange={(v) =>
|
||||
setSelectedType(
|
||||
v && typeof v === "object" && "planType" in v
|
||||
? String((v as any).planType)
|
||||
: null
|
||||
)
|
||||
}
|
||||
className="h-64"
|
||||
/>
|
||||
) : (
|
||||
<DonutChart
|
||||
data={grouped}
|
||||
category="count"
|
||||
index="planType"
|
||||
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
|
||||
valueFormatter={(v) => `${v} home${v === 1 ? "" : "s"}`}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Metrics */}
|
||||
<Card className="col-span-2 border border-gray-200 bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-brandblue">
|
||||
{mappedTitles[selectedType || "default"]}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-8">
|
||||
{/* --- Top Row: Investment + Funding --- */}
|
||||
<div className="grid grid-cols-2 gap-4 border-b border-gray-100 pb-6">
|
||||
<div className="flex flex-col items-start justify-center bg-gradient-to-br from-amber-50/50 to-white rounded-xl p-4 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<PoundSterling className="h-4 w-4 text-brandbrown" />
|
||||
<p className="text-sm text-gray-600 font-medium">
|
||||
Capex Investment
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-brandbrown leading-tight">
|
||||
£{formatNumber(selectedData?.totalClientContribution || 0)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Avg per home £
|
||||
{formatNumber(selectedData?.avgClientContribution || 0)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start justify-center bg-gradient-to-br from-blue-50/50 to-white rounded-xl p-4 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CoinsIcon className="h-4 w-4 text-brandblue" />
|
||||
<p className="text-sm text-gray-600 font-medium">
|
||||
Funding Unlocked
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-brandblue leading-tight">
|
||||
£{formatNumber(selectedData?.totalFunding || 0)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Total funding available for this work type
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- Bottom Row: Bills, Carbon, Post-SAP --- */}
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div className="bg-white rounded-lg shadow-sm p-3 hover:bg-gray-50 transition-colors">
|
||||
<Zap className="h-4 w-4 mx-auto text-amber-600 mb-1" />
|
||||
<p className="text-xs text-gray-500 mb-1">Bill Savings</p>
|
||||
<p className="text-md font-semibold text-brandblue">
|
||||
£{formatNumber(selectedData?.totalBills || 0)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm p-3 hover:bg-gray-50 transition-colors">
|
||||
<Leaf className="h-4 w-4 mx-auto text-emerald-600 mb-1" />
|
||||
<p className="text-xs text-gray-500 mb-1">Carbon Savings</p>
|
||||
<p className="text-md font-semibold text-brandblue">
|
||||
{formatNumber((selectedData?.totalCarbon || 0) * 1000)}
|
||||
<span className="text-xs text-gray-600 align-bottom">
|
||||
kgCO₂e
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-sm p-3 hover:bg-gray-50 transition-colors">
|
||||
<LineChart className="h-4 w-4 mx-auto text-blue-600 mb-1" />
|
||||
<p className="text-xs text-gray-500 mb-1">Avg Post-SAP</p>
|
||||
<p className="text-md font-semibold text-brandblue">
|
||||
{selectedData?.avgSapPost
|
||||
? Number(selectedData.avgSapPost).toFixed(0)
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardSummary({ plans }: { plans: any[] }) {
|
||||
const totalFunding = plans.reduce((sum, p) => sum + (p.totalFunding || 0), 0);
|
||||
const totalInvestment = plans.reduce(
|
||||
(sum, p) => sum + (p.clientContribution || 0),
|
||||
0
|
||||
);
|
||||
const totalCarbon = plans.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings || 0),
|
||||
0
|
||||
);
|
||||
const averageSapPost =
|
||||
plans.reduce((sum, p) => sum + (p.averageSapPost || 0), 0) / plans.length;
|
||||
|
||||
const averageBillSavings =
|
||||
plans.reduce((sum, p) => sum + (p.totalBillSavings || 0), 0) / plans.length;
|
||||
|
||||
const totalBills = plans.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings || 0),
|
||||
0
|
||||
);
|
||||
const planCount = plans.length;
|
||||
|
||||
// Will be using this UK gov report for car stats:
|
||||
//www.gov.uk/government/statistics/vehicle-licensing-statistics-2022/vehicle-licensing-statistics-2022.
|
||||
// https: "In the UK, the average CO2 emissions for cars registered for the first time in 2022 was 110.8 grams per kilometre (g/km)";
|
||||
const avgCarCO2PerYearKg = (108 * 11_000) / 1000; // average 108 g/km × 11,000 km
|
||||
const carsOffRoad = Math.round((totalCarbon * 1000) / avgCarCO2PerYearKg);
|
||||
|
||||
const cards: {
|
||||
title: string;
|
||||
value: string | number | React.ReactNode;
|
||||
subtitle: string | React.ReactNode;
|
||||
icon: React.ElementType;
|
||||
}[] = [
|
||||
{
|
||||
title: "Number of Homes",
|
||||
value: planCount,
|
||||
subtitle: "Properties included across your project plans.",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: "Estimated Capex Investment",
|
||||
value: `£${formatNumber(totalInvestment)}`,
|
||||
subtitle:
|
||||
"This is an estimate of the total investment needed for your project.",
|
||||
icon: PoundSterling,
|
||||
},
|
||||
{
|
||||
title: "Total Funding",
|
||||
value: `£${formatNumber(totalFunding)}`,
|
||||
subtitle: "Domna will help you unlock this much grant funding.",
|
||||
icon: CoinsIcon,
|
||||
},
|
||||
{
|
||||
title: "Bill Savings",
|
||||
value: `£${formatNumber(totalBills)}`,
|
||||
subtitle: `This amounts to a £${formatNumber(averageBillSavings ? Number(averageBillSavings.toFixed(0)) : 0)} saving per home`,
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: "Average post SAP",
|
||||
value: `${averageSapPost.toFixed(0)}`,
|
||||
subtitle: "Expected average SAP rating across all homes, after works.",
|
||||
icon: LineChart,
|
||||
},
|
||||
{
|
||||
title: "Carbon Savings",
|
||||
value: (
|
||||
<>
|
||||
{formatNumber(totalCarbon * 1000)}{" "}
|
||||
<span className="text-sm text-gray-600 align-bottom">kgCO₂e</span>
|
||||
</>
|
||||
),
|
||||
subtitle: (
|
||||
<>
|
||||
<span>
|
||||
This is equivalent to removing {carsOffRoad} cars from the road for
|
||||
one year.
|
||||
</span>
|
||||
<CarIcon className="inline-block ml-1 h-4 w-4 text-gray-500 align-middle text-brandbrown" />
|
||||
</>
|
||||
),
|
||||
icon: Leaf,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{cards.map((c) => {
|
||||
const style = cardStyles[c.title] || {
|
||||
color: "text-brandblue",
|
||||
icon: Home,
|
||||
};
|
||||
const Icon = style.icon;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={c.title}
|
||||
className="relative h-full flex flex-col border border-gray-100 bg-gradient-to-br from-white to-brandlightblue/10 hover:shadow-lg hover:-translate-y-0.5 transition-all duration-300"
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-2 pb-1">
|
||||
<div className="p-1.5 rounded-md bg-brandlightblue/40">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-1.5 rounded-md bg-brandlightblue/40"
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${style.color}`} />
|
||||
</motion.div>
|
||||
</div>
|
||||
<CardTitle className="text-md font-medium text-gray-600 mb-2">
|
||||
{c.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col pb-2 mb-2">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<div className="text-3xl font-semibold text-transparent bg-clip-text bg-gradient-to-r from-brandblue to-midblue">
|
||||
{c.value}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="pt-0 pb-4">
|
||||
<p className="text-xs text-gray-500">{c.subtitle}</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ export interface PlanWithTotals extends Record<string, unknown> {
|
|||
surveyCost?: number;
|
||||
clientContribution?: number;
|
||||
totalRecommendationSapPoints: number | null;
|
||||
averageSapPost?: number;
|
||||
}
|
||||
|
||||
export async function getPlansWithTotals(
|
||||
|
|
@ -95,12 +96,15 @@ export async function getPlansWithTotals(
|
|||
|
||||
const rawContribution = totalCost + surveyCost - funding - uplift;
|
||||
const clientContribution = rawContribution > 0 ? rawContribution : 0;
|
||||
const postSapPoints =
|
||||
(plan.currentSapPoints ?? 0) + (plan.totalRecommendationSapPoints ?? 0);
|
||||
|
||||
return {
|
||||
...plan,
|
||||
totalFunding: funding, // overwrite
|
||||
surveyCost,
|
||||
clientContribution,
|
||||
averageSapPost: postSapPoints,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/app/shadcn_components/ui/card";
|
||||
import { BarChart, DonutChart } from "@tremor/react";
|
||||
import { formatNumber } from "@/app/utils";
|
||||
import { PoundSterling, Leaf, Zap, Home } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const mappedTitles: Record<string, string> = {
|
||||
solar_eco4: "Solar ECO4 project metrics",
|
||||
solar_hhrsh_eco4: "Solar HHRSH ECO4 project metrics",
|
||||
empty_cavity_eco: "Empty Cavity Insulation metrics",
|
||||
partial_cavity_eco: "Partial Cavity Insulation metrics",
|
||||
extraction_eco: "Extraction & Refill project metrics",
|
||||
default: "Select a work type to view metrics",
|
||||
};
|
||||
|
||||
export function ProjectProposal({ plans }: { plans: any[] }) {
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null);
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const map: Record<string, any[]> = {};
|
||||
for (const plan of plans) {
|
||||
if (!plan.planType) continue;
|
||||
if (!map[plan.planType]) map[plan.planType] = [];
|
||||
map[plan.planType].push(plan);
|
||||
}
|
||||
|
||||
return Object.entries(map).map(([type, list]) => {
|
||||
const totalFunding = list.reduce(
|
||||
(sum, p) => sum + (p.totalFunding ?? 0),
|
||||
0
|
||||
);
|
||||
const totalClientContribution = list.reduce(
|
||||
(sum, p) => sum + (p.clientContribution ?? 0),
|
||||
0
|
||||
);
|
||||
const totalCarbon = list.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings ?? 0),
|
||||
0
|
||||
);
|
||||
const totalBills = list.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings ?? 0),
|
||||
0
|
||||
);
|
||||
return {
|
||||
planType: type,
|
||||
count: list.length,
|
||||
avgClientContribution: totalClientContribution / list.length,
|
||||
totalClientContribution,
|
||||
totalFunding,
|
||||
totalCarbon,
|
||||
totalBills,
|
||||
};
|
||||
});
|
||||
}, [plans]);
|
||||
|
||||
useMemo(() => {
|
||||
if (grouped.length === 1 && !selectedType)
|
||||
setSelectedType(grouped[0].planType);
|
||||
}, [grouped, selectedType]);
|
||||
|
||||
const selectedData =
|
||||
selectedType && grouped.length
|
||||
? grouped.find((d) => d.planType === selectedType)
|
||||
: grouped.length === 1
|
||||
? grouped[0]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 grid-cols-5 gap-4">
|
||||
{/* Chart */}
|
||||
<Card className="col-span-3 border border-gray-100 bg-gradient-to-br from-white to-gray-50/40 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-brandblue">
|
||||
Homes by Work Type
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{grouped.length > 1 ? (
|
||||
<BarChart
|
||||
data={grouped}
|
||||
index="planType"
|
||||
categories={["count"]}
|
||||
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
|
||||
valueFormatter={(v) => v.toString()}
|
||||
onValueChange={(v) =>
|
||||
setSelectedType(
|
||||
v && typeof v === "object" && "planType" in v
|
||||
? String((v as any).planType)
|
||||
: null
|
||||
)
|
||||
}
|
||||
className="h-64"
|
||||
/>
|
||||
) : (
|
||||
<DonutChart
|
||||
data={grouped}
|
||||
category="count"
|
||||
index="planType"
|
||||
colors={["#2d348f", "#14163d", "#3943b7", "#5d6be0"]}
|
||||
valueFormatter={(v) => `${v} home${v === 1 ? "" : "s"}`}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Metrics */}
|
||||
<Card className="col-span-2 border border-gray-200 bg-white">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold text-brandblue">
|
||||
{mappedTitles[selectedType || "default"]}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-1">Total investment</p>
|
||||
<p className="text-2xl font-semibold text-brandbrown">
|
||||
£{formatNumber(selectedData?.totalClientContribution || 0)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Avg per home £
|
||||
{formatNumber(selectedData?.avgClientContribution || 0)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 text-center border-t pt-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Funding</p>
|
||||
<p className="text-md font-medium text-brandblue">
|
||||
£{formatNumber(selectedData?.totalFunding || 0)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Carbon</p>
|
||||
<p className="text-md font-medium text-brandblue">
|
||||
{((selectedData?.totalCarbon || 0) * 1000).toFixed(0)}{" "}
|
||||
<span className="text-sm text-gray-600 align-top">kgCO₂e</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 mb-1">Bills</p>
|
||||
<p className="text-md font-medium text-brandblue">
|
||||
£{formatNumber(selectedData?.totalBills || 0)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardSummary({ plans }: { plans: any[] }) {
|
||||
const totalFunding = plans.reduce((sum, p) => sum + (p.totalFunding || 0), 0);
|
||||
const totalCarbon = plans.reduce(
|
||||
(sum, p) => sum + (p.totalCarbonSavings || 0),
|
||||
0
|
||||
);
|
||||
const totalBills = plans.reduce(
|
||||
(sum, p) => sum + (p.totalBillSavings || 0),
|
||||
0
|
||||
);
|
||||
const planCount = plans.length;
|
||||
|
||||
const cards: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle: string;
|
||||
icon: React.ElementType;
|
||||
}[] = [
|
||||
{
|
||||
title: "Total Funding",
|
||||
value: `£${formatNumber(totalFunding)}`,
|
||||
subtitle: "Domna will help you unlock this much funding.",
|
||||
icon: PoundSterling, // ✅ no <PoundSterling />
|
||||
},
|
||||
{
|
||||
title: "Carbon Savings",
|
||||
value: `${(totalCarbon * 1000).toFixed(2)} kgCO₂e`,
|
||||
subtitle: "Your projects’ total estimated CO₂e savings, per year.",
|
||||
icon: Leaf,
|
||||
},
|
||||
{
|
||||
title: "Bill Savings",
|
||||
value: `£${formatNumber(totalBills)}`,
|
||||
subtitle: "Expected total bill reductions across all homes, per year.",
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: "Number of Homes",
|
||||
value: planCount,
|
||||
subtitle: "Properties included across your project plans.",
|
||||
icon: Home,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{cards.map((c) => {
|
||||
const Icon = c.icon;
|
||||
return (
|
||||
<Card
|
||||
key={c.title}
|
||||
className="border border-gray-100 bg-gradient-to-br from-white to-brandlightblue/10 hover:shadow-lg hover:-translate-y-0.5 transition-all duration-300"
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-2 pb-1">
|
||||
<div className="p-1.5 rounded-md bg-brandlightblue/40">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-1.5 rounded-md bg-brandlightblue/40"
|
||||
>
|
||||
<Icon
|
||||
className={`h-4 w-4 ${
|
||||
c.title.includes("Funding")
|
||||
? "text-brandbrown"
|
||||
: c.title.includes("Carbon")
|
||||
? "text-emerald-600"
|
||||
: c.title.includes("Bill")
|
||||
? "text-amber-600"
|
||||
: "text-brandblue"
|
||||
}`}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<CardTitle className="text-lg font-medium text-gray-600">
|
||||
{c.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="text-3xl font-semibold text-transparent bg-clip-text bg-gradient-to-r from-brandblue to-midblue mb-1 pb-2">
|
||||
{c.value}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{c.subtitle}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,8 +15,6 @@ export default async function Recommendations(props: {
|
|||
const planMeta = await getPlanMeta(params.planId);
|
||||
const funding = await getPlanFunding(params.planId);
|
||||
|
||||
console.log("funding", funding);
|
||||
|
||||
return (
|
||||
<div className="leading-loose tracking-wider">
|
||||
<RecommendationContainer
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue