From 0c0529b234b0624ec3660f4958c01ff1d5ef17da Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 8 Apr 2026 20:47:14 +0000 Subject: [PATCH] Updating the plans ui --- src/app/api/plan/[id]/set-default/route.ts | 39 +++ .../[propertyId]/plans/PlanCard.tsx | 147 ++++++-- .../[propertyId]/plans/PlanHeroCard.tsx | 319 ++++++++++++++++++ .../[propertyId]/plans/page.tsx | 133 ++++++-- 4 files changed, 571 insertions(+), 67 deletions(-) create mode 100644 src/app/api/plan/[id]/set-default/route.ts create mode 100644 src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanHeroCard.tsx diff --git a/src/app/api/plan/[id]/set-default/route.ts b/src/app/api/plan/[id]/set-default/route.ts new file mode 100644 index 00000000..ee33489b --- /dev/null +++ b/src/app/api/plan/[id]/set-default/route.ts @@ -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 }); +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx index 5c0dce2f..f40fa076 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx @@ -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 { } } +async function setDefaultPlan(planId: string): Promise { + 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 ( <> -
+
{/* Header */} -
+

Retrofit Plan

-

+

{planName ?? "Unnamed Plan"}

- + + + + + + + {!isDefault && ( + setSetDefaultOpen(true)} + > + Set as Default + + )} + setDeleteOpen(true)} + > + Delete Plan + + +
{/* Body */} -
+
{/* EPC progression */} -
+
@@ -176,33 +224,33 @@ export default function PlanCard({
{/* Stats row */} -
-
-
- - Est. Cost +
+
+
+ + Cost
- + £{formatNumber(totalEstimatedCost)}
-
-
- - SAP Gain +
+
+ + SAP
- - +{sapImprovement} pts + + +{sapImprovement}
-
-
- - Created +
+
+ + Date
- {createdDate} + {createdDate}
@@ -218,8 +266,35 @@ export default function PlanCard({
+ {/* Set default confirmation dialog */} + + + + Set as default plan? + +

+ {planName ?? "This plan"} will become the default plan for this property. The current default plan will be moved to the secondary list. +

+ + + + +
+
+ {/* Delete preview modal */} - + Delete plan @@ -253,7 +328,7 @@ export default function PlanCard({ + + + setDeleteOpen(true)} + > + Delete Plan + + + +
+ + {/* EPC progression */} +
+
+ Current Rating: {currentEpcRating} + Target Rating: {expectedEpcRating} +
+
+
+
+
+
+ + + +
+
+
+ + {/* Stat chips */} +
+
+
+ + SAP Gain +
+ + +{sapImprovement} + +
+ +
+
+ + Est. Cost +
+ + £{formatNumber(totalEstimatedCost)} + +
+ +
+
+ + CO₂ Saved +
+ + {co2Savings != null ? `${co2Savings.toFixed(1)} t` : "—"} + +
+ +
+
+ + Bill Saving +
+ + {energyBillSavings != null ? `£${formatNumber(energyBillSavings)}/yr` : "—"} + +
+
+ + {/* CTA */} +
+ +
+
+ + {/* Right: visual panel */} +
+
+
+
+
+
+
+ +
+

+ Curated retrofit strategy +

+

+ Engineered to maximise energy performance and long-term value. +

+
+
+
+
+ + {/* Delete modal */} + + + + Delete plan + + {previewLoading ? ( +

Loading deletion preview…

+ ) : previewError ? ( +

Failed to load deletion preview

+ ) : ( +
+ + + + Table + Rows deleted + + + + {preview.map((row) => ( + + {row.table} + {row.count} + + ))} + +
+
+ )} + + + + +
+
+ + ); +} diff --git a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx index 32c283ab..16896edc 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx @@ -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 ( +
+
+

+ Retrofit Strategy +

+

+ Retrofit Plans +

+
+
+
+ +
+
+

No plans yet

+

Retrofit plans will appear here once they have been generated.

+
+
+
+ ); + } + + 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 ( -
-
Retrofit Plans
+
-
- {plans.map((plan) => { - const totalEstimatedCost = plan.costOfWorks || 0; + {/* Page header */} +
+

+ Retrofit Strategy +

+

+ Retrofit Plans +

+
- const totalSapPoints = - (plan.postSapPoints || propertyMeta.currentSapPoints) - - propertyMeta.currentSapPoints; + {/* Hero — default plan */} + - const expectedSapPoints = Math.min( - propertyMeta.currentSapPoints + totalSapPoints, - 100 - ); + {/* Secondary plans */} + {otherPlans.length > 0 && ( +
+
+

+ Other Plans +

+ + {otherPlans.length} + +
- const expectedEpcRating = sapToEpc(expectedSapPoints); - - return ( -
- -
- ); - })} -
+
+ {otherPlans.map((plan) => { + const { totalEstimatedCost, totalSapPoints, expectedEpcRating } = getPlanMetrics(plan); + return ( + + ); + })} +
+ + )}
); }