refactor(api): extract scenario router helpers

This commit is contained in:
2026-03-31 11:03:50 +02:00
parent e013d1af9f
commit 3e0d9d9af7
5 changed files with 705 additions and 632 deletions
+196
View File
@@ -0,0 +1,196 @@
import { calculateAllocation } from "@capakraken/engine/allocation";
import type { WeekdayAvailability } from "@capakraken/shared";
import {
calculateEffectiveBookedHours,
loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import type { TRPCContext } from "../trpc.js";
export type ScenarioDb = TRPCContext["db"];
export type ScenarioChangeInput = {
assignmentId?: string | undefined;
resourceId?: string | undefined;
roleId?: string | undefined;
startDate: Date;
endDate: Date;
hoursPerDay: number;
remove?: boolean | undefined;
};
export type ScenarioAvailability = {
monday: number;
tuesday: number;
wednesday: number;
thursday: number;
friday: number;
saturday: number;
sunday: number;
};
export type ScenarioEntry = {
resourceId: string | null;
lcrCents: number;
hoursPerDay: number;
startDate: Date;
endDate: Date;
availability: ScenarioAvailability;
isNew: boolean;
};
type ScenarioResourceContextInput = {
id: string;
availability: unknown;
countryId: string | null;
country?: { code: string } | null;
federalState: string | null;
metroCityId: string | null;
metroCity?: { name: string } | null;
};
type ScenarioSkillsValue = Array<{ skill?: string | null }> | null | undefined;
export const DEFAULT_AVAILABILITY: ScenarioAvailability = {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
};
export const scenarioRoleSelect = {
id: true,
name: true,
color: true,
} as const;
export const scenarioResourceSelect = {
id: true,
displayName: true,
eid: true,
lcrCents: true,
availability: true,
chargeabilityTarget: true,
skills: true,
countryId: true,
federalState: true,
metroCityId: true,
country: { select: { code: true } },
metroCity: { select: { name: true } },
} as const;
export const scenarioBaselineProjectSelect = {
id: true,
name: true,
shortCode: true,
startDate: true,
endDate: true,
budgetCents: true,
orderType: true,
} as const;
export const scenarioSimulationProjectSelect = {
id: true,
name: true,
budgetCents: true,
orderType: true,
startDate: true,
endDate: true,
} as const;
export const scenarioBaselineAssignmentInclude = {
resource: { select: scenarioResourceSelect },
roleEntity: { select: scenarioRoleSelect },
} as const;
export const scenarioDemandInclude = {
roleEntity: { select: scenarioRoleSelect },
} as const;
export const scenarioSimulationAssignmentInclude = {
resource: { select: scenarioResourceSelect },
} as const;
export const scenarioUtilizationAssignmentSelect = {
id: true,
resourceId: true,
projectId: true,
hoursPerDay: true,
startDate: true,
endDate: true,
} as const;
export function getScenarioAvailability(availability: unknown): ScenarioAvailability {
return (availability as ScenarioAvailability) ?? DEFAULT_AVAILABILITY;
}
export function roundToTenths(value: number): number {
return Math.round(value * 10) / 10;
}
export function collectScenarioSkillSet(skills: ScenarioSkillsValue): Set<string> {
const normalized = new Set<string>();
for (const skill of skills ?? []) {
if (typeof skill?.skill === "string" && skill.skill.trim().length > 0) {
normalized.add(skill.skill.toLowerCase());
}
}
return normalized;
}
function toResourceAvailabilityContextInput(resource: ScenarioResourceContextInput) {
return {
id: resource.id,
availability: resource.availability as WeekdayAvailability,
countryId: resource.countryId,
countryCode: resource.country?.code,
federalState: resource.federalState,
metroCityId: resource.metroCityId,
metroCityName: resource.metroCity?.name,
};
}
export async function loadScenarioAvailabilityContexts(
db: ScenarioDb,
resources: ScenarioResourceContextInput[],
periodStart: Date,
periodEnd: Date,
) {
return loadResourceDailyAvailabilityContexts(
db,
resources.map(toResourceAvailabilityContextInput),
periodStart,
periodEnd,
);
}
export function calculateScenarioEntryHours(
entry: Pick<ScenarioEntry, "resourceId" | "lcrCents" | "hoursPerDay" | "startDate" | "endDate" | "availability">,
options: {
periodStart: Date;
periodEnd: Date;
contexts: Awaited<ReturnType<typeof loadScenarioAvailabilityContexts>>;
},
): number {
if (!entry.resourceId) {
return calculateAllocation({
lcrCents: entry.lcrCents,
hoursPerDay: entry.hoursPerDay,
startDate: entry.startDate,
endDate: entry.endDate,
availability: entry.availability,
}).totalHours;
}
return calculateEffectiveBookedHours({
availability: entry.availability,
startDate: entry.startDate,
endDate: entry.endDate,
hoursPerDay: entry.hoursPerDay,
periodStart: options.periodStart,
periodEnd: options.periodEnd,
context: options.contexts.get(entry.resourceId),
});
}