From de947062bba3d87d6167c8721e17ff270d678fa5 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 15 Jan 2026 13:38:31 +0000 Subject: [PATCH] delete plan works --- src/app/api/plan/[id]/delete/confirm/route.ts | 10 +- src/app/api/plan/[id]/delete/preview/route.ts | 6 +- .../[propertyId]/plans/PlanCard.tsx | 274 +++++++++++++----- .../[propertyId]/plans/page.tsx | 1 - .../[slug]/components/PropertyTable.tsx | 5 +- src/lib/services/propertyDeletion.ts | 113 ++++++++ 6 files changed, 327 insertions(+), 82 deletions(-) diff --git a/src/app/api/plan/[id]/delete/confirm/route.ts b/src/app/api/plan/[id]/delete/confirm/route.ts index 39191ca1..c5a4488d 100644 --- a/src/app/api/plan/[id]/delete/confirm/route.ts +++ b/src/app/api/plan/[id]/delete/confirm/route.ts @@ -1,15 +1,15 @@ import { NextResponse } from "next/server"; -import { deleteProperty } from "@/lib/services/propertyDeletion"; +import { deletePlan } from "@/lib/services/propertyDeletion"; export async function POST( req: Request, context: { params: Promise<{ id: string }> } ) { const { id } = await context.params; - const propertyId = Number(id); + const planId = Number(id); - if (Number.isNaN(propertyId)) { - return NextResponse.json({ error: "Invalid property id" }, { status: 400 }); + if (Number.isNaN(planId)) { + return NextResponse.json({ error: "Invalid plan id" }, { status: 400 }); } const { confirm } = await req.json(); @@ -21,7 +21,7 @@ export async function POST( ); } - await deleteProperty(propertyId); + await deletePlan(planId); return NextResponse.json({ success: true, diff --git a/src/app/api/plan/[id]/delete/preview/route.ts b/src/app/api/plan/[id]/delete/preview/route.ts index 108b65b2..1e17597b 100644 --- a/src/app/api/plan/[id]/delete/preview/route.ts +++ b/src/app/api/plan/[id]/delete/preview/route.ts @@ -1,13 +1,13 @@ import { NextResponse } from "next/server"; -import { previewPropertyDeletion } from "@/lib/services/propertyDeletion"; +import { previewPlanDeletion } from "@/lib/services/propertyDeletion"; export async function POST( _req: Request, context: { params: Promise<{ id: string }> } ) { const { id } = await context.params; // 👈 THIS IS THE FIX - const propertyId = Number(id); - const preview = await previewPropertyDeletion(propertyId); + const planId = Number(id); + const preview = await previewPlanDeletion(planId); return NextResponse.json({ preview }); } 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 3ed8da6a..f6aa2c6e 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/PlanCard.tsx @@ -1,15 +1,76 @@ "use client"; +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TrashIcon } from "@heroicons/react/24/outline"; -import EpcCard from "@/app/components/building-passport/EpcCard"; -import { - Card, - CardContent, - CardHeader, -} from "@/app/shadcn_components/ui/card"; -import GoToPlanButton from "@/app/components/building-passport/GoToPlanButton"; -import { formatDateTime, formatNumber } from "@/app/utils"; +import { useRouter } from "next/navigation"; +import EpcCard from "@/app/components/building-passport/EpcCard"; +import GoToPlanButton from "@/app/components/building-passport/GoToPlanButton"; + +import { Card, CardContent, CardHeader } from "@/app/shadcn_components/ui/card"; + +import { + Dialog, + DialogContent, + DialogHeader as ModalHeader, + 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 { formatNumber } from "@/app/utils"; + +/* ---------------------------------------- + Types +----------------------------------------- */ +type DeletionPreviewRow = { + table: string; + count: number; +}; + +/* ---------------------------------------- + Fetchers +----------------------------------------- */ +async function fetchPlanDeletionPreview( + planId: string +): Promise { + 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"); + + const json = await res.json(); + return json.preview; +} + +async function confirmPlanDeletion(planId: string): Promise { + const res = await fetch(`/api/plan/${planId}/delete/confirm`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ confirm: true }), + }); + + if (!res.ok) { + const msg = await res.text().catch(() => ""); + throw new Error(msg || "Failed to delete plan"); + } +} + +/* ---------------------------------------- + Component +----------------------------------------- */ export default function PlanCard({ expectedEpcRating, createdAt, @@ -17,7 +78,6 @@ export default function PlanCard({ totalSapPoints, planName, planId, - isDefault, }: { expectedEpcRating: string; createdAt: Date; @@ -25,74 +85,148 @@ export default function PlanCard({ totalSapPoints: number; planName: string | null; planId: string; - isDefault: boolean; }) { + const [open, setOpen] = useState(false); + const queryClient = useQueryClient(); + const router = useRouter(); + + /* -------- Preview query -------- */ + const { + data: preview = [], + isLoading, + isError, + } = useQuery({ + queryKey: ["planDeletionPreview", planId], + queryFn: () => fetchPlanDeletionPreview(planId), + enabled: open, // only fetch when modal opens + }); + + /* -------- Delete mutation -------- */ + const deleteMutation = useMutation({ + mutationFn: () => confirmPlanDeletion(planId), + onSuccess: () => { + setOpen(false); + router.refresh(); + }, + }); + return ( - - {/* Delete button (top-right, subtle) */} - + <> + + {/* Delete button */} + - {/* EPC card — unchanged */} -
- -
+ {/* EPC */} +
+ +
- {/* Main content */} -
- - {planName && ( -
- {planName} + {/* Content */} +
+ + {planName && ( +
+ {planName} +
+ )} +
+ + +
+ Total cost: + £{formatNumber(totalEstimatedCost)} +
+
+ Total SAP points: + + {Math.round((totalSapPoints + Number.EPSILON) * 100) / 100} + +
+
+
+ + {/* Right column */} +
+
+ +
+
+ + + {/* ---------------------------------------- + Delete preview modal + ----------------------------------------- */} + + + + Delete plan + + + {isLoading ? ( +

Loading deletion preview…

+ ) : isError ? ( +

+ Failed to load deletion preview +

+ ) : ( +
+ + + + Table + Rows deleted + + + + {preview.map((row) => ( + + + {row.table} + + + {row.count} + + + ))} + +
)} - - -
- Total cost: - £{formatNumber(totalEstimatedCost)} -
-
- Total SAP points: - - {Math.round((totalSapPoints + Number.EPSILON) * 100) / 100} - -
-
-
+ + - {/* Right column */} -
-
- {/*
- Created {formatDateTime(createdAt)} -
*/} - - -
-
- - + +
+ + + ); } 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 d6f6a85b..d1549b44 100644 --- a/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx +++ b/src/app/portfolio/[slug]/building-passport/[propertyId]/plans/page.tsx @@ -37,7 +37,6 @@ export default async function RecommendationPlans(props: { totalSapPoints={totalSapPoints} planName={plan.name} planId={String(plan.id)} - isDefault={plan.isDefault} />
); diff --git a/src/app/portfolio/[slug]/components/PropertyTable.tsx b/src/app/portfolio/[slug]/components/PropertyTable.tsx index 455096e3..c77e02a9 100644 --- a/src/app/portfolio/[slug]/components/PropertyTable.tsx +++ b/src/app/portfolio/[slug]/components/PropertyTable.tsx @@ -121,7 +121,6 @@ export default function PropertyTable({ const [previewError, setPreviewError] = useState(null); const [deleteLoading, setDeleteLoading] = useState(false); - return (
@@ -233,13 +232,13 @@ export default function PropertyTable({ Cancel - + */} diff --git a/src/lib/services/propertyDeletion.ts b/src/lib/services/propertyDeletion.ts index 8d4757b9..a591c1aa 100644 --- a/src/lib/services/propertyDeletion.ts +++ b/src/lib/services/propertyDeletion.ts @@ -173,6 +173,119 @@ export async function deleteProperty(propertyId: number) { `); }); } +export async function previewPlanDeletion(planId: number) { + const result = await db.execute(sql` + -- --------------------------------- + -- Recommendation materials + -- --------------------------------- + SELECT 'recommendation_materials' AS table, COUNT(*)::int AS count + FROM recommendation_materials rm + JOIN recommendation r ON rm.recommendation_id = r.id + JOIN plan_recommendations pr ON pr.recommendation_id = r.id + WHERE pr.plan_id = ${planId} + + UNION ALL + -- --------------------------------- + -- Recommendations + -- --------------------------------- + SELECT 'recommendation', COUNT(*)::int + FROM recommendation r + JOIN plan_recommendations pr ON pr.recommendation_id = r.id + WHERE pr.plan_id = ${planId} + + UNION ALL + -- --------------------------------- + -- Plan recommendations + -- --------------------------------- + SELECT 'plan_recommendations', COUNT(*)::int + FROM plan_recommendations + WHERE plan_id = ${planId} + + UNION ALL + -- --------------------------------- + -- Funding chain + -- --------------------------------- + SELECT 'funding_package_measures', COUNT(*)::int + FROM funding_package_measures fpm + JOIN funding_package fp ON fpm.funding_package_id = fp.id + WHERE fp.plan_id = ${planId} + + UNION ALL + SELECT 'funding_package', COUNT(*)::int + FROM funding_package + WHERE plan_id = ${planId} + + UNION ALL + -- --------------------------------- + -- Root + -- --------------------------------- + SELECT 'plan', COUNT(*)::int + FROM plan + WHERE id = ${planId}; + `); + + return result.rows; +} +export async function deletePlan(planId: number) { + await db.transaction(async (tx) => { + // --------------------------------- + // Recommendation materials (LEAF) + // --------------------------------- + await tx.execute(sql` + DELETE FROM recommendation_materials rm + USING recommendation r, plan_recommendations pr + WHERE rm.recommendation_id = r.id + AND r.id = pr.recommendation_id + AND pr.plan_id = ${planId}; + `); + + // --------------------------------- + // Plan recommendations (FK OWNER) + // --------------------------------- + await tx.execute(sql` + DELETE FROM plan_recommendations + WHERE plan_id = ${planId}; + `); + + // --------------------------------- + // Recommendations (NOW SAFE) + // --------------------------------- + await tx.execute(sql` + DELETE FROM recommendation r + WHERE r.id NOT IN ( + SELECT recommendation_id FROM plan_recommendations + ) + AND r.id IN ( + SELECT recommendation_id + FROM plan_recommendations + WHERE plan_id = ${planId} + ); + `); + + // --------------------------------- + // Funding chain + // --------------------------------- + await tx.execute(sql` + DELETE FROM funding_package_measures fpm + USING funding_package fp + WHERE fpm.funding_package_id = fp.id + AND fp.plan_id = ${planId}; + `); + + await tx.execute(sql` + DELETE FROM funding_package + WHERE plan_id = ${planId}; + `); + + // --------------------------------- + // Root (LAST) + // --------------------------------- + await tx.execute(sql` + DELETE FROM plan + WHERE id = ${planId}; + `); + }); +} // Find All foregin keys used for 'property' table with id via this command // SELECT