179 lines
6.7 KiB
TypeScript
179 lines
6.7 KiB
TypeScript
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<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment" | "systemSettings" | "resource">,
|
|
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<T extends { resource?: { id: string; eid?: string | null; displayName?: string | null; email?: string | null } | null }>(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<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
|
|
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<typeof CreateDemandRequirementSchema> {
|
|
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<import("@capakraken/db").PrismaClient, "demandRequirement">,
|
|
id: string,
|
|
) {
|
|
return findUniqueOrThrow(
|
|
db.demandRequirement.findUnique({
|
|
where: { id },
|
|
include: DEMAND_INCLUDE,
|
|
}),
|
|
"Demand requirement",
|
|
);
|
|
}
|
|
|
|
export async function resolveAssignmentBySelection(
|
|
db: Pick<import("@capakraken/db").PrismaClient, "assignment">,
|
|
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;
|
|
}
|