From 973c909e3d2a77856153eb5c89afb6140588ea49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 18:46:08 +0200 Subject: [PATCH] refactor(api): extract timeline allocation router support --- ...timeline-allocation-router-support.test.ts | 186 ++++++++++++++++++ .../router/timeline-allocation-mutations.ts | 41 +--- .../timeline-allocation-router-support.ts | 69 +++++++ 3 files changed, 265 insertions(+), 31 deletions(-) create mode 100644 packages/api/src/__tests__/timeline-allocation-router-support.test.ts create mode 100644 packages/api/src/router/timeline-allocation-router-support.ts diff --git a/packages/api/src/__tests__/timeline-allocation-router-support.test.ts b/packages/api/src/__tests__/timeline-allocation-router-support.test.ts new file mode 100644 index 0000000..8613b32 --- /dev/null +++ b/packages/api/src/__tests__/timeline-allocation-router-support.test.ts @@ -0,0 +1,186 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../sse/event-bus.js", () => ({ + emitAllocationUpdated: vi.fn(), +})); + +vi.mock("../router/timeline-allocation-assignment-procedure-support.js", () => ({ + createTimelineBatchQuickAssignments: vi.fn(), + createTimelineQuickAssignment: vi.fn(), +})); + +vi.mock("../router/timeline-allocation-inline-support.js", () => ({ + applyTimelineInlineAllocationUpdate: vi.fn(), +})); + +vi.mock("../router/timeline-allocation-procedure-support.js", () => ({ + shiftTimelineAllocations: vi.fn(), +})); + +import { emitAllocationUpdated } from "../sse/event-bus.js"; +import { + createTimelineBatchQuickAssignments, + createTimelineQuickAssignment, +} from "../router/timeline-allocation-assignment-procedure-support.js"; +import { applyTimelineInlineAllocationUpdate } from "../router/timeline-allocation-inline-support.js"; +import { shiftTimelineAllocations } from "../router/timeline-allocation-procedure-support.js"; +import { + applyTimelineAllocationBatchShiftMutation, + createTimelineBatchQuickAssignMutation, + createTimelineQuickAssignMutation, + updateTimelineAllocationInlineMutation, +} from "../router/timeline-allocation-router-support.js"; + +const emitAllocationUpdatedMock = vi.mocked(emitAllocationUpdated); +const createTimelineBatchQuickAssignmentsMock = vi.mocked(createTimelineBatchQuickAssignments); +const createTimelineQuickAssignmentMock = vi.mocked(createTimelineQuickAssignment); +const applyTimelineInlineAllocationUpdateMock = vi.mocked(applyTimelineInlineAllocationUpdate); +const shiftTimelineAllocationsMock = vi.mocked(shiftTimelineAllocations); + +describe("timeline allocation router support", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("updates an inline allocation and emits an update event", async () => { + const db = {} as never; + applyTimelineInlineAllocationUpdateMock.mockResolvedValueOnce({ + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + } as never); + + await expect( + updateTimelineAllocationInlineMutation({ + db, + allocationId: "allocation_1", + hoursPerDay: 8, + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + includeSaturday: true, + role: "Lead", + }), + ).resolves.toEqual({ + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + }); + + expect(applyTimelineInlineAllocationUpdateMock).toHaveBeenCalledWith({ + db, + allocationId: "allocation_1", + hoursPerDay: 8, + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + includeSaturday: true, + role: "Lead", + }); + expect(emitAllocationUpdatedMock).toHaveBeenCalledWith({ + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + }); + }); + + it("creates a quick assignment with the router source metadata", async () => { + const db = {} as never; + createTimelineQuickAssignmentMock.mockResolvedValueOnce({ id: "allocation_1" } as never); + + await expect( + createTimelineQuickAssignMutation(db, { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Lead", + status: "PROPOSED", + }), + ).resolves.toEqual({ id: "allocation_1" }); + + expect(createTimelineQuickAssignmentMock).toHaveBeenCalledWith(db, { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Lead", + status: "PROPOSED", + source: "quickAssign", + }); + }); + + it("maps batch quick assignments to the batch router source metadata", async () => { + const db = {} as never; + createTimelineBatchQuickAssignmentsMock.mockResolvedValueOnce({ count: 2 } as never); + + await expect( + createTimelineBatchQuickAssignMutation(db, { + assignments: [ + { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Lead", + status: "PROPOSED", + }, + { + resourceId: "resource_2", + projectId: "project_2", + startDate: new Date("2026-04-06T00:00:00.000Z"), + endDate: new Date("2026-04-10T00:00:00.000Z"), + hoursPerDay: 6, + role: "Artist", + status: "PROPOSED", + }, + ], + }), + ).resolves.toEqual({ count: 2 }); + + expect(createTimelineBatchQuickAssignmentsMock).toHaveBeenCalledWith(db, { + assignments: [ + { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Lead", + status: "PROPOSED", + source: "batchQuickAssign", + }, + { + resourceId: "resource_2", + projectId: "project_2", + startDate: new Date("2026-04-06T00:00:00.000Z"), + endDate: new Date("2026-04-10T00:00:00.000Z"), + hoursPerDay: 6, + role: "Artist", + status: "PROPOSED", + source: "batchQuickAssign", + }, + ], + }); + }); + + it("delegates allocation batch shifts unchanged", async () => { + const db = {} as never; + shiftTimelineAllocationsMock.mockResolvedValueOnce({ count: 3 } as never); + + await expect( + applyTimelineAllocationBatchShiftMutation(db, { + allocationIds: ["allocation_1", "allocation_2"], + daysDelta: 5, + mode: "preserve-duration", + }), + ).resolves.toEqual({ count: 3 }); + + expect(shiftTimelineAllocationsMock).toHaveBeenCalledWith(db, { + allocationIds: ["allocation_1", "allocation_2"], + daysDelta: 5, + mode: "preserve-duration", + }); + }); +}); diff --git a/packages/api/src/router/timeline-allocation-mutations.ts b/packages/api/src/router/timeline-allocation-mutations.ts index 130e5b6..ab5f293 100644 --- a/packages/api/src/router/timeline-allocation-mutations.ts +++ b/packages/api/src/router/timeline-allocation-mutations.ts @@ -1,26 +1,25 @@ import type { PrismaClient } from "@capakraken/db"; import { PermissionKey } from "@capakraken/shared"; -import { emitAllocationUpdated } from "../sse/event-bus.js"; import { managerProcedure, requirePermission } from "../trpc.js"; -import { - createTimelineBatchQuickAssignments, - createTimelineQuickAssignment, -} from "./timeline-allocation-assignment-procedure-support.js"; -import { shiftTimelineAllocations } from "./timeline-allocation-procedure-support.js"; import { UpdateAllocationHoursSchema, timelineBatchQuickAssignInputSchema, timelineBatchShiftAllocationsInputSchema, timelineQuickAssignInputSchema, } from "./timeline-allocation-mutation-schema-support.js"; -import { applyTimelineInlineAllocationUpdate } from "./timeline-allocation-inline-support.js"; +import { + applyTimelineAllocationBatchShiftMutation, + createTimelineBatchQuickAssignMutation, + createTimelineQuickAssignMutation, + updateTimelineAllocationInlineMutation, +} from "./timeline-allocation-router-support.js"; export const timelineAllocationMutationProcedures = { updateAllocationInline: managerProcedure .input(UpdateAllocationHoursSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - const updated = await applyTimelineInlineAllocationUpdate({ + return updateTimelineAllocationInlineMutation({ db: ctx.db as PrismaClient, allocationId: input.allocationId, hoursPerDay: input.hoursPerDay, @@ -29,46 +28,26 @@ export const timelineAllocationMutationProcedures = { includeSaturday: input.includeSaturday, role: input.role, }); - - emitAllocationUpdated({ - id: updated.id, - projectId: updated.projectId, - resourceId: updated.resourceId, - }); - - return updated; }), quickAssign: managerProcedure .input(timelineQuickAssignInputSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - return createTimelineQuickAssignment(ctx.db as PrismaClient, { - ...input, - source: "quickAssign", - }); + return createTimelineQuickAssignMutation(ctx.db as PrismaClient, input); }), batchQuickAssign: managerProcedure .input(timelineBatchQuickAssignInputSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - return createTimelineBatchQuickAssignments(ctx.db as PrismaClient, { - assignments: input.assignments.map((assignment) => ({ - ...assignment, - source: "batchQuickAssign" as const, - })), - }); + return createTimelineBatchQuickAssignMutation(ctx.db as PrismaClient, input); }), batchShiftAllocations: managerProcedure .input(timelineBatchShiftAllocationsInputSchema) .mutation(async ({ ctx, input }) => { requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - return shiftTimelineAllocations(ctx.db as PrismaClient, { - allocationIds: input.allocationIds, - daysDelta: input.daysDelta, - mode: input.mode, - }); + return applyTimelineAllocationBatchShiftMutation(ctx.db as PrismaClient, input); }), }; diff --git a/packages/api/src/router/timeline-allocation-router-support.ts b/packages/api/src/router/timeline-allocation-router-support.ts new file mode 100644 index 0000000..fbebb6c --- /dev/null +++ b/packages/api/src/router/timeline-allocation-router-support.ts @@ -0,0 +1,69 @@ +import type { PrismaClient } from "@capakraken/db"; +import { emitAllocationUpdated } from "../sse/event-bus.js"; +import { + createTimelineBatchQuickAssignments, + createTimelineQuickAssignment, +} from "./timeline-allocation-assignment-procedure-support.js"; +import { applyTimelineInlineAllocationUpdate } from "./timeline-allocation-inline-support.js"; +import { shiftTimelineAllocations } from "./timeline-allocation-procedure-support.js"; + +export async function updateTimelineAllocationInlineMutation(input: { + db: PrismaClient; + allocationId: string; + hoursPerDay?: number | undefined; + startDate?: Date | null | undefined; + endDate?: Date | null | undefined; + includeSaturday?: boolean | undefined; + role?: string | null | undefined; +}) { + const updated = await applyTimelineInlineAllocationUpdate({ + db: input.db, + allocationId: input.allocationId, + hoursPerDay: input.hoursPerDay, + startDate: input.startDate ?? undefined, + endDate: input.endDate ?? undefined, + includeSaturday: input.includeSaturday, + role: input.role ?? undefined, + }); + + emitAllocationUpdated({ + id: updated.id, + projectId: updated.projectId, + resourceId: updated.resourceId, + }); + + return updated; +} + +export async function createTimelineQuickAssignMutation( + db: PrismaClient, + input: Omit[1], "source">, +) { + return createTimelineQuickAssignment(db, { + ...input, + source: "quickAssign", + }); +} + +export async function createTimelineBatchQuickAssignMutation( + db: PrismaClient, + input: { + assignments: Array< + Omit[1]["assignments"][number], "source"> + >; + }, +) { + return createTimelineBatchQuickAssignments(db, { + assignments: input.assignments.map((assignment) => ({ + ...assignment, + source: "batchQuickAssign" as const, + })), + }); +} + +export async function applyTimelineAllocationBatchShiftMutation( + db: PrismaClient, + input: Parameters[1], +) { + return shiftTimelineAllocations(db, input); +}