This commit is contained in:
Jun-te Kim 2026-01-07 16:25:57 +00:00
parent 8f78f64de6
commit 25b826c42c
3 changed files with 155 additions and 87 deletions

View file

@ -281,18 +281,28 @@ export default function PortfolioSettings({
setIsDeleteModalOpen(true);
}
function handleDeleteConfirmation() {
if (deleteConfirmationByName === portfolioSettingsData.name) {
mutateDelete({
async function handleDeleteConfirmation() {
if (deleteConfirmationByName !== portfolioSettingsData.name) {
console.warn("Delete confirmation name does not match");
return;
}
try {
console.log("[DELETE] starting delete mutation");
await mutateDelete({
userId,
portfolioId,
});
console.log("succesfully called the mututate function");
console.log("[DELETE] mutation completed successfully");
// Refresh table / page data
router.refresh();
} catch (err) {
console.error("[DELETE] mutation failed", err);
}
}
// RENAMING FUNCTIONS
// Change NAME functionality - changing state
function handlePortfolioNameChange(e: React.ChangeEvent<HTMLInputElement>) {
@ -460,7 +470,7 @@ export default function PortfolioSettings({
</TableBody>
</Table>
</div>
<UsersPermissionsCard portfolioId={portfolioId}/>
<UsersPermissionsCard portfolioId={portfolioId} />
<div className="rounded-md border border-red-500 mt-2">
<Table>
<TableHead className="text-lg text-brandblue">Danger Zone:</TableHead>

View file

@ -1,6 +1,7 @@
"use client";
import { useState, useMemo, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useProperties } from "./useProperties";
import DataTable from "./dataTable";
import PropertyFilters, { PropertyFilterValues } from "./PropertyFilters";
@ -86,6 +87,8 @@ export default function PropertyTable({
}: {
portfolioId: string;
}) {
const router = useRouter();
const [filters, setFilters] = useState<PropertyFilterValues>({
address: "",
postcode: "",
@ -101,6 +104,7 @@ export default function PropertyTable({
isLoading,
isFetching,
isError,
refetch,
} = useProperties({
portfolioId,
filters: parsedFilters,
@ -123,8 +127,6 @@ export default function PropertyTable({
useEffect(() => {
if (!deletePropertyId) return;
console.log("[PREVIEW] fetching delete preview for:", deletePropertyId);
setPreviewLoading(true);
setPreviewError(null);
setDeletePreview(null);
@ -133,50 +135,37 @@ export default function PropertyTable({
method: "POST",
})
.then(async (res) => {
if (!res.ok) {
throw new Error("Failed to fetch delete preview");
}
if (!res.ok) throw new Error("Failed to fetch delete preview");
return res.json();
})
.then((data) => {
console.log("[PREVIEW] result:", data.preview);
setDeletePreview(data.preview);
})
.catch((err) => {
console.error("[PREVIEW] error:", err);
setPreviewError(err.message);
})
.finally(() => {
setPreviewLoading(false);
});
.then((data) => setDeletePreview(data.preview))
.catch((err) => setPreviewError(err.message))
.finally(() => setPreviewLoading(false));
}, [deletePropertyId]);
/* ----------------------------------------
Confirm delete
----------------------------------------- */
const handleConfirmDelete = async () => {
if (!deletePropertyId) return;
setDeleteLoading(true);
try {
console.log("[CONFIRM DELETE] sending request…");
await fetch(`/api/properties/${deletePropertyId}/delete/confirm`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: true }),
});
const res = await fetch(
`/api/properties/${deletePropertyId}/delete/confirm`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: true }),
}
);
const data = await res.json();
console.log("[CONFIRM DELETE] response:", data);
// TEMP: just close modal after backend confirms
// Close modal
setDeletePropertyId(null);
setDeletePreview(null);
// ✅ THIS FIXES THE GHOST ROW
await refetch();
} catch (err) {
console.error("[CONFIRM DELETE] error:", err);
console.error("[DELETE] failed", err);
} finally {
setDeleteLoading(false);
}
@ -186,24 +175,20 @@ export default function PropertyTable({
<div className="flex justify-center">
<div className="grid grid-cols-11 w-full max-w-8xl">
<div className="col-span-11 bg-white rounded-md border">
{/* Filters */}
<PropertyFilters onApply={setFilters} />
{/* Loading bar */}
{isFetching && (
<div className="h-1 w-full bg-gray-100 overflow-hidden">
<div className="h-full w-1/3 bg-black animate-[loading_1.2s_infinite]" />
</div>
)}
{/* Filter info */}
{hasActiveFilters && !isFetching && (
<div className="px-4 py-2 text-xs text-gray-500 border-b">
Filters applied ({parsedFilters.length})
</div>
)}
{/* Content */}
{isLoading ? (
<div className="p-6 text-gray-400">Loading properties</div>
) : isError ? (
@ -231,10 +216,7 @@ export default function PropertyTable({
<DataTable
data={data}
columns={columns}
onDeleteProperty={(propertyId) => {
console.log("[META] onDeleteProperty fired:", propertyId);
setDeletePropertyId(propertyId);
}}
onDeleteProperty={(id) => setDeletePropertyId(id)}
/>
)}
</div>
@ -263,7 +245,6 @@ export default function PropertyTable({
</DialogDescription>
</DialogHeader>
{/* Loading */}
{previewLoading && (
<div className="flex items-center gap-3 py-6">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-muted border-t-foreground" />
@ -273,12 +254,10 @@ export default function PropertyTable({
</div>
)}
{/* Error */}
{previewError && (
<p className="text-sm text-red-600">{previewError}</p>
)}
{/* Preview result */}
{deletePreview && !previewLoading && (
<ul className="space-y-1 text-sm">
{deletePreview.map((row) => (
@ -302,19 +281,13 @@ export default function PropertyTable({
>
Cancel
</Button>
<Button
variant="destructive"
disabled={!deletePreview || previewLoading || deleteLoading}
onClick={handleConfirmDelete}
>
{deleteLoading ? (
<span className="flex items-center gap-2">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Deleting
</span>
) : (
"Confirm delete"
)}
{deleteLoading ? "Deleting…" : "Confirm delete"}
</Button>
</DialogFooter>
</DialogContent>

View file

@ -5,17 +5,22 @@ export async function previewPropertyDeletion(propertyId: number) {
const id = sql`${propertyId}`;
const result = await db.execute(sql`
-- ---------------------------------
-- Recommendation chain
-- ---------------------------------
SELECT 'recommendation_materials' AS table, COUNT(*)::int AS count
FROM recommendation_materials rm
JOIN recommendation r ON rm.recommendation_id = r.id
WHERE r.property_id = ${id}
UNION ALL
SELECT 'plan_recommendations', COUNT(*)
FROM plan_recommendations pr
JOIN plan p ON pr.plan_id = p.id
WHERE p.property_id = ${id}
SELECT 'recommendation', COUNT(*)
FROM recommendation
WHERE property_id = ${id}
-- ---------------------------------
-- Funding / plan chain
-- ---------------------------------
UNION ALL
SELECT 'funding_package_measures', COUNT(*)
FROM funding_package_measures fpm
@ -24,20 +29,53 @@ export async function previewPropertyDeletion(propertyId: number) {
WHERE p.property_id = ${id}
UNION ALL
SELECT 'inspections', COUNT(*)
FROM inspections
WHERE property_id = ${id}
SELECT 'funding_package', COUNT(*)
FROM funding_package fp
JOIN plan p ON fp.plan_id = p.id
WHERE p.property_id = ${id}
UNION ALL
SELECT 'recommendation', COUNT(*)
FROM recommendation
WHERE property_id = ${id}
SELECT 'plan_recommendations', COUNT(*)
FROM plan_recommendations pr
JOIN plan p ON pr.plan_id = p.id
WHERE p.property_id = ${id}
UNION ALL
SELECT 'plan', COUNT(*)
FROM plan
WHERE property_id = ${id}
-- ---------------------------------
-- Property direct children
-- ---------------------------------
UNION ALL
SELECT 'property_details_epc', COUNT(*)
FROM property_details_epc
WHERE property_id = ${id}
UNION ALL
SELECT 'property_targets', COUNT(*)
FROM property_targets
WHERE property_id = ${id}
UNION ALL
SELECT 'property_status_tracker', COUNT(*)
FROM property_status_tracker
WHERE property_id = ${id}
UNION ALL
SELECT 'inspections', COUNT(*)
FROM inspections
WHERE property_id = ${id}
UNION ALL
SELECT 'files_from_surveyor', COUNT(*)
FROM files_from_surveyor
WHERE property_id = ${id}
-- ---------------------------------
-- Root
-- ---------------------------------
UNION ALL
SELECT 'property', COUNT(*)
FROM property
@ -49,6 +87,47 @@ export async function previewPropertyDeletion(propertyId: number) {
export async function deleteProperty(propertyId: number) {
await db.transaction(async (tx) => {
// -----------------------------
// LEAF TABLES FIRST
// -----------------------------
await tx.execute(sql`
DELETE FROM funding_package_measures fpm
USING funding_package fp, plan p
WHERE fpm.funding_package_id = fp.id
AND fp.plan_id = p.id
AND p.property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM recommendation_materials rm
USING recommendation r
WHERE rm.recommendation_id = r.id
AND r.property_id = ${propertyId};
`);
// -----------------------------
// PLAN CHILDREN
// -----------------------------
await tx.execute(sql`
DELETE FROM funding_package fp
USING plan p
WHERE fp.plan_id = p.id
AND p.property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM plan_recommendations pr
USING plan p
WHERE pr.plan_id = p.id
AND p.property_id = ${propertyId};
`);
// -----------------------------
// PROPERTY DIRECT CHILDREN
// -----------------------------
await tx.execute(sql`
DELETE FROM property_details_epc
WHERE property_id = ${propertyId};
@ -60,25 +139,8 @@ export async function deleteProperty(propertyId: number) {
`);
await tx.execute(sql`
DELETE FROM recommendation_materials rm
USING recommendation r
WHERE rm.recommendation_id = r.id
AND r.property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM plan_recommendations pr
USING plan p
WHERE pr.plan_id = p.id
AND p.property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM funding_package_measures fpm
USING funding_package fp, plan p
WHERE fpm.funding_package_id = fp.id
AND fp.plan_id = p.id
AND p.property_id = ${propertyId};
DELETE FROM property_status_tracker
WHERE property_id = ${propertyId};
`);
await tx.execute(sql`
@ -86,6 +148,11 @@ export async function deleteProperty(propertyId: number) {
WHERE property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM files_from_surveyor
WHERE property_id = ${propertyId};
`);
await tx.execute(sql`
DELETE FROM recommendation
WHERE property_id = ${propertyId};
@ -96,9 +163,27 @@ export async function deleteProperty(propertyId: number) {
WHERE property_id = ${propertyId};
`);
// -----------------------------
// PROPERTY LAST (ALWAYS)
// -----------------------------
await tx.execute(sql`
DELETE FROM property
WHERE id = ${propertyId};
`);
});
}
// Find All foregin keys used for 'property' table with id via this command
// SELECT
// tc.table_name,
// kcu.column_name,
// ccu.table_name AS foreign_table_name,
// ccu.column_name AS foreign_column_name
// FROM information_schema.table_constraints AS tc
// JOIN information_schema.key_column_usage AS kcu
// ON tc.constraint_name = kcu.constraint_name
// JOIN information_schema.constraint_column_usage AS ccu
// ON ccu.constraint_name = tc.constraint_name
// WHERE tc.constraint_type = 'FOREIGN KEY'
// AND ccu.table_name = 'property';