From 269288f5dfb50b0b7dbb9650a03ad1ed6e32c076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 11:02:35 +0200 Subject: [PATCH] refactor(api): extract allocation read procedures --- .../src/router/allocation-read-procedures.ts | 165 ++++++++++++++++++ packages/api/src/router/allocation.ts | 162 +---------------- 2 files changed, 168 insertions(+), 159 deletions(-) create mode 100644 packages/api/src/router/allocation-read-procedures.ts diff --git a/packages/api/src/router/allocation-read-procedures.ts b/packages/api/src/router/allocation-read-procedures.ts new file mode 100644 index 0000000..c378542 --- /dev/null +++ b/packages/api/src/router/allocation-read-procedures.ts @@ -0,0 +1,165 @@ +import { AllocationStatus } from "@capakraken/shared"; +import { z } from "zod"; +import { findUniqueOrThrow } from "../db/helpers.js"; +import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js"; +import { planningReadProcedure } from "../trpc.js"; +import { buildResourceAvailabilitySummary, buildResourceAvailabilityView } from "./allocation-availability.js"; +import { ASSIGNMENT_INCLUDE, DEMAND_INCLUDE } from "./allocation-shared.js"; +import { getDemandRequirementByIdOrThrow, loadAllocationReadModel, resolveAssignmentBySelection } from "./allocation-support.js"; + +export const allocationReadProcedures = { + list: planningReadProcedure + .input( + z.object({ + projectId: z.string().optional(), + resourceId: z.string().optional(), + status: z.nativeEnum(AllocationStatus).optional(), + }), + ) + .query(async ({ ctx, input }) => { + const readModel = await loadAllocationReadModel(ctx.db, input); + return readModel.allocations; + }), + + listView: planningReadProcedure + .input( + z.object({ + projectId: z.string().optional(), + resourceId: z.string().optional(), + status: z.nativeEnum(AllocationStatus).optional(), + }), + ) + .query(async ({ ctx, input }) => loadAllocationReadModel(ctx.db, input)), + + listDemands: planningReadProcedure + .input( + z.object({ + projectId: z.string().optional(), + status: z.nativeEnum(AllocationStatus).optional(), + roleId: z.string().optional(), + }), + ) + .query(async ({ ctx, input }) => { + const demands = await ctx.db.demandRequirement.findMany({ + where: { + ...(input.projectId ? { projectId: input.projectId } : {}), + ...(input.status ? { status: input.status } : {}), + ...(input.roleId ? { roleId: input.roleId } : {}), + }, + include: DEMAND_INCLUDE, + orderBy: { startDate: "asc" }, + }); + const directory = await getAnonymizationDirectory(ctx.db); + if (!directory) { + return demands; + } + return demands.map((demand) => ({ + ...demand, + assignments: demand.assignments.map((assignment) => ( + assignment.resource + ? { ...assignment, resource: anonymizeResource(assignment.resource, directory) } + : assignment + )), + })); + }), + + listAssignments: planningReadProcedure + .input( + z.object({ + projectId: z.string().optional(), + resourceId: z.string().optional(), + status: z.nativeEnum(AllocationStatus).optional(), + demandRequirementId: z.string().optional(), + }), + ) + .query(async ({ ctx, input }) => { + const assignments = await ctx.db.assignment.findMany({ + where: { + ...(input.projectId ? { projectId: input.projectId } : {}), + ...(input.resourceId ? { resourceId: input.resourceId } : {}), + ...(input.status ? { status: input.status } : {}), + ...(input.demandRequirementId ? { demandRequirementId: input.demandRequirementId } : {}), + }, + include: ASSIGNMENT_INCLUDE, + orderBy: { startDate: "asc" }, + }); + const directory = await getAnonymizationDirectory(ctx.db); + if (!directory) { + return assignments; + } + return assignments.map((assignment) => ( + assignment.resource + ? { ...assignment, resource: anonymizeResource(assignment.resource, directory) } + : assignment + )); + }), + + getAssignmentById: planningReadProcedure + .input(z.object({ id: z.string() })) + .query(async ({ ctx, input }) => { + const assignment = await findUniqueOrThrow( + ctx.db.assignment.findUnique({ + where: { id: input.id }, + include: ASSIGNMENT_INCLUDE, + }), + "Assignment", + ); + const directory = await getAnonymizationDirectory(ctx.db); + if (!directory || !assignment.resource) { + return assignment; + } + return { + ...assignment, + resource: anonymizeResource(assignment.resource, directory), + }; + }), + + resolveAssignment: planningReadProcedure + .input(z.object({ + assignmentId: z.string().optional(), + resourceId: z.string().optional(), + projectId: z.string().optional(), + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), + selectionMode: z.enum(["WINDOW", "EXACT_START"]).default("EXACT_START"), + excludeCancelled: z.boolean().default(false), + })) + .query(async ({ ctx, input }) => resolveAssignmentBySelection(ctx.db, input)), + + getDemandRequirementById: planningReadProcedure + .input(z.object({ id: z.string() })) + .query(async ({ ctx, input }) => getDemandRequirementByIdOrThrow(ctx.db, input.id)), + + checkResourceAvailability: planningReadProcedure + .input(z.object({ + resourceId: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), + hoursPerDay: z.number().min(0.5).max(24).default(8), + })) + .query(async ({ ctx, input }) => { + const { vacations: _vacations, ...availability } = await buildResourceAvailabilityView(ctx.db, input); + return availability; + }), + + getResourceAvailabilityView: planningReadProcedure + .input(z.object({ + resourceId: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), + hoursPerDay: z.number().min(0.5).max(24).default(8), + })) + .query(async ({ ctx, input }) => buildResourceAvailabilityView(ctx.db, input)), + + getResourceAvailabilitySummary: planningReadProcedure + .input(z.object({ + resourceId: z.string(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), + hoursPerDay: z.number().min(0.5).max(24).default(8), + })) + .query(async ({ ctx, input }) => { + const availability = await buildResourceAvailabilityView(ctx.db, input); + return buildResourceAvailabilitySummary(availability, input); + }), +}; diff --git a/packages/api/src/router/allocation.ts b/packages/api/src/router/allocation.ts index 318d6b9..1613671 100644 --- a/packages/api/src/router/allocation.ts +++ b/packages/api/src/router/allocation.ts @@ -26,9 +26,7 @@ import { import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; -import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js"; import { emitAllocationCreated, emitAllocationDeleted, emitAllocationUpdated } from "../sse/event-bus.js"; -import { buildResourceAvailabilitySummary, buildResourceAvailabilityView } from "./allocation-availability.js"; import { checkBudgetThresholdsInBackground, createDemandRequirementWithEffects, @@ -36,45 +34,19 @@ import { fillDemandRequirementWithEffects, invalidateDashboardCacheInBackground, } from "./allocation-effects.js"; -import { - ASSIGNMENT_INCLUDE, - DEMAND_INCLUDE, - toIsoDate, -} from "./allocation-shared.js"; +import { allocationReadProcedures } from "./allocation-read-procedures.js"; +import { ASSIGNMENT_INCLUDE, DEMAND_INCLUDE, toIsoDate } from "./allocation-shared.js"; import { buildCreateDemandRequirementInput, findAllocationEntryOrNull, getDemandRequirementByIdOrThrow, - loadAllocationReadModel, - resolveAssignmentBySelection, toAssignmentUpdateInput, toDemandRequirementUpdateInput, } from "./allocation-support.js"; import { createTRPCRouter, managerProcedure, planningReadProcedure, requirePermission } from "../trpc.js"; export const allocationRouter = createTRPCRouter({ - list: planningReadProcedure - .input( - z.object({ - projectId: z.string().optional(), - resourceId: z.string().optional(), - status: z.nativeEnum(AllocationStatus).optional(), - }), - ) - .query(async ({ ctx, input }) => { - const readModel = await loadAllocationReadModel(ctx.db, input); - return readModel.allocations; - }), - - listView: planningReadProcedure - .input( - z.object({ - projectId: z.string().optional(), - resourceId: z.string().optional(), - status: z.nativeEnum(AllocationStatus).optional(), - }), - ) - .query(async ({ ctx, input }) => loadAllocationReadModel(ctx.db, input)), + ...allocationReadProcedures, create: managerProcedure .input(CreateAllocationSchema) @@ -141,134 +113,6 @@ export const allocationRouter = createTRPCRouter({ return allocation; }), - listDemands: planningReadProcedure - .input( - z.object({ - projectId: z.string().optional(), - status: z.nativeEnum(AllocationStatus).optional(), - roleId: z.string().optional(), - }), - ) - .query(async ({ ctx, input }) => { - const demands = await ctx.db.demandRequirement.findMany({ - where: { - ...(input.projectId ? { projectId: input.projectId } : {}), - ...(input.status ? { status: input.status } : {}), - ...(input.roleId ? { roleId: input.roleId } : {}), - }, - include: DEMAND_INCLUDE, - orderBy: { startDate: "asc" }, - }); - const dir = await getAnonymizationDirectory(ctx.db); - if (!dir) return demands; - return demands.map((d) => ({ - ...d, - assignments: d.assignments.map((a) => - a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a, - ), - })); - }), - - listAssignments: planningReadProcedure - .input( - z.object({ - projectId: z.string().optional(), - resourceId: z.string().optional(), - status: z.nativeEnum(AllocationStatus).optional(), - demandRequirementId: z.string().optional(), - }), - ) - .query(async ({ ctx, input }) => { - const assignments = await ctx.db.assignment.findMany({ - where: { - ...(input.projectId ? { projectId: input.projectId } : {}), - ...(input.resourceId ? { resourceId: input.resourceId } : {}), - ...(input.status ? { status: input.status } : {}), - ...(input.demandRequirementId ? { demandRequirementId: input.demandRequirementId } : {}), - }, - include: ASSIGNMENT_INCLUDE, - orderBy: { startDate: "asc" }, - }); - const dir = await getAnonymizationDirectory(ctx.db); - if (!dir) return assignments; - return assignments.map((a) => - a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a, - ); - }), - - getAssignmentById: planningReadProcedure - .input(z.object({ id: z.string() })) - .query(async ({ ctx, input }) => { - const assignment = await findUniqueOrThrow( - ctx.db.assignment.findUnique({ - where: { id: input.id }, - include: ASSIGNMENT_INCLUDE, - }), - "Assignment", - ); - const dir = await getAnonymizationDirectory(ctx.db); - if (!dir || !assignment.resource) { - return assignment; - } - return { - ...assignment, - resource: anonymizeResource(assignment.resource, dir), - }; - }), - - resolveAssignment: planningReadProcedure - .input(z.object({ - assignmentId: z.string().optional(), - resourceId: z.string().optional(), - projectId: z.string().optional(), - startDate: z.coerce.date().optional(), - endDate: z.coerce.date().optional(), - selectionMode: z.enum(["WINDOW", "EXACT_START"]).default("EXACT_START"), - excludeCancelled: z.boolean().default(false), - })) - .query(async ({ ctx, input }) => resolveAssignmentBySelection(ctx.db, input)), - - getDemandRequirementById: planningReadProcedure - .input(z.object({ id: z.string() })) - .query(async ({ ctx, input }) => getDemandRequirementByIdOrThrow(ctx.db, input.id)), - - /** - * Check a resource's availability for a date range. - * Returns working days, existing allocations, conflict days, and available capacity. - */ - checkResourceAvailability: planningReadProcedure - .input(z.object({ - resourceId: z.string(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), - hoursPerDay: z.number().min(0.5).max(24).default(8), - })) - .query(async ({ ctx, input }) => { - const { vacations: _vacations, ...availability } = await buildResourceAvailabilityView(ctx.db, input); - return availability; - }), - - getResourceAvailabilityView: planningReadProcedure - .input(z.object({ - resourceId: z.string(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), - hoursPerDay: z.number().min(0.5).max(24).default(8), - })) - .query(async ({ ctx, input }) => buildResourceAvailabilityView(ctx.db, input)), - - getResourceAvailabilitySummary: planningReadProcedure - .input(z.object({ - resourceId: z.string(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), - hoursPerDay: z.number().min(0.5).max(24).default(8), - })) - .query(async ({ ctx, input }) => { - const availability = await buildResourceAvailabilityView(ctx.db, input); - return buildResourceAvailabilitySummary(availability, input); - }), - createDemandRequirement: managerProcedure .input(CreateDemandRequirementSchema) .mutation(async ({ ctx, input }) => {