refactor(api): extract timeline shift support

This commit is contained in:
2026-03-31 14:52:34 +02:00
parent b093a47c1b
commit b669de54e1
3 changed files with 522 additions and 103 deletions
+11 -103
View File
@@ -1,13 +1,10 @@
import { updateAssignment, updateDemandRequirement } from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import { calculateAllocation, validateShift } from "@capakraken/engine";
import { PermissionKey, ShiftProjectSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { emitProjectShifted } from "../sse/event-bus.js";
import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
import { buildAbsenceDays, loadCalculationRules, timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
import { timelineReadProcedures } from "./timeline-read.js";
import { loadProjectShiftContext } from "./timeline-project-read.js";
import { applyTimelineProjectShift } from "./timeline-shift-support.js";
export const timelineRouter = createTRPCRouter({
...timelineReadProcedures,
@@ -22,109 +19,20 @@ export const timelineRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
const { projectId, newStartDate, newEndDate } = input;
const { project, demandRequirements, assignments, shiftPlan } = await loadProjectShiftContext(
ctx.db,
const context = await loadProjectShiftContext(ctx.db, projectId);
const result = await applyTimelineProjectShift({
db: ctx.db,
projectId,
);
const validation = validateShift({
project: {
id: project.id,
budgetCents: project.budgetCents,
winProbability: project.winProbability,
startDate: project.startDate,
endDate: project.endDate,
},
newStartDate,
newEndDate,
allocations: shiftPlan.validationAllocations,
context,
});
if (!validation.valid) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Shift validation failed: ${validation.errors.map((error) => error.message).join(", ")}`,
});
}
emitProjectShifted(result.event);
const shiftRules = await loadCalculationRules(ctx.db as PrismaClient);
const updatedProject = await ctx.db.$transaction(async (tx) => {
const projectRecord = await tx.project.update({
where: { id: projectId },
data: { startDate: newStartDate, endDate: newEndDate },
});
for (const demandRequirement of demandRequirements) {
await updateDemandRequirement(
tx as unknown as Parameters<typeof updateDemandRequirement>[0],
demandRequirement.id,
{
startDate: newStartDate,
endDate: newEndDate,
},
);
}
for (const assignment of assignments) {
const metadata = (assignment.metadata as Record<string, unknown> | null | undefined) ?? {};
const includeSaturday = (metadata.includeSaturday as boolean | undefined) ?? false;
const shiftAbsenceData = await buildAbsenceDays(
ctx.db as PrismaClient,
assignment.resourceId!,
newStartDate,
newEndDate,
);
const newDailyCost = calculateAllocation({
lcrCents: assignment.resource!.lcrCents,
hoursPerDay: assignment.hoursPerDay,
startDate: newStartDate,
endDate: newEndDate,
availability:
assignment.resource!.availability as unknown as import("@capakraken/shared").WeekdayAvailability,
includeSaturday,
vacationDates: shiftAbsenceData.legacyVacationDates,
absenceDays: shiftAbsenceData.absenceDays,
calculationRules: shiftRules,
}).dailyCostCents;
await updateAssignment(
tx as unknown as Parameters<typeof updateAssignment>[0],
assignment.id,
{
startDate: newStartDate,
endDate: newEndDate,
dailyCostCents: newDailyCost,
},
);
}
await tx.auditLog.create({
data: {
entityType: "Project",
entityId: projectId,
action: "SHIFT",
changes: {
before: { startDate: project.startDate, endDate: project.endDate },
after: { startDate: newStartDate, endDate: newEndDate },
costImpact: validation.costImpact,
} as unknown as import("@capakraken/db").Prisma.InputJsonValue,
},
});
return projectRecord;
});
emitProjectShifted({
projectId,
newStartDate: newStartDate.toISOString(),
newEndDate: newEndDate.toISOString(),
costDeltaCents: validation.costImpact.deltaCents,
resourceIds: assignments.map((assignment) => assignment.resourceId),
});
return { project: updatedProject, validation };
return {
project: result.project,
validation: result.validation,
};
}),
});