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