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:
@@ -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 "{deleteTarget?.name}"? This action cannot be
|
Are you sure you want to delete "{deleteTarget?.name}"? 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"
|
||||||
|
|||||||
@@ -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 "{deleteTarget?.name}"? This action cannot be undone.
|
Are you sure you want to delete "{deleteTarget?.name}"? 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)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user