197 lines
4.8 KiB
TypeScript
197 lines
4.8 KiB
TypeScript
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),
|
|
});
|
|
}
|