import { CreateOrgUnitSchema, UpdateOrgUnitSchema } from "@capakraken/shared"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { createAuditEntry } from "../lib/audit.js"; import { adminProcedure, createTRPCRouter, protectedProcedure, resourceOverviewProcedure, } from "../trpc.js"; import { assertOrgUnitParentLevel, buildOrgUnitCreateData, buildOrgUnitListWhere, buildOrgUnitTree, buildOrgUnitUpdateData, findOrgUnitByIdentifier, } from "./org-unit-support.js"; type OrgUnitIdentifierReadModel = { id: string; name: string; shortName: string | null; level: number; isActive: boolean; }; type OrgUnitDetailReadModel = OrgUnitIdentifierReadModel & { parentId: string | null; sortOrder: number; createdAt: Date; updatedAt: Date; _count: { resources: number }; }; export const orgUnitRouter = createTRPCRouter({ list: resourceOverviewProcedure .input( z.object({ level: z.number().int().min(5).max(7).optional(), parentId: z.string().optional(), isActive: z.boolean().optional(), }).optional(), ) .query(async ({ ctx, input }) => { return ctx.db.orgUnit.findMany({ where: buildOrgUnitListWhere(input ?? {}), orderBy: [{ level: "asc" }, { sortOrder: "asc" }, { name: "asc" }], }); }), getTree: resourceOverviewProcedure .input(z.object({ isActive: z.boolean().optional() }).optional()) .query(async ({ ctx, input }) => { const all = await ctx.db.orgUnit.findMany({ where: buildOrgUnitListWhere({ isActive: input?.isActive }), orderBy: [{ sortOrder: "asc" }, { name: "asc" }], }); return buildOrgUnitTree(all); }), getById: resourceOverviewProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const unit = await findUniqueOrThrow( ctx.db.orgUnit.findUnique({ where: { id: input.id }, include: { parent: true, children: { orderBy: { sortOrder: "asc" } }, _count: { select: { resources: true } }, }, }), "Org unit", ); return unit; }), resolveByIdentifier: protectedProcedure .input(z.object({ identifier: z.string().trim().min(1) })) .query(async ({ ctx, input }) => { return findOrgUnitByIdentifier(ctx.db, input.identifier, { select: { id: true, name: true, shortName: true, level: true, isActive: true, }, }); }), getByIdentifier: resourceOverviewProcedure .input(z.object({ identifier: z.string().trim().min(1) })) .query(async ({ ctx, input }) => { return findOrgUnitByIdentifier(ctx.db, input.identifier, { include: { _count: { select: { resources: true } } }, }); }), create: adminProcedure .input(CreateOrgUnitSchema) .mutation(async ({ ctx, input }) => { if (input.parentId) { const parent = await findUniqueOrThrow( ctx.db.orgUnit.findUnique({ where: { id: input.parentId } }), "Parent org unit", ); assertOrgUnitParentLevel(parent, input.level); } const created = await ctx.db.orgUnit.create({ data: buildOrgUnitCreateData(input), }); void createAuditEntry({ db: ctx.db, entityType: "OrgUnit", entityId: created.id, entityName: created.name, action: "CREATE", userId: ctx.dbUser?.id, after: created as unknown as Record, source: "ui", }); return created; }), update: adminProcedure .input(z.object({ id: z.string(), data: UpdateOrgUnitSchema })) .mutation(async ({ ctx, input }) => { const existing = await findUniqueOrThrow( ctx.db.orgUnit.findUnique({ where: { id: input.id } }), "Org unit", ); const before = existing as unknown as Record; const updated = await ctx.db.orgUnit.update({ where: { id: input.id }, data: buildOrgUnitUpdateData(input.data), }); void createAuditEntry({ db: ctx.db, entityType: "OrgUnit", entityId: updated.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before, after: updated as unknown as Record, source: "ui", }); return updated; }), deactivate: adminProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const updated = await ctx.db.orgUnit.update({ where: { id: input.id }, data: { isActive: false }, }); void createAuditEntry({ db: ctx.db, entityType: "OrgUnit", entityId: updated.id, entityName: updated.name, action: "UPDATE", userId: ctx.dbUser?.id, before: { isActive: true }, after: { isActive: false }, source: "ui", summary: "Deactivated OrgUnit", }); return updated; }), });