refactor(api): extract timeline shift support
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user