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; id: number;
name: string; name: string;
} | null>(null); } | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
// Inline rename state // Inline rename state
const [renamingId, setRenamingId] = useState<string | null>(null); const [renamingId, setRenamingId] = useState<string | null>(null);
@@ -353,14 +354,38 @@ export function CatalogPage() {
const res = await fetch(`/api/${typePath}/${deleteTarget.id}`, { method: "DELETE" }); const res = await fetch(`/api/${typePath}/${deleteTarget.id}`, { method: "DELETE" });
if (!res.ok) { if (!res.ok) {
const data = await res.json(); 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`); toast.success(`${deleteTarget.type} deleted`);
setDeleteTarget(null); setDeleteTarget(null);
setDeleteError(null);
fetchTree(); fetchTree();
fetchProducts(); fetchProducts();
} catch (e) { } 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> </Dialog>
{/* ─── Delete Confirmation ─── */} {/* ─── Delete Confirmation ─── */}
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}> <AlertDialog open={!!deleteTarget} onOpenChange={(open) => { if (!open) { setDeleteTarget(null); setDeleteError(null); } }}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Delete {deleteTarget?.type}?</AlertDialogTitle> <AlertDialogTitle>Delete {deleteTarget?.type}?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Are you sure you want to delete &quot;{deleteTarget?.name}&quot;? This action cannot be Are you sure you want to delete &quot;{deleteTarget?.name}&quot;? This action cannot be
undone. undone.
{deleteError && (
<span className="block mt-2 text-red-600 font-medium">
{deleteError}
</span>
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <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 <AlertDialogAction
onClick={handleDelete} onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 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 // Group locations by country > city > district
const groupedLocations = locations.reduce<Record<string, Record<string, Record<string, LocationItem>>>>( const groupedLocations = locations.reduce<Record<string, Record<string, Record<string, LocationItem>>>>(
(acc, loc) => { (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. Are you sure you want to delete &quot;{deleteTarget?.name}&quot;? This action cannot be undone.
{deleteTarget && deleteTarget._count.products > 0 && ( {deleteTarget && deleteTarget._count.products > 0 && (
<span className="block mt-2 text-red-600 font-medium"> <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> </span>
)} )}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
{deleteTarget && deleteTarget._count.products > 0 && (
<Button variant="outline" onClick={handleDeactivate}>
🔕 Deactivate
</Button>
)}
<AlertDialogAction <AlertDialogAction
onClick={handleDelete} onClick={handleDelete}
disabled={deleting || (deleteTarget ? deleteTarget._count.products > 0 : true)} 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 ( return (
<div className="page-enter p-4 md:p-6 space-y-4"> <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"> <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) && ( {deleteTarget && (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) && (
<span className="block mt-2 text-red-600 font-medium"> <span className="block mt-2 text-red-600 font-medium">
This location has {deleteTarget._count.categories} categor{deleteTarget._count.categories === 1 ? "y" : "ies"} 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> </span>
)} )}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
{deleteTarget && (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) && (
<Button variant="outline" onClick={handleDeactivate}>
🔕 Deactivate
</Button>
)}
<AlertDialogAction <AlertDialogAction
onClick={handleDelete} onClick={handleDelete}
disabled={deleting || (deleteTarget ? (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) : true)} disabled={deleting || (deleteTarget ? (deleteTarget._count.categories > 0 || deleteTarget._count.products > 0) : true)}