import { buildSplitAllocationReadModel, loadAllocationEntry } from "@capakraken/application"; import { AllocationStatus, CreateDemandRequirementSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js"; import { ASSIGNMENT_INCLUDE, type AllocationEntryUpdateInput, type AllocationListFilters, type AssignmentResolutionInput, type CreateDemandDraftInput, DEMAND_INCLUDE, toIsoDate } from "./allocation-shared.js"; export function toDemandRequirementUpdateInput(input: AllocationEntryUpdateInput) { return { ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), ...(input.startDate !== undefined ? { startDate: input.startDate } : {}), ...(input.endDate !== undefined ? { endDate: input.endDate } : {}), ...(input.hoursPerDay !== undefined ? { hoursPerDay: input.hoursPerDay } : {}), ...(input.percentage !== undefined ? { percentage: input.percentage } : {}), ...(input.role !== undefined ? { role: input.role } : {}), ...(input.roleId !== undefined ? { roleId: input.roleId } : {}), ...(input.headcount !== undefined ? { headcount: input.headcount } : {}), ...(input.budgetCents !== undefined ? { budgetCents: input.budgetCents } : {}), ...(input.status !== undefined ? { status: input.status } : {}), ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), }; } export function toAssignmentUpdateInput(input: AllocationEntryUpdateInput) { return { ...(input.resourceId !== undefined ? { resourceId: input.resourceId } : {}), ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), ...(input.startDate !== undefined ? { startDate: input.startDate } : {}), ...(input.endDate !== undefined ? { endDate: input.endDate } : {}), ...(input.hoursPerDay !== undefined ? { hoursPerDay: input.hoursPerDay } : {}), ...(input.percentage !== undefined ? { percentage: input.percentage } : {}), ...(input.role !== undefined ? { role: input.role } : {}), ...(input.roleId !== undefined ? { roleId: input.roleId } : {}), ...(input.status !== undefined ? { status: input.status } : {}), ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), }; } export async function loadAllocationReadModel( db: Pick, input: AllocationListFilters, ) { const [demandRequirements, assignments] = await Promise.all([ input.resourceId ? Promise.resolve([]) : db.demandRequirement.findMany({ where: { ...(input.projectId ? { projectId: input.projectId } : {}), ...(input.status ? { status: input.status } : {}), }, include: DEMAND_INCLUDE, orderBy: { startDate: "asc" }, }), db.assignment.findMany({ where: { ...(input.projectId ? { projectId: input.projectId } : {}), ...(input.resourceId ? { resourceId: input.resourceId } : {}), ...(input.status ? { status: input.status } : {}), }, include: ASSIGNMENT_INCLUDE, orderBy: { startDate: "asc" }, }), ]); const readModel = buildSplitAllocationReadModel({ demandRequirements, assignments }); const directory = await getAnonymizationDirectory(db as import("@capakraken/db").PrismaClient); if (!directory) { return readModel; } function anonymizeAllocation(allocation: T): T { if (!allocation.resource) { return allocation; } return { ...allocation, resource: anonymizeResource(allocation.resource, directory) }; } return { ...readModel, allocations: readModel.allocations.map(anonymizeAllocation), demands: readModel.demands.map(anonymizeAllocation), assignments: readModel.assignments.map(anonymizeAllocation), }; } export async function findAllocationEntryOrNull( db: Pick, id: string, ) { try { return await loadAllocationEntry(db, id); } catch (error) { if (error instanceof TRPCError && error.code === "NOT_FOUND") { return null; } throw error; } } export function buildCreateDemandRequirementInput(input: CreateDemandDraftInput): z.infer { return { projectId: input.projectId, startDate: input.startDate, endDate: input.endDate, hoursPerDay: input.hoursPerDay, percentage: (input.hoursPerDay / 8) * 100, status: AllocationStatus.PROPOSED, headcount: input.headcount ?? 1, budgetCents: input.budgetCents ?? 0, metadata: input.metadata ?? {}, ...(input.role ? { role: input.role } : {}), ...(input.roleId ? { roleId: input.roleId } : {}), }; } export async function getDemandRequirementByIdOrThrow( db: Pick, id: string, ) { return findUniqueOrThrow( db.demandRequirement.findUnique({ where: { id }, include: DEMAND_INCLUDE, }), "Demand requirement", ); } export async function resolveAssignmentBySelection( db: Pick, input: AssignmentResolutionInput, ) { if (input.assignmentId) { return findUniqueOrThrow( db.assignment.findUnique({ where: { id: input.assignmentId }, include: ASSIGNMENT_INCLUDE, }), "Assignment", ); } if (!input.resourceId || !input.projectId) { throw new TRPCError({ code: "BAD_REQUEST", message: "resourceId and projectId are required when assignmentId is not provided", }); } const assignments = await db.assignment.findMany({ where: { resourceId: input.resourceId, projectId: input.projectId, ...(input.excludeCancelled ? { status: { not: AllocationStatus.CANCELLED } } : {}), }, include: ASSIGNMENT_INCLUDE, orderBy: { startDate: "asc" }, }); const matchingAssignment = assignments .filter((assignment) => { if (input.selectionMode === "WINDOW") { return (!input.startDate || assignment.startDate >= input.startDate) && (!input.endDate || assignment.endDate <= input.endDate); } return !input.startDate || toIsoDate(assignment.startDate) === toIsoDate(input.startDate); }) .sort((left, right) => right.startDate.getTime() - left.startDate.getTime())[0] ?? null; if (!matchingAssignment) { throw new TRPCError({ code: "NOT_FOUND", message: "Assignment not found" }); } return matchingAssignment; }