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:
@@ -47,7 +47,9 @@ type ModalState =
|
|||||||
type ConfirmState =
|
type ConfirmState =
|
||||||
| { type: "closed" }
|
| { type: "closed" }
|
||||||
| { type: "batchDeactivate"; ids: string[] }
|
| { 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 ActiveFilter = "active" | "inactive" | "all";
|
||||||
type BooleanFilter = "all" | "yes" | "no";
|
type BooleanFilter = "all" | "yes" | "no";
|
||||||
@@ -163,7 +165,7 @@ export function ResourcesClient() {
|
|||||||
|
|
||||||
const selection = useSelection();
|
const selection = useSelection();
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const { canViewScores, canViewCosts } = usePermissions();
|
const { canViewScores, canViewCosts, canManageUsers } = usePermissions();
|
||||||
const {
|
const {
|
||||||
customFieldFilters,
|
customFieldFilters,
|
||||||
setCustomFieldFilter,
|
setCustomFieldFilter,
|
||||||
@@ -323,6 +325,19 @@ export function ResourcesClient() {
|
|||||||
selection.clear();
|
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(() => {
|
useEffect(() => {
|
||||||
selection.clear();
|
selection.clear();
|
||||||
@@ -348,6 +363,10 @@ export function ResourcesClient() {
|
|||||||
deactivateMutation.mutate({ id: confirm.resource.id });
|
deactivateMutation.mutate({ id: confirm.resource.id });
|
||||||
} else if (confirm.type === "batchDeactivate") {
|
} else if (confirm.type === "batchDeactivate") {
|
||||||
batchDeactivateMutation.mutate({ ids: confirm.ids });
|
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" });
|
setConfirm({ type: "closed" });
|
||||||
}
|
}
|
||||||
@@ -1384,6 +1403,21 @@ export function ResourcesClient() {
|
|||||||
>
|
>
|
||||||
{isDeactivating ? "Deactivating…" : "Deactivate"}
|
{isDeactivating ? "Deactivating…" : "Deactivate"}
|
||||||
</button>
|
</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>
|
</td>
|
||||||
</DraggableTableRow>
|
</DraggableTableRow>
|
||||||
);
|
);
|
||||||
@@ -1426,6 +1460,16 @@ export function ResourcesClient() {
|
|||||||
onClick: () => setConfirm({ type: "batchDeactivate", ids: selection.selectedArray }),
|
onClick: () => setConfirm({ type: "batchDeactivate", ids: selection.selectedArray }),
|
||||||
disabled: batchDeactivateMutation.isPending,
|
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" })}
|
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
|
<SuccessToast
|
||||||
show={successToast !== null}
|
show={successToast !== null}
|
||||||
|
|||||||
@@ -289,4 +289,31 @@ export const resourceMutationProcedures = {
|
|||||||
|
|
||||||
return { deleted: true };
|
return { deleted: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
batchHardDelete: adminProcedure
|
||||||
|
.input(z.object({ ids: z.array(z.string()).min(1) }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const resources = await ctx.db.resource.findMany({
|
||||||
|
where: { id: { in: input.ids } },
|
||||||
|
select: { id: true, displayName: true, eid: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.db.$transaction([
|
||||||
|
ctx.db.assignment.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
||||||
|
ctx.db.vacation.deleteMany({ where: { resourceId: { in: input.ids } } }),
|
||||||
|
ctx.db.resource.deleteMany({ where: { id: { in: input.ids } } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await ctx.db.auditLog.createMany({
|
||||||
|
data: resources.map((r) => ({
|
||||||
|
entityType: "Resource",
|
||||||
|
entityId: r.id,
|
||||||
|
action: "DELETE",
|
||||||
|
userId: ctx.dbUser?.id,
|
||||||
|
changes: { before: r } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { deleted: resources.length };
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user