feat(plan): add hard-delete for categories (blocked when in use)
DELETE /plan/categories/:id now hard-deletes the row, guarded: refuses when any live entry/override uses the key (suggests hide instead) and keeps >=1 active category. Modal gains a per-row Smazat with inline confirm. Hiding stays via PATCH is_active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ export default function PlanCategoriesModal({
|
||||
const alert = useAlert();
|
||||
const [newLabel, setNewLabel] = useState("");
|
||||
const [newColor, setNewColor] = useState(DEFAULT_NEW_COLOR);
|
||||
// Id of the row showing the inline "really delete?" confirm, or null.
|
||||
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
|
||||
|
||||
const createCat = useApiMutation<
|
||||
{ label: string; color: string },
|
||||
@@ -37,6 +39,11 @@ export default function PlanCategoriesModal({
|
||||
method: "PATCH",
|
||||
invalidate: ["plan", "dashboard", "audit-log"],
|
||||
});
|
||||
const deleteCat = useApiMutation<{ id: number }, { ok: true }>({
|
||||
url: (v) => `/api/admin/plan/categories/${v.id}`,
|
||||
method: "DELETE",
|
||||
invalidate: ["plan", "dashboard", "audit-log"],
|
||||
});
|
||||
|
||||
const handleAdd = () => {
|
||||
const label = newLabel.trim();
|
||||
@@ -66,7 +73,23 @@ export default function PlanCategoriesModal({
|
||||
);
|
||||
};
|
||||
|
||||
const busy = createCat.isPending || updateCat.isPending;
|
||||
const handleDelete = (id: number) => {
|
||||
deleteCat.mutate(
|
||||
{ id },
|
||||
{
|
||||
onSuccess: () => setConfirmDeleteId(null),
|
||||
// Server blocks deleting a category that is still in use (409) — show
|
||||
// the Czech message ("…můžete ji skrýt") and keep the row.
|
||||
onError: (e) => {
|
||||
alert.error(e.message);
|
||||
setConfirmDeleteId(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const busy =
|
||||
createCat.isPending || updateCat.isPending || deleteCat.isPending;
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
@@ -110,14 +133,47 @@ export default function PlanCategoriesModal({
|
||||
}}
|
||||
aria-label={`Název – ${c.label}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
||||
>
|
||||
{c.is_active ? "Skrýt" : "Obnovit"}
|
||||
</button>
|
||||
{confirmDeleteId === c.id ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn admin-btn-danger admin-btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => handleDelete(c.id)}
|
||||
>
|
||||
Opravdu smazat
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => setConfirmDeleteId(null)}
|
||||
>
|
||||
Zrušit
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
||||
>
|
||||
{c.is_active ? "Skrýt" : "Obnovit"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="admin-btn admin-btn-danger admin-btn-sm plan-cat-delete"
|
||||
disabled={busy}
|
||||
onClick={() => setConfirmDeleteId(c.id)}
|
||||
aria-label={`Smazat – ${c.label}`}
|
||||
title="Smazat kategorii"
|
||||
>
|
||||
Smazat
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -1011,3 +1011,13 @@
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--plan-paper-rule);
|
||||
}
|
||||
/* The per-row delete trigger reads quieter than the solid danger confirm
|
||||
button it turns into; keep it an outline until the user commits. */
|
||||
.plan-cat-delete.admin-btn-danger {
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
border: 1px solid color-mix(in srgb, #dc2626 40%, transparent);
|
||||
}
|
||||
.plan-cat-delete.admin-btn-danger:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, #dc2626 12%, transparent);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user