feat(resources): add hard-delete action to resource list (per-row and batch)

- Add batchHardDelete adminProcedure to resource-mutations router
- Per-row Delete button visible to ADMIN role only
- Delete Selected button in BatchActionBar for ADMIN role only
- Two-step confirmation dialogs with permanent-action warnings
- Audit log written for each deleted resource

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-03 23:18:30 +02:00
parent f5e41f7efe
commit fba65387fe
2 changed files with 93 additions and 2 deletions
@@ -47,7 +47,9 @@ type ModalState =
type ConfirmState =
| { type: "closed" }
| { type: "batchDeactivate"; ids: string[] }
| { type: "deactivate"; resource: Resource };
| { type: "deactivate"; resource: Resource }
| { type: "delete"; resource: Resource }
| { type: "batchDelete"; ids: string[] };
type ActiveFilter = "active" | "inactive" | "all";
type BooleanFilter = "all" | "yes" | "no";
@@ -163,7 +165,7 @@ export function ResourcesClient() {
const selection = useSelection();
const utils = trpc.useUtils();
const { canViewScores, canViewCosts } = usePermissions();
const { canViewScores, canViewCosts, canManageUsers } = usePermissions();
const {
customFieldFilters,
setCustomFieldFilter,
@@ -323,6 +325,19 @@ export function ResourcesClient() {
selection.clear();
},
});
const hardDeleteMutation = trpc.resource.hardDelete.useMutation({
onSuccess: () => {
void utils.resource.directory.invalidate();
void utils.resource.listStaff.invalidate();
},
});
const batchHardDeleteMutation = trpc.resource.batchHardDelete.useMutation({
onSuccess: () => {
void utils.resource.directory.invalidate();
void utils.resource.listStaff.invalidate();
selection.clear();
},
});
useEffect(() => {
selection.clear();
@@ -348,6 +363,10 @@ export function ResourcesClient() {
deactivateMutation.mutate({ id: confirm.resource.id });
} else if (confirm.type === "batchDeactivate") {
batchDeactivateMutation.mutate({ ids: confirm.ids });
} else if (confirm.type === "delete") {
hardDeleteMutation.mutate({ id: confirm.resource.id });
} else if (confirm.type === "batchDelete") {
batchHardDeleteMutation.mutate({ ids: confirm.ids });
}
setConfirm({ type: "closed" });
}
@@ -1384,6 +1403,21 @@ export function ResourcesClient() {
>
{isDeactivating ? "Deactivating…" : "Deactivate"}
</button>
{canManageUsers && (
<button
type="button"
onClick={() =>
setConfirm({
type: "delete",
resource: resource as unknown as Resource,
})
}
disabled={hardDeleteMutation.isPending}
className="ml-3 text-xs font-medium text-red-800 transition-colors hover:text-red-950 disabled:opacity-50 dark:text-red-400 dark:hover:text-red-200"
>
Delete
</button>
)}
</td>
</DraggableTableRow>
);
@@ -1426,6 +1460,16 @@ export function ResourcesClient() {
onClick: () => setConfirm({ type: "batchDeactivate", ids: selection.selectedArray }),
disabled: batchDeactivateMutation.isPending,
},
...(canManageUsers
? [
{
label: `Delete ${selection.count > 0 ? `(${selection.count})` : ""}`,
variant: "danger" as const,
onClick: () => setConfirm({ type: "batchDelete", ids: selection.selectedArray }),
disabled: batchHardDeleteMutation.isPending,
},
]
: []),
]}
/>
@@ -1469,6 +1513,26 @@ export function ResourcesClient() {
onCancel={() => setConfirm({ type: "closed" })}
/>
)}
{confirm.type === "delete" && (
<ConfirmDialog
title="Permanently Delete Resource"
message={`Delete "${confirm.resource.displayName}" (${confirm.resource.eid}) permanently? This will also delete all their assignments and vacation records. This action cannot be undone.`}
confirmLabel="Delete Permanently"
variant="danger"
onConfirm={handleConfirm}
onCancel={() => setConfirm({ type: "closed" })}
/>
)}
{confirm.type === "batchDelete" && (
<ConfirmDialog
title="Permanently Delete Resources"
message={`Delete ${confirm.ids.length} selected resource${confirm.ids.length !== 1 ? "s" : ""} permanently? All their assignments and vacation records will also be deleted. This action cannot be undone.`}
confirmLabel="Delete All Permanently"
variant="danger"
onConfirm={handleConfirm}
onCancel={() => setConfirm({ type: "closed" })}
/>
)}
<SuccessToast
show={successToast !== null}