mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
delete plan works
This commit is contained in:
parent
b06a78b110
commit
de947062bb
6 changed files with 327 additions and 82 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export default async function RecommendationPlans(props: {
|
|||
totalSapPoints={totalSapPoints}
|
||||
planName={plan.name}
|
||||
planId={String(plan.id)}
|
||||
isDefault={plan.isDefault}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue