refactor(api): extract timeline allocation mutation support

This commit is contained in:
2026-03-31 15:06:38 +02:00
parent b05758db69
commit b1ada431e1
3 changed files with 265 additions and 77 deletions
@@ -6,7 +6,13 @@ import {
updateAllocationEntry,
} from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import { AllocationStatus, PermissionKey, UpdateAllocationHoursSchema } from "@capakraken/shared";
import {
AllocationStatus,
PermissionKey,
UpdateAllocationHoursSchema,
type RecurrencePattern,
type WeekdayAvailability,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
@@ -14,6 +20,15 @@ import {
emitAllocationUpdated,
} from "../sse/event-bus.js";
import { managerProcedure, requirePermission } from "../trpc.js";
import {
assertTimelineDateRangeValid,
buildTimelineAllocationMetadata,
buildTimelineAllocationUpdateAuditChanges,
buildTimelineBatchShiftAuditChanges,
buildTimelineQuickAssignMetadata,
calculateTimelineAllocationPercentage,
shiftTimelineAllocationWindow,
} from "./timeline-allocation-mutation-support.js";
import { calculateTimelineAllocationDailyCost } from "./timeline-cost-support.js";
export const timelineAllocationMutationProcedures = {
@@ -34,22 +49,11 @@ export const timelineAllocationMutationProcedures = {
const newStartDate = input.startDate ?? existing.startDate;
const newEndDate = input.endDate ?? existing.endDate;
if (newEndDate < newStartDate) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "End date must be after start date",
});
}
const existingMeta = (existing.metadata as Record<string, unknown>) ?? {};
const newMeta: Record<string, unknown> = {
...existingMeta,
...(input.includeSaturday !== undefined
? { includeSaturday: input.includeSaturday }
: {}),
};
const includeSaturday =
input.includeSaturday ?? (existingMeta.includeSaturday as boolean | undefined) ?? false;
assertTimelineDateRangeValid(newStartDate, newEndDate);
const { metadata: newMeta, includeSaturday } = buildTimelineAllocationMetadata({
existingMetadata: existing.metadata as Record<string, unknown> | null | undefined,
includeSaturday: input.includeSaturday,
});
let newDailyCostCents = 0;
if (resolved.resourceId) {
@@ -57,9 +61,8 @@ export const timelineAllocationMutationProcedures = {
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
}
const availability =
existingResource.availability as unknown as import("@capakraken/shared").WeekdayAvailability;
const recurrence = newMeta.recurrence as import("@capakraken/shared").RecurrencePattern | undefined;
const availability = existingResource.availability as unknown as WeekdayAvailability;
const recurrence = newMeta.recurrence as RecurrencePattern | undefined;
newDailyCostCents = await calculateTimelineAllocationDailyCost({
db: ctx.db as PrismaClient,
resourceId: resolved.resourceId,
@@ -101,21 +104,17 @@ export const timelineAllocationMutationProcedures = {
entityType: "Allocation",
entityId: input.allocationId,
action: "UPDATE",
changes: {
before: {
id: resolved.entry.id,
hoursPerDay: existing.hoursPerDay,
startDate: existing.startDate,
endDate: existing.endDate,
},
after: {
id: updatedAllocation.id,
hoursPerDay: newHoursPerDay,
startDate: newStartDate,
endDate: newEndDate,
includeSaturday,
},
},
changes: buildTimelineAllocationUpdateAuditChanges({
allocationId: resolved.entry.id,
previousHoursPerDay: existing.hoursPerDay,
previousStartDate: existing.startDate,
previousEndDate: existing.endDate,
nextAllocationId: updatedAllocation.id,
nextHoursPerDay: newHoursPerDay,
nextStartDate: newStartDate,
nextEndDate: newEndDate,
includeSaturday,
}),
},
});
@@ -146,12 +145,10 @@ export const timelineAllocationMutationProcedures = {
)
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
if (input.endDate < input.startDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "End date must be after start date" });
}
assertTimelineDateRangeValid(input.startDate, input.endDate);
const percentage = Math.min(100, Math.round((input.hoursPerDay / 8) * 100));
const metadata = { source: "quickAssign" } satisfies Record<string, unknown>;
const percentage = calculateTimelineAllocationPercentage(input.hoursPerDay);
const metadata = buildTimelineQuickAssignMetadata("quickAssign");
const allocation = await ctx.db.$transaction(async (tx) => {
const assignment = await createAssignment(
@@ -210,24 +207,14 @@ export const timelineAllocationMutationProcedures = {
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
for (const assignment of input.assignments) {
if (assignment.endDate < assignment.startDate) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "End date must be after start date",
});
}
assertTimelineDateRangeValid(assignment.startDate, assignment.endDate);
}
const results = await ctx.db.$transaction(async (tx) => {
const created = [];
for (const assignment of input.assignments) {
const percentage = Math.min(
100,
Math.round((assignment.hoursPerDay / 8) * 100),
);
const metadata = {
source: "batchQuickAssign",
} satisfies Record<string, unknown>;
const percentage = calculateTimelineAllocationPercentage(assignment.hoursPerDay);
const metadata = buildTimelineQuickAssignMetadata("batchQuickAssign");
const createdAssignment = await createAssignment(
tx as unknown as Parameters<typeof createAssignment>[0],
@@ -289,35 +276,24 @@ export const timelineAllocationMutationProcedures = {
const updated = [];
for (const entry of resolved) {
const existing = entry.entry;
const newStart = new Date(existing.startDate);
const newEnd = new Date(existing.endDate);
if (input.mode === "move") {
newStart.setDate(newStart.getDate() + input.daysDelta);
newEnd.setDate(newEnd.getDate() + input.daysDelta);
} else if (input.mode === "resize-start") {
newStart.setDate(newStart.getDate() + input.daysDelta);
if (newStart > newEnd) {
newStart.setTime(newEnd.getTime());
}
} else {
newEnd.setDate(newEnd.getDate() + input.daysDelta);
if (newEnd < newStart) {
newEnd.setTime(newStart.getTime());
}
}
const shiftedWindow = shiftTimelineAllocationWindow({
startDate: existing.startDate,
endDate: existing.endDate,
daysDelta: input.daysDelta,
mode: input.mode,
});
const result = await updateAllocationEntry(
tx as unknown as Parameters<typeof updateAllocationEntry>[0],
{
id: existing.id,
demandRequirementUpdate: {
startDate: newStart,
endDate: newEnd,
startDate: shiftedWindow.startDate,
endDate: shiftedWindow.endDate,
},
assignmentUpdate: {
startDate: newStart,
endDate: newEnd,
startDate: shiftedWindow.startDate,
endDate: shiftedWindow.endDate,
},
},
);
@@ -329,12 +305,11 @@ export const timelineAllocationMutationProcedures = {
entityType: "Allocation",
entityId: input.allocationIds.join(","),
action: "UPDATE",
changes: {
operation: "batchShift",
changes: buildTimelineBatchShiftAuditChanges({
mode: input.mode,
daysDelta: input.daysDelta,
count: resolved.length,
} as unknown as import("@capakraken/db").Prisma.InputJsonValue,
}),
},
});