delete plan works

This commit is contained in:
Jun-te Kim 2026-01-15 13:38:31 +00:00
parent b06a78b110
commit de947062bb
6 changed files with 327 additions and 82 deletions

View file

@ -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,

View file

@ -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 });
}

View file

@ -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<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");
const json = await res.json();
return 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) {
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 (
<Card className="relative flex items-start">
{/* Delete button (top-right, subtle) */}
<button
type="button"
onClick={() =>
console.log("going to delete soon", { planId, isDefault })
}
className="
absolute top-3 right-3
rounded-md 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"
title="Delete plan"
>
<TrashIcon className="h-4 w-4" />
</button>
<>
<Card className="relative flex items-start">
{/* Delete button */}
<button
type="button"
onClick={() => setOpen(true)}
className="
absolute top-3 right-3
rounded-md 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"
title="Delete plan"
>
<TrashIcon className="h-4 w-4" />
</button>
{/* EPC card — unchanged */}
<div className="flex-none w-1/5">
<EpcCard
epcRating={expectedEpcRating}
fullMargin={true}
expected={true}
/>
</div>
{/* EPC */}
<div className="flex-none w-1/5">
<EpcCard epcRating={expectedEpcRating} fullMargin expected />
</div>
{/* Main content */}
<div className="flex-grow pl-4 flex flex-col justify-between">
<CardHeader className="flex justify-end items-start">
{planName && (
<div className="text-lg font-bold mb-2 text-gray-900">
{planName}
{/* Content */}
<div className="flex-grow pl-4 flex flex-col justify-between">
<CardHeader className="flex justify-end items-start">
{planName && (
<div className="text-lg font-bold mb-2 text-gray-900">
{planName}
</div>
)}
</CardHeader>
<CardContent>
<div className="flex justify-between mb-2">
<span>Total cost:</span>
<span>£{formatNumber(totalEstimatedCost)}</span>
</div>
<div className="flex justify-between">
<span>Total SAP points:</span>
<span>
{Math.round((totalSapPoints + Number.EPSILON) * 100) / 100}
</span>
</div>
</CardContent>
</div>
{/* Right column */}
<div className="flex flex-col justify-end mr-2 self-stretch w-1/5">
<div className="flex flex-col items-end gap-2">
<GoToPlanButton planId={planId} />
</div>
</div>
</Card>
{/* ----------------------------------------
Delete preview modal
----------------------------------------- */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<ModalHeader>
<DialogTitle className="text-red-600">Delete plan</DialogTitle>
</ModalHeader>
{isLoading ? (
<p className="text-sm text-gray-500">Loading deletion preview</p>
) : isError ? (
<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>
)}
</CardHeader>
<CardContent>
<div className="flex justify-between mb-2">
<span>Total cost:</span>
<span>£{formatNumber(totalEstimatedCost)}</span>
</div>
<div className="flex justify-between">
<span>Total SAP points:</span>
<span>
{Math.round((totalSapPoints + Number.EPSILON) * 100) / 100}
</span>
</div>
</CardContent>
</div>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setOpen(false)}
disabled={deleteMutation.isPending}
>
Cancel
</Button>
{/* Right column */}
<div className="flex flex-col justify-end mr-2 self-stretch w-1/5">
<div className="flex flex-col items-end gap-2">
{/* <div className="text-xs text-gray-400">
Created {formatDateTime(createdAt)}
</div> */}
<GoToPlanButton planId={planId} />
</div>
</div>
</Card>
<Button
variant="destructive"
onClick={() => deleteMutation.mutate()}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting…" : "Delete plan"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View file

@ -37,7 +37,6 @@ export default async function RecommendationPlans(props: {
totalSapPoints={totalSapPoints}
planName={plan.name}
planId={String(plan.id)}
isDefault={plan.isDefault}
/>
</div>
);

View file

@ -121,7 +121,6 @@ export default function PropertyTable({
const [previewError, setPreviewError] = useState<string | null>(null);
const [deleteLoading, setDeleteLoading] = useState(false);
return (
<div className="flex justify-center">
<div className="grid grid-cols-11 w-full max-w-8xl">
@ -233,13 +232,13 @@ export default function PropertyTable({
Cancel
</Button>
<Button
{/* <Button
variant="destructive"
disabled={!deletePreview || previewLoading || deleteLoading}
onClick={handleConfirmDelete}
>
{deleteLoading ? "Deleting…" : "Confirm delete"}
</Button>
</Button> */}
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -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