diff --git a/apps/web/src/components/resources/ResourceModal.tsx b/apps/web/src/components/resources/ResourceModal.tsx index e74d5d8..91936d9 100644 --- a/apps/web/src/components/resources/ResourceModal.tsx +++ b/apps/web/src/components/resources/ResourceModal.tsx @@ -6,6 +6,7 @@ import type { Resource, SkillEntry } from "@capakraken/shared"; import { GERMAN_FEDERAL_STATES, inferStateFromPostalCode, ResourceType } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; +import { usePermissions } from "~/hooks/usePermissions.js"; interface RoleAssignment { roleId: string; @@ -193,10 +194,12 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) { resource ? resourceToFormState(resource) : defaultFormState(), ); const [errorMsg, setErrorMsg] = useState(null); + const [confirmDelete, setConfirmDelete] = useState(false); const panelRef = useRef(null); useFocusTrap(panelRef, true); + const { canManageUsers } = usePermissions(); const utils = trpc.useUtils(); const { data: availableRoles } = trpc.role.list.useQuery( @@ -225,8 +228,17 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) { const createMutation = trpc.resource.create.useMutation(); const updateMutation = trpc.resource.update.useMutation(); + const hardDeleteMutation = trpc.resource.hardDelete.useMutation({ + onSuccess: () => { + void utils.resource.invalidate(); + onClose(); + }, + onError: (err) => { + setErrorMsg(err.message); + }, + }); - const isMutating = createMutation.isPending || updateMutation.isPending; + const isMutating = createMutation.isPending || updateMutation.isPending || hardDeleteMutation.isPending; function setField(key: K, value: FormState[K]) { setForm((prev) => ({ ...prev, [key]: value })); @@ -955,19 +967,55 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) { {/* Footer */} -
- - +
+
+ {mode === "edit" && canManageUsers && resource && ( + confirmDelete ? ( +
+ Permanently delete this resource? + + +
+ ) : ( + + ) + )} +
+
+ + +
diff --git a/packages/api/src/router/resource-mutations.ts b/packages/api/src/router/resource-mutations.ts index 283e87a..1367776 100644 --- a/packages/api/src/router/resource-mutations.ts +++ b/packages/api/src/router/resource-mutations.ts @@ -3,7 +3,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { ROLE_BRIEF_SELECT } from "../db/selects.js"; -import { managerProcedure, requirePermission } from "../trpc.js"; +import { adminProcedure, managerProcedure, requirePermission } from "../trpc.js"; import { assertBlueprintDynamicFields } from "./blueprint-validation.js"; export const resourceMutationProcedures = { @@ -259,4 +259,34 @@ export const resourceMutationProcedures = { return { updated: input.ids.length }; }), + + hardDelete: adminProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ ctx, input }) => { + const resource = await ctx.db.resource.findUnique({ + where: { id: input.id }, + select: { id: true, displayName: true, eid: true }, + }); + if (!resource) { + throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" }); + } + + await ctx.db.$transaction([ + ctx.db.assignment.deleteMany({ where: { resourceId: input.id } }), + ctx.db.vacation.deleteMany({ where: { resourceId: input.id } }), + ctx.db.resource.delete({ where: { id: input.id } }), + ]); + + await ctx.db.auditLog.create({ + data: { + entityType: "Resource", + entityId: input.id, + action: "DELETE", + userId: ctx.dbUser?.id, + changes: { before: resource } as unknown as import("@capakraken/db").Prisma.InputJsonValue, + }, + }); + + return { deleted: true }; + }), };