From 86d919d7dfdb0de9a61ff87eb3cf32f5c60eaa20 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 5 Nov 2025 17:12:18 +0000 Subject: [PATCH] releasing new ui for abri projects --- src/app/components/portfolio/Toolbar.tsx | 2 +- .../(portfolio)/your-projects/layout.tsx | 4 +- .../(portfolio)/your-projects/live/page.tsx | 1 + .../your-projects/plan/ProjectProposal.tsx | 360 ++++++++++++++++++ .../{proposal => plan}/ProposalColumns.tsx | 0 .../your-projects/{proposal => plan}/page.tsx | 0 .../your-projects/{proposal => plan}/utils.ts | 4 + .../proposal/ProjectProposal.tsx | 249 ------------ .../[propertyId]/plans/[planId]/page.tsx | 2 - 9 files changed, 367 insertions(+), 255 deletions(-) create mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProjectProposal.tsx rename src/app/portfolio/[slug]/(portfolio)/your-projects/{proposal => plan}/ProposalColumns.tsx (100%) rename src/app/portfolio/[slug]/(portfolio)/your-projects/{proposal => plan}/page.tsx (100%) rename src/app/portfolio/[slug]/(portfolio)/your-projects/{proposal => plan}/utils.ts (95%) delete mode 100644 src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index a723b14..4d16b46 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -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", diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx index 7fb552f..d7afbe7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/layout.tsx @@ -12,9 +12,7 @@ export default async function Layout({ return (
- - Proposal - + Plan Live Reporting diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx index 52b463a..640b04c 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/live/page.tsx @@ -19,6 +19,7 @@ export default async function LiveReportingPage(props: { } // 🏢 Fetch the company + const [company] = await surveyDB .select() .from(hubspotCompanyData) diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProjectProposal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProjectProposal.tsx new file mode 100644 index 0000000..33179db --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProjectProposal.tsx @@ -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 = { + 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 = { + 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 = { + "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 = {}; + 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( + 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 ( +
+ {/* Chart */} + + + + Homes by Work Type + + {grouped.length > 1 && ( + + Click on the bars to view metrics for each work type. + + )} + + + {grouped.length > 1 ? ( + v.toString()} + onValueChange={(v) => + setSelectedType( + v && typeof v === "object" && "planType" in v + ? String((v as any).planType) + : null + ) + } + className="h-64" + /> + ) : ( + `${v} home${v === 1 ? "" : "s"}`} + /> + )} + + + + {/* Metrics */} + + + + {mappedTitles[selectedType || "default"]} + + + + {/* --- Top Row: Investment + Funding --- */} +
+
+
+ +

+ Capex Investment +

+
+

+ £{formatNumber(selectedData?.totalClientContribution || 0)} +

+

+ Avg per home £ + {formatNumber(selectedData?.avgClientContribution || 0)} +

+
+ +
+
+ +

+ Funding Unlocked +

+
+

+ £{formatNumber(selectedData?.totalFunding || 0)} +

+

+ Total funding available for this work type +

+
+
+ + {/* --- Bottom Row: Bills, Carbon, Post-SAP --- */} +
+
+ +

Bill Savings

+

+ £{formatNumber(selectedData?.totalBills || 0)} +

+
+ +
+ +

Carbon Savings

+

+ {formatNumber((selectedData?.totalCarbon || 0) * 1000)} + + kgCO₂e + +

+
+ +
+ +

Avg Post-SAP

+

+ {selectedData?.avgSapPost + ? Number(selectedData.avgSapPost).toFixed(0) + : "—"} +

+
+
+
+
+
+ ); +} + +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)}{" "} + kgCO₂e + + ), + subtitle: ( + <> + + This is equivalent to removing {carsOffRoad} cars from the road for + one year. + + + + ), + icon: Leaf, + }, + ]; + + return ( +
+ {cards.map((c) => { + const style = cardStyles[c.title] || { + color: "text-brandblue", + icon: Home, + }; + const Icon = style.icon; + + return ( + + +
+ + + +
+ + {c.title} + +
+ + +
+
+ {c.value} +
+
+
+ + +

{c.subtitle}

+
+
+ ); + })} +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProposalColumns.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProposalColumns.tsx similarity index 100% rename from src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProposalColumns.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/plan/ProposalColumns.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/page.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/page.tsx similarity index 100% rename from src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/page.tsx rename to src/app/portfolio/[slug]/(portfolio)/your-projects/plan/page.tsx diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/utils.ts similarity index 95% rename from src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts rename to src/app/portfolio/[slug]/(portfolio)/your-projects/plan/utils.ts index db310d0..cc84c00 100644 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/utils.ts +++ b/src/app/portfolio/[slug]/(portfolio)/your-projects/plan/utils.ts @@ -23,6 +23,7 @@ export interface PlanWithTotals extends Record { 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, }; }); diff --git a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx b/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx deleted file mode 100644 index 3994534..0000000 --- a/src/app/portfolio/[slug]/(portfolio)/your-projects/proposal/ProjectProposal.tsx +++ /dev/null @@ -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 = { - 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(null); - - const grouped = useMemo(() => { - const map: Record = {}; - 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 ( -
- {/* Chart */} - - - - Homes by Work Type - - - - {grouped.length > 1 ? ( - v.toString()} - onValueChange={(v) => - setSelectedType( - v && typeof v === "object" && "planType" in v - ? String((v as any).planType) - : null - ) - } - className="h-64" - /> - ) : ( - `${v} home${v === 1 ? "" : "s"}`} - /> - )} - - - - {/* Metrics */} - - - - {mappedTitles[selectedType || "default"]} - - - -
-

Total investment

-

- £{formatNumber(selectedData?.totalClientContribution || 0)} -

-

- Avg per home £ - {formatNumber(selectedData?.avgClientContribution || 0)} -

-
- -
-
-

Funding

-

- £{formatNumber(selectedData?.totalFunding || 0)} -

-
-
-

Carbon

-

- {((selectedData?.totalCarbon || 0) * 1000).toFixed(0)}{" "} - kgCO₂e -

-
-
-

Bills

-

- £{formatNumber(selectedData?.totalBills || 0)} -

-
-
-
-
-
- ); -} - -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 - }, - { - 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 ( -
- {cards.map((c) => { - const Icon = c.icon; - return ( - - -
- - - -
- - {c.title} - -
- - -
- {c.value} -
-

{c.subtitle}

-
-
- ); - })} -
- ); -} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx index cf98d39..aa48775 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/[planId]/page.tsx @@ -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 (