Updating the plans ui
Some checks are pending
Next.js Build Check / build (push) Waiting to run

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-08 20:47:14 +00:00
parent c3e657847a
commit 0c0529b234
4 changed files with 571 additions and 67 deletions

View file

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { db } from "@/app/db/db";
import { plan } from "@/app/db/schema/recommendations";
import { eq } from "drizzle-orm";
export async function POST(
_req: Request,
context: { params: Promise<{ id: string }> }
) {
const { id } = await context.params;
const planId = Number(id);
if (Number.isNaN(planId)) {
return NextResponse.json({ error: "Invalid plan id" }, { status: 400 });
}
const target = await db.query.plan.findFirst({
where: eq(plan.id, BigInt(planId)),
columns: { propertyId: true },
});
if (!target) {
return NextResponse.json({ error: "Plan not found" }, { status: 404 });
}
await db.transaction(async (tx) => {
await tx
.update(plan)
.set({ isDefault: false })
.where(eq(plan.propertyId, target.propertyId));
await tx
.update(plan)
.set({ isDefault: true })
.where(eq(plan.id, BigInt(planId)));
});
return NextResponse.json({ success: true });
}

View file

@ -3,11 +3,11 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import {
TrashIcon,
ArrowTrendingUpIcon,
CalendarIcon,
BanknotesIcon,
ArrowRightIcon,
EllipsisVerticalIcon,
} from "@heroicons/react/24/outline";
import { useRouter, usePathname } from "next/navigation";
@ -30,6 +30,13 @@ import {
TableRow,
} from "@/app/shadcn_components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/app/shadcn_components/ui/dropdown-menu";
import { Button } from "@/app/shadcn_components/ui/button";
/* ----------------------------------------
@ -70,6 +77,15 @@ async function confirmPlanDeletion(planId: string): Promise<void> {
}
}
async function setDefaultPlan(planId: string): Promise<void> {
const res = await fetch(`/api/plan/${planId}/set-default`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error("Failed to set default plan");
}
/* ----------------------------------------
EPC Badge
----------------------------------------- */
@ -98,6 +114,7 @@ export default function PlanCard({
totalSapPoints,
planName,
planId,
isDefault,
}: {
expectedEpcRating: string;
currentEpcRating: string;
@ -106,8 +123,10 @@ export default function PlanCard({
totalSapPoints: number;
planName: string | null;
planId: string;
isDefault: boolean;
}) {
const [open, setOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [setDefaultOpen, setSetDefaultOpen] = useState(false);
const router = useRouter();
const pathname = usePathname();
@ -119,14 +138,23 @@ export default function PlanCard({
} = useQuery({
queryKey: ["planDeletionPreview", planId],
queryFn: () => fetchPlanDeletionPreview(planId),
enabled: open,
enabled: deleteOpen,
});
/* -------- Delete mutation -------- */
const deleteMutation = useMutation({
mutationFn: () => confirmPlanDeletion(planId),
onSuccess: () => {
setOpen(false);
setDeleteOpen(false);
router.refresh();
},
});
/* -------- Set default mutation -------- */
const setDefaultMutation = useMutation({
mutationFn: () => setDefaultPlan(planId),
onSuccess: () => {
setSetDefaultOpen(false);
router.refresh();
},
});
@ -141,31 +169,51 @@ export default function PlanCard({
return (
<>
<div className="rounded-2xl border border-gray-200 bg-white shadow-sm overflow-hidden hover:shadow-md transition-shadow">
<div className="rounded-2xl border border-gray-200 bg-white shadow-sm overflow-hidden hover:shadow-md transition-shadow w-72 shrink-0">
{/* Header */}
<div className="flex items-start justify-between px-6 pt-5 pb-4 border-b border-gray-100">
<div className="flex items-start justify-between px-5 pt-5 pb-4 border-b border-gray-100">
<div>
<p className="text-[10px] font-bold uppercase tracking-widest text-gray-400 mb-1">Retrofit Plan</p>
<h3 className="text-base font-bold text-brandblue">
<h3 className="text-sm font-bold text-brandblue leading-snug">
{planName ?? "Unnamed Plan"}
</h3>
</div>
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-lg p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-400/40 transition"
aria-label="Delete plan"
>
<TrashIcon className="h-4 w-4" />
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="rounded-lg p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 focus:outline-none transition"
aria-label="Plan options"
>
<EllipsisVerticalIcon className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{!isDefault && (
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => setSetDefaultOpen(true)}
>
Set as Default
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer"
onSelect={() => setDeleteOpen(true)}
>
Delete Plan
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Body */}
<div className="px-6 py-4 flex flex-col gap-5">
<div className="px-5 py-4 flex flex-col gap-4">
{/* EPC progression */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<EpcBadge rating={currentEpcRating} label="Current" />
<div className="flex-1 flex items-center gap-1">
<div className="flex-1 h-px bg-gray-200" />
@ -176,33 +224,33 @@ export default function PlanCard({
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-3">
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-3 py-2.5">
<div className="flex items-center gap-1.5">
<BanknotesIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-medium">Est. Cost</span>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-2.5 py-2">
<div className="flex items-center gap-1">
<BanknotesIcon className="w-3 h-3 text-gray-400 shrink-0" />
<span className="text-[9px] uppercase tracking-wide text-gray-400 font-medium">Cost</span>
</div>
<span className="text-sm font-bold text-gray-800 tabular-nums">
<span className="text-xs font-bold text-gray-800 tabular-nums">
£{formatNumber(totalEstimatedCost)}
</span>
</div>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-3 py-2.5">
<div className="flex items-center gap-1.5">
<ArrowTrendingUpIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-medium">SAP Gain</span>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-2.5 py-2">
<div className="flex items-center gap-1">
<ArrowTrendingUpIcon className="w-3 h-3 text-gray-400 shrink-0" />
<span className="text-[9px] uppercase tracking-wide text-gray-400 font-medium">SAP</span>
</div>
<span className="text-sm font-bold text-gray-800 tabular-nums">
+{sapImprovement} pts
<span className="text-xs font-bold text-gray-800 tabular-nums">
+{sapImprovement}
</span>
</div>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-3 py-2.5">
<div className="flex items-center gap-1.5">
<CalendarIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-medium">Created</span>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-2.5 py-2">
<div className="flex items-center gap-1">
<CalendarIcon className="w-3 h-3 text-gray-400 shrink-0" />
<span className="text-[9px] uppercase tracking-wide text-gray-400 font-medium">Date</span>
</div>
<span className="text-xs font-semibold text-gray-700">{createdDate}</span>
<span className="text-[10px] font-semibold text-gray-700">{createdDate}</span>
</div>
</div>
@ -218,8 +266,35 @@ export default function PlanCard({
</div>
</div>
{/* Set default confirmation dialog */}
<Dialog open={setDefaultOpen} onOpenChange={setSetDefaultOpen}>
<DialogContent className="max-w-sm">
<ModalHeader>
<DialogTitle>Set as default plan?</DialogTitle>
</ModalHeader>
<p className="text-sm text-gray-500">
<span className="font-semibold text-brandblue">{planName ?? "This plan"}</span> will become the default plan for this property. The current default plan will be moved to the secondary list.
</p>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setSetDefaultOpen(false)}
disabled={setDefaultMutation.isPending}
>
Cancel
</Button>
<Button
onClick={() => setDefaultMutation.mutate()}
disabled={setDefaultMutation.isPending}
>
{setDefaultMutation.isPending ? "Updating…" : "Set as Default"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete preview modal */}
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent className="max-w-lg">
<ModalHeader>
<DialogTitle className="text-red-600">Delete plan</DialogTitle>
@ -253,7 +328,7 @@ export default function PlanCard({
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setOpen(false)}
onClick={() => setDeleteOpen(false)}
disabled={deleteMutation.isPending}
>
Cancel

View file

@ -0,0 +1,319 @@
"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { useRouter, usePathname } from "next/navigation";
import {
ArrowRightIcon,
BanknotesIcon,
ArrowTrendingUpIcon,
CloudIcon,
WrenchScrewdriverIcon,
EllipsisVerticalIcon,
} from "@heroicons/react/24/outline";
import { useQuery } from "@tanstack/react-query";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/app/shadcn_components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/app/shadcn_components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/app/shadcn_components/ui/table";
import { Button } from "@/app/shadcn_components/ui/button";
import { getEpcColorClass, formatNumber } from "@/app/utils";
type DeletionPreviewRow = { table: string; count: number };
async function fetchPlanDeletionPreview(planId: string): Promise<DeletionPreviewRow[]> {
const res = await fetch(`/api/plan/${planId}/delete/preview`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error("Failed to load deletion preview");
return (await res.json()).preview;
}
async function confirmPlanDeletion(planId: string): Promise<void> {
const res = await fetch(`/api/plan/${planId}/delete/confirm`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: true }),
});
if (!res.ok) throw new Error("Failed to delete plan");
}
function getEpcHex(letter: string | null | undefined): string {
switch (letter?.toUpperCase()) {
case "A": return "#117d58";
case "B": return "#2da55c";
case "C": return "#8dbd40";
case "D": return "#f7cd14";
case "E": return "#f3a96a";
case "F": return "#ef8026";
case "G": return "#e41e3b";
default: return "#9ca3af";
}
}
function EpcBadge({ rating, label }: { rating: string; label: string }) {
const colorClass = getEpcColorClass(rating);
return (
<div className="flex flex-col items-center gap-1.5">
<span className="text-[10px] uppercase tracking-widest text-gray-400 font-bold">{label}</span>
<span className={`${colorClass} text-white text-2xl font-black w-12 h-12 rounded-xl flex items-center justify-center leading-none shadow-sm`}>
{rating}
</span>
</div>
);
}
export default function PlanHeroCard({
planId,
planName,
currentEpcRating,
expectedEpcRating,
totalEstimatedCost,
totalSapPoints,
co2Savings,
energyBillSavings,
createdAt,
}: {
planId: string;
planName: string | null;
currentEpcRating: string;
expectedEpcRating: string;
totalEstimatedCost: number;
totalSapPoints: number;
co2Savings: number | null;
energyBillSavings: number | null;
createdAt: Date;
}) {
const [deleteOpen, setDeleteOpen] = useState(false);
const router = useRouter();
const pathname = usePathname();
const sapImprovement = Math.round((totalSapPoints + Number.EPSILON) * 100) / 100;
const createdDate = new Date(createdAt).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
});
const currentHex = getEpcHex(currentEpcRating);
const expectedHex = getEpcHex(expectedEpcRating);
/* Delete preview query */
const { data: preview = [], isLoading: previewLoading, isError: previewError } = useQuery({
queryKey: ["planDeletionPreview", planId],
queryFn: () => fetchPlanDeletionPreview(planId),
enabled: deleteOpen,
});
/* Delete mutation */
const deleteMutation = useMutation({
mutationFn: () => confirmPlanDeletion(planId),
onSuccess: () => {
setDeleteOpen(false);
router.refresh();
},
});
return (
<>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="grid grid-cols-1 lg:grid-cols-12">
{/* Left: content */}
<div className="lg:col-span-8 p-8 md:p-10 flex flex-col gap-8">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div>
<span className="inline-flex items-center gap-1.5 bg-brandblue/10 text-brandblue text-[10px] font-bold uppercase tracking-widest px-2.5 py-1 rounded-full mb-3">
Default Plan
</span>
<h2 className="font-manrope font-extrabold text-3xl text-brandblue tracking-tight">
{planName ?? "Unnamed Plan"}
</h2>
<p className="text-sm text-gray-400 font-medium mt-1">Created {createdDate}</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="rounded-lg p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 focus:outline-none transition"
aria-label="Plan options"
>
<EllipsisVerticalIcon className="h-5 w-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer"
onSelect={() => setDeleteOpen(true)}
>
Delete Plan
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* EPC progression */}
<div className="space-y-4">
<div className="flex justify-between text-[10px] font-bold uppercase tracking-widest text-gray-400">
<span>Current Rating: {currentEpcRating}</span>
<span>Target Rating: {expectedEpcRating}</span>
</div>
<div className="relative h-3 w-full bg-gray-100 rounded-full overflow-hidden">
<div
className="absolute inset-y-0 left-0 rounded-full"
style={{
width: "100%",
background: `linear-gradient(to right, ${currentHex}, ${expectedHex})`,
}}
/>
</div>
<div className="flex items-center justify-between pt-1">
<div className="flex items-center gap-3">
<EpcBadge rating={currentEpcRating} label="Current" />
<ArrowRightIcon className="w-5 h-5 text-gray-300 shrink-0" />
<EpcBadge rating={expectedEpcRating} label="Expected" />
</div>
</div>
</div>
{/* Stat chips */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-4 py-3">
<div className="flex items-center gap-1.5">
<ArrowTrendingUpIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-bold">SAP Gain</span>
</div>
<span className="font-manrope text-lg font-black text-brandblue tabular-nums">
+{sapImprovement}
</span>
</div>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-4 py-3">
<div className="flex items-center gap-1.5">
<BanknotesIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-bold">Est. Cost</span>
</div>
<span className="font-manrope text-lg font-black text-brandblue tabular-nums">
£{formatNumber(totalEstimatedCost)}
</span>
</div>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-4 py-3">
<div className="flex items-center gap-1.5">
<CloudIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-bold">CO Saved</span>
</div>
<span className="font-manrope text-lg font-black text-brandblue tabular-nums">
{co2Savings != null ? `${co2Savings.toFixed(1)} t` : "—"}
</span>
</div>
<div className="flex flex-col gap-1 rounded-xl bg-gray-50 border border-gray-100 px-4 py-3">
<div className="flex items-center gap-1.5">
<BanknotesIcon className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className="text-[10px] uppercase tracking-wide text-gray-400 font-bold">Bill Saving</span>
</div>
<span className="font-manrope text-lg font-black text-brandblue tabular-nums">
{energyBillSavings != null ? `£${formatNumber(energyBillSavings)}/yr` : "—"}
</span>
</div>
</div>
{/* CTA */}
<div>
<button
type="button"
onClick={() => router.push(`${pathname}/${planId}`)}
className="inline-flex items-center gap-2 bg-brandblue text-white font-manrope font-bold px-8 py-3.5 rounded-xl hover:bg-hoverblue transition-colors"
>
View Plan
<ArrowRightIcon className="w-4 h-4" />
</button>
</div>
</div>
{/* Right: visual panel */}
<div className="lg:col-span-4 bg-gradient-to-br from-brandblue to-hoverblue hidden lg:flex items-center justify-center p-12 relative overflow-hidden">
<div className="absolute inset-0 opacity-10">
<div className="absolute top-8 right-8 w-40 h-40 rounded-full bg-white" />
<div className="absolute bottom-4 left-4 w-24 h-24 rounded-full bg-white" />
</div>
<div className="relative z-10 flex flex-col items-center gap-4 text-white text-center">
<div className="w-20 h-20 rounded-2xl bg-white/10 border border-white/20 flex items-center justify-center">
<WrenchScrewdriverIcon className="w-10 h-10 text-white/80" />
</div>
<p className="font-manrope font-bold text-lg text-white/90 leading-snug max-w-[180px]">
Curated retrofit strategy
</p>
<p className="text-xs text-white/60 font-medium leading-relaxed max-w-[160px]">
Engineered to maximise energy performance and long-term value.
</p>
</div>
</div>
</div>
</div>
{/* Delete modal */}
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="text-red-600">Delete plan</DialogTitle>
</DialogHeader>
{previewLoading ? (
<p className="text-sm text-gray-500">Loading deletion preview</p>
) : previewError ? (
<p className="text-sm text-red-600">Failed to load deletion preview</p>
) : (
<div className="rounded-md border border-gray-200">
<Table>
<TableHeader>
<TableRow>
<TableHead>Table</TableHead>
<TableHead className="text-right">Rows deleted</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{preview.map((row) => (
<TableRow key={row.table}>
<TableCell className="font-mono text-sm">{row.table}</TableCell>
<TableCell className="text-right font-semibold">{row.count}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<DialogFooter className="gap-2">
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleteMutation.isPending}>
Cancel
</Button>
<Button variant="destructive" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending}>
{deleteMutation.isPending ? "Deleting…" : "Delete plan"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -1,48 +1,119 @@
import { getPlans, getPropertyMeta } from "../utils";
import { sapToEpc } from "@/app/utils";
import PlanCard from "./PlanCard";
import PlanHeroCard from "./PlanHeroCard";
import { WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
export default async function RecommendationPlans(props: {
params: Promise<{ slug: string; propertyId: string }>;
}) {
const params = await props.params;
const propertyMeta = await getPropertyMeta(params.propertyId);
const plans = await getPlans(params.propertyId);
const [propertyMeta, plans] = await Promise.all([
getPropertyMeta(params.propertyId),
getPlans(params.propertyId),
]);
if (plans.length === 0) {
return (
<div className="max-w-7xl mx-auto py-10 space-y-8">
<header>
<p className="font-manrope text-xs font-bold text-brandmidblue uppercase tracking-widest mb-2">
Retrofit Strategy
</p>
<h1 className="font-manrope font-extrabold text-4xl text-brandblue tracking-tight">
Retrofit Plans
</h1>
</header>
<div className="bg-white rounded-2xl border border-dashed border-gray-200 p-16 flex flex-col items-center justify-center text-center gap-4">
<div className="w-14 h-14 rounded-2xl bg-gray-50 border border-gray-200 flex items-center justify-center">
<WrenchScrewdriverIcon className="w-7 h-7 text-gray-400" />
</div>
<div>
<p className="font-manrope font-bold text-brandblue text-lg">No plans yet</p>
<p className="text-sm text-gray-400 mt-1">Retrofit plans will appear here once they have been generated.</p>
</div>
</div>
</div>
);
}
function getPlanMetrics(plan: (typeof plans)[number]) {
const totalEstimatedCost = plan.costOfWorks ?? 0;
const totalSapPoints =
(plan.postSapPoints ?? propertyMeta.currentSapPoints) -
propertyMeta.currentSapPoints;
const expectedSapPoints = Math.min(
propertyMeta.currentSapPoints + totalSapPoints,
100
);
const expectedEpcRating = sapToEpc(expectedSapPoints);
return { totalEstimatedCost, totalSapPoints, expectedEpcRating };
}
/* Identify the default plan (fallback: first plan) */
const defaultPlan = plans.find((p) => p.isDefault) ?? plans[0];
const otherPlans = plans.filter((p) => p.id !== defaultPlan.id);
const defaultMetrics = getPlanMetrics(defaultPlan);
return (
<div className="leading-loose tracking-wider">
<div className="flex py-8 text-lg">Retrofit Plans</div>
<div className="max-w-7xl mx-auto py-10 space-y-10">
<div>
{plans.map((plan) => {
const totalEstimatedCost = plan.costOfWorks || 0;
{/* Page header */}
<header>
<p className="font-manrope text-xs font-bold text-brandmidblue uppercase tracking-widest mb-2">
Retrofit Strategy
</p>
<h1 className="font-manrope font-extrabold text-4xl text-brandblue tracking-tight">
Retrofit Plans
</h1>
</header>
const totalSapPoints =
(plan.postSapPoints || propertyMeta.currentSapPoints) -
propertyMeta.currentSapPoints;
{/* Hero — default plan */}
<PlanHeroCard
planId={String(defaultPlan.id)}
planName={defaultPlan.name}
currentEpcRating={propertyMeta.currentEpcRating}
expectedEpcRating={defaultMetrics.expectedEpcRating}
totalEstimatedCost={defaultMetrics.totalEstimatedCost}
totalSapPoints={defaultMetrics.totalSapPoints}
co2Savings={defaultPlan.co2Savings ?? null}
energyBillSavings={defaultPlan.energyBillSavings ?? null}
createdAt={defaultPlan.createdAt}
/>
const expectedSapPoints = Math.min(
propertyMeta.currentSapPoints + totalSapPoints,
100
);
{/* Secondary plans */}
{otherPlans.length > 0 && (
<section>
<div className="flex items-center gap-3 mb-6">
<p className="font-manrope text-xs font-bold text-brandmidblue uppercase tracking-widest">
Other Plans
</p>
<span className="bg-gray-100 text-gray-500 text-[10px] font-bold px-2 py-0.5 rounded-full">
{otherPlans.length}
</span>
</div>
const expectedEpcRating = sapToEpc(expectedSapPoints);
return (
<div key={plan.id} className="mb-4">
<PlanCard
expectedEpcRating={expectedEpcRating}
currentEpcRating={propertyMeta.currentEpcRating}
createdAt={plan.createdAt}
totalEstimatedCost={totalEstimatedCost}
totalSapPoints={totalSapPoints}
planName={plan.name}
planId={String(plan.id)}
/>
</div>
);
})}
</div>
<div className="flex gap-5 overflow-x-auto pb-4 -mx-1 px-1">
{otherPlans.map((plan) => {
const { totalEstimatedCost, totalSapPoints, expectedEpcRating } = getPlanMetrics(plan);
return (
<PlanCard
key={String(plan.id)}
expectedEpcRating={expectedEpcRating}
currentEpcRating={propertyMeta.currentEpcRating}
createdAt={plan.createdAt}
totalEstimatedCost={totalEstimatedCost}
totalSapPoints={totalSapPoints}
planName={plan.name}
planId={String(plan.id)}
isDefault={false}
/>
);
})}
</div>
</section>
)}
</div>
);
}