feat(admin): issue #145 — delete-blocked hint + Deactivate/Edit alternative

- categories dialog: red hint with count + Deactivate button when products>0
- locations dialog: red hint + Deactivate button when categories/products>0
- catalog dialog: deleteError shown on 400/409; Deactivate for category/subcategory/location, Edit for product
- Deactivate calls existing PATCH toggle, closes dialog, reloads
- tsc clean
This commit is contained in:
NW
2026-08-10 08:44:12 +01:00
parent 3b8e6416c4
commit 792efc07ce
3 changed files with 67 additions and 5 deletions

View File

@@ -211,6 +211,7 @@ export function CatalogPage() {
id: number;
name: string;
} | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
// Inline rename state
const [renamingId, setRenamingId] = useState<string | null>(null);
@@ -353,14 +354,38 @@ export function CatalogPage() {
const res = await fetch(`/api/${typePath}/${deleteTarget.id}`, { method: "DELETE" });
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to delete");
const msg = data.error || "Failed to delete";
setDeleteError(msg);
toast.error(msg);
return;
}
toast.success(`${deleteTarget.type} deleted`);
setDeleteTarget(null);
setDeleteError(null);
fetchTree();
fetchProducts();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to delete");
const msg = e instanceof Error ? e.message : "Failed to delete";
setDeleteError(msg);
toast.error(msg);
}
};
const handleDeactivateFromCatalog = () => {
if (!deleteTarget) return;
handleToggleActive(deleteTarget.type, deleteTarget.id);
setDeleteTarget(null);
setDeleteError(null);
};
const handleEditFromCatalog = () => {
if (!deleteTarget || deleteTarget.type !== "product") return;
// Find the product in the current products list
const product = products.find((p) => p.id === deleteTarget.id);
setDeleteTarget(null);
setDeleteError(null);
if (product) {
openProductModal(product);
}
};
@@ -1488,17 +1513,32 @@ export function CatalogPage() {
</Dialog>
{/* ─── Delete Confirmation ─── */}
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => { if (!open) { setDeleteTarget(null); setDeleteError(null); } }}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {deleteTarget?.type}?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{deleteTarget?.name}&quot;? This action cannot be
undone.
{deleteError && (
<span className="block mt-2 text-red-600 font-medium">
{deleteError}
</span>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
{deleteError && deleteTarget && deleteTarget.type !== "product" && (
<Button variant="outline" onClick={handleDeactivateFromCatalog}>
🔕 Deactivate
</Button>
)}
{deleteError && deleteTarget && deleteTarget.type === "product" && (
<Button variant="outline" onClick={handleEditFromCatalog}>
Edit
</Button>
)}
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"

View File

@@ -242,6 +242,12 @@ export function CategoriesPage() {
}
};
const handleDeactivate = () => {
if (!deleteTarget) return;
handleToggle(deleteTarget);
setDeleteTarget(null);
};
// Group locations by country > city > district
const groupedLocations = locations.reduce<Record<string, Record<string, Record<string, LocationItem>>>>(
(acc, loc) => {
@@ -469,13 +475,18 @@ export function CategoriesPage() {
Are you sure you want to delete &quot;{deleteTarget?.name}&quot;? This action cannot be undone.
{deleteTarget && deleteTarget._count.products > 0 && (
<span className="block mt-2 text-red-600 font-medium">
This category has {deleteTarget._count.products} product(s) and cannot be deleted.
This category has {deleteTarget._count.products} product(s) and cannot be deleted. You can deactivate it instead.
</span>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
{deleteTarget && deleteTarget._count.products > 0 && (
<Button variant="outline" onClick={handleDeactivate}>
🔕 Deactivate
</Button>
)}
<AlertDialogAction
onClick={handleDelete}
disabled={deleting || (deleteTarget ? deleteTarget._count.products > 0 : true)}

View File

@@ -201,6 +201,12 @@ export function LocationsPage() {
}
};
const handleDeactivate = () => {
if (!deleteTarget) return;
handleToggle(deleteTarget);
setDeleteTarget(null);
};
return (
<div className="page-enter p-4 md:p-6 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
@@ -344,13 +350,18 @@ export function LocationsPage() {
{deleteTarget && (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) && (
<span className="block mt-2 text-red-600 font-medium">
This location has {deleteTarget._count.categories} categor{deleteTarget._count.categories === 1 ? "y" : "ies"}
and {deleteTarget._count.products} product(s) and cannot be deleted.
{" "}and {deleteTarget._count.products} product(s) and cannot be deleted. You can deactivate it instead.
</span>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
{deleteTarget && (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) && (
<Button variant="outline" onClick={handleDeactivate}>
🔕 Deactivate
</Button>
)}
<AlertDialogAction
onClick={handleDelete}
disabled={deleting || (deleteTarget ? (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) : true)}