refactor(api): split resource graph snapshot loading
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
calculateSAH,
|
||||||
|
getMonthRange,
|
||||||
|
DEFAULT_CALCULATION_RULES,
|
||||||
|
} from "@capakraken/engine";
|
||||||
|
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
|
||||||
|
import type { TRPCContext } from "../trpc.js";
|
||||||
|
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
|
||||||
|
|
||||||
|
export type ResourceGraphInput = {
|
||||||
|
resourceId: string;
|
||||||
|
month: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadResourceGraphSnapshot(
|
||||||
|
ctx: { db: TRPCContext["db"] },
|
||||||
|
input: ResourceGraphInput,
|
||||||
|
) {
|
||||||
|
const [year, month] = input.month.split("-").map(Number) as [number, number];
|
||||||
|
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
|
||||||
|
|
||||||
|
const resource = await ctx.db.resource.findUniqueOrThrow({
|
||||||
|
where: { id: input.resourceId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
displayName: true,
|
||||||
|
eid: true,
|
||||||
|
fte: true,
|
||||||
|
lcrCents: true,
|
||||||
|
chargeabilityTarget: true,
|
||||||
|
countryId: true,
|
||||||
|
federalState: true,
|
||||||
|
metroCityId: true,
|
||||||
|
availability: true,
|
||||||
|
country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
|
||||||
|
metroCity: { select: { id: true, name: true } },
|
||||||
|
managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dailyHours = resource.country?.dailyWorkingHours ?? 8;
|
||||||
|
const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
|
||||||
|
const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);
|
||||||
|
|
||||||
|
const availability = resource.availability as WeekdayAvailability | null;
|
||||||
|
const weeklyAvailability: WeekdayAvailability = availability ?? {
|
||||||
|
monday: dailyHours,
|
||||||
|
tuesday: dailyHours,
|
||||||
|
wednesday: dailyHours,
|
||||||
|
thursday: dailyHours,
|
||||||
|
friday: dailyHours,
|
||||||
|
saturday: 0,
|
||||||
|
sunday: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignments = await ctx.db.assignment.findMany({
|
||||||
|
where: {
|
||||||
|
resourceId: input.resourceId,
|
||||||
|
startDate: { lte: monthEnd },
|
||||||
|
endDate: { gte: monthStart },
|
||||||
|
status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
hoursPerDay: true,
|
||||||
|
startDate: true,
|
||||||
|
endDate: true,
|
||||||
|
dailyCostCents: true,
|
||||||
|
status: true,
|
||||||
|
project: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
shortCode: true,
|
||||||
|
budgetCents: true,
|
||||||
|
winProbability: true,
|
||||||
|
utilizationCategory: { select: { code: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const availabilitySummary = await loadResourceGraphAvailability({
|
||||||
|
db: ctx.db,
|
||||||
|
resource,
|
||||||
|
weeklyAvailability,
|
||||||
|
monthStart,
|
||||||
|
monthEnd,
|
||||||
|
});
|
||||||
|
|
||||||
|
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
|
||||||
|
try {
|
||||||
|
const dbRules = await ctx.db.calculationRule.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ priority: "desc" }],
|
||||||
|
});
|
||||||
|
if (dbRules.length > 0) {
|
||||||
|
calcRules = dbRules as unknown as CalculationRule[];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// table may not exist yet
|
||||||
|
}
|
||||||
|
|
||||||
|
const sahResult = calculateSAH({
|
||||||
|
dailyWorkingHours: dailyHours,
|
||||||
|
scheduleRules,
|
||||||
|
fte: resource.fte,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
publicHolidays: availabilitySummary.publicHolidayStrings,
|
||||||
|
absenceDays: availabilitySummary.absenceDateStrings,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
assignments,
|
||||||
|
calcRules,
|
||||||
|
dailyHours,
|
||||||
|
monthEnd,
|
||||||
|
monthStart,
|
||||||
|
resource,
|
||||||
|
sahResult,
|
||||||
|
scheduleRules,
|
||||||
|
targetPct,
|
||||||
|
weeklyAvailability,
|
||||||
|
...availabilitySummary,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,90 +1,25 @@
|
|||||||
import {
|
|
||||||
calculateSAH,
|
|
||||||
getMonthRange,
|
|
||||||
DEFAULT_CALCULATION_RULES,
|
|
||||||
} from "@capakraken/engine";
|
|
||||||
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
|
|
||||||
import type { TRPCContext } from "../trpc.js";
|
|
||||||
import { buildResourceAllocationSummary } from "./computation-graph-resource-allocation.js";
|
import { buildResourceAllocationSummary } from "./computation-graph-resource-allocation.js";
|
||||||
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
|
|
||||||
import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js";
|
import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js";
|
||||||
import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js";
|
import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js";
|
||||||
|
import {
|
||||||
type ResourceGraphInput = {
|
loadResourceGraphSnapshot,
|
||||||
resourceId: string;
|
type ResourceGraphInput,
|
||||||
month: string;
|
} from "./computation-graph-resource-snapshot.js";
|
||||||
};
|
|
||||||
|
|
||||||
export async function readResourceGraphSnapshot(
|
export async function readResourceGraphSnapshot(
|
||||||
ctx: { db: TRPCContext["db"] },
|
ctx: Parameters<typeof loadResourceGraphSnapshot>[0],
|
||||||
input: ResourceGraphInput,
|
input: ResourceGraphInput,
|
||||||
) {
|
) {
|
||||||
const [year, month] = input.month.split("-").map(Number) as [number, number];
|
|
||||||
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
|
|
||||||
|
|
||||||
const resource = await ctx.db.resource.findUniqueOrThrow({
|
|
||||||
where: { id: input.resourceId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
displayName: true,
|
|
||||||
eid: true,
|
|
||||||
fte: true,
|
|
||||||
lcrCents: true,
|
|
||||||
chargeabilityTarget: true,
|
|
||||||
countryId: true,
|
|
||||||
federalState: true,
|
|
||||||
metroCityId: true,
|
|
||||||
availability: true,
|
|
||||||
country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
|
|
||||||
metroCity: { select: { id: true, name: true } },
|
|
||||||
managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const dailyHours = resource.country?.dailyWorkingHours ?? 8;
|
|
||||||
const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
|
|
||||||
const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);
|
|
||||||
|
|
||||||
const avail = resource.availability as WeekdayAvailability | null;
|
|
||||||
const weeklyAvailability: WeekdayAvailability = avail ?? {
|
|
||||||
monday: dailyHours, tuesday: dailyHours, wednesday: dailyHours,
|
|
||||||
thursday: dailyHours, friday: dailyHours, saturday: 0, sunday: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const assignments = await ctx.db.assignment.findMany({
|
|
||||||
where: {
|
|
||||||
resourceId: input.resourceId,
|
|
||||||
startDate: { lte: monthEnd },
|
|
||||||
endDate: { gte: monthStart },
|
|
||||||
status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
hoursPerDay: true,
|
|
||||||
startDate: true,
|
|
||||||
endDate: true,
|
|
||||||
dailyCostCents: true,
|
|
||||||
status: true,
|
|
||||||
project: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
shortCode: true,
|
|
||||||
budgetCents: true,
|
|
||||||
winProbability: true,
|
|
||||||
utilizationCategory: { select: { code: true } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
absenceDateStrings,
|
absenceDateStrings,
|
||||||
absenceDayEquivalent,
|
absenceDayEquivalent,
|
||||||
absenceDays,
|
absenceDays,
|
||||||
absenceHoursDeduction,
|
absenceHoursDeduction,
|
||||||
|
assignments,
|
||||||
baseAvailableHours,
|
baseAvailableHours,
|
||||||
baseWorkingDays,
|
baseWorkingDays,
|
||||||
|
calcRules,
|
||||||
|
dailyHours,
|
||||||
effectiveAvailableHours,
|
effectiveAvailableHours,
|
||||||
effectiveHoursPerWorkingDay,
|
effectiveHoursPerWorkingDay,
|
||||||
effectiveWorkingDays,
|
effectiveWorkingDays,
|
||||||
@@ -92,55 +27,32 @@ export async function readResourceGraphSnapshot(
|
|||||||
holidayExamples,
|
holidayExamples,
|
||||||
holidayScopeBreakdown,
|
holidayScopeBreakdown,
|
||||||
holidayScopeSummary,
|
holidayScopeSummary,
|
||||||
|
monthEnd,
|
||||||
|
monthStart,
|
||||||
publicHolidayCount,
|
publicHolidayCount,
|
||||||
publicHolidayHoursDeduction,
|
publicHolidayHoursDeduction,
|
||||||
publicHolidayStrings,
|
|
||||||
publicHolidayWorkdayCount,
|
publicHolidayWorkdayCount,
|
||||||
resolvedHolidays,
|
resolvedHolidays,
|
||||||
sickDayCount,
|
|
||||||
vacationDayCount,
|
|
||||||
} = await loadResourceGraphAvailability({
|
|
||||||
db: ctx.db,
|
|
||||||
resource,
|
resource,
|
||||||
weeklyAvailability,
|
sahResult,
|
||||||
monthStart,
|
|
||||||
monthEnd,
|
|
||||||
});
|
|
||||||
|
|
||||||
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
|
|
||||||
try {
|
|
||||||
const dbRules = await ctx.db.calculationRule.findMany({
|
|
||||||
where: { isActive: true },
|
|
||||||
orderBy: [{ priority: "desc" }],
|
|
||||||
});
|
|
||||||
if (dbRules.length > 0) {
|
|
||||||
calcRules = dbRules as unknown as CalculationRule[];
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// table may not exist yet
|
|
||||||
}
|
|
||||||
|
|
||||||
const sahResult = calculateSAH({
|
|
||||||
dailyWorkingHours: dailyHours,
|
|
||||||
scheduleRules,
|
scheduleRules,
|
||||||
fte: resource.fte,
|
sickDayCount,
|
||||||
periodStart: monthStart,
|
|
||||||
periodEnd: monthEnd,
|
|
||||||
publicHolidays: publicHolidayStrings,
|
|
||||||
absenceDays: absenceDateStrings,
|
|
||||||
});
|
|
||||||
|
|
||||||
const assignmentSummary = buildResourceAllocationSummary({
|
|
||||||
assignments,
|
|
||||||
effectiveAvailableHours,
|
|
||||||
monthStart,
|
|
||||||
monthEnd,
|
|
||||||
resourceLcrCents: resource.lcrCents,
|
|
||||||
resourceFte: resource.fte,
|
|
||||||
targetPct,
|
targetPct,
|
||||||
|
vacationDayCount,
|
||||||
weeklyAvailability,
|
weeklyAvailability,
|
||||||
|
} = await loadResourceGraphSnapshot(ctx, input);
|
||||||
|
|
||||||
|
const allocationSummary = buildResourceAllocationSummary({
|
||||||
|
assignments,
|
||||||
absenceDays,
|
absenceDays,
|
||||||
calcRules,
|
calcRules,
|
||||||
|
effectiveAvailableHours,
|
||||||
|
monthEnd,
|
||||||
|
monthStart,
|
||||||
|
resourceFte: resource.fte,
|
||||||
|
resourceLcrCents: resource.lcrCents,
|
||||||
|
targetPct,
|
||||||
|
weeklyAvailability,
|
||||||
});
|
});
|
||||||
const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph(
|
const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
@@ -176,17 +88,17 @@ export async function readResourceGraphSnapshot(
|
|||||||
baseAvailableHours,
|
baseAvailableHours,
|
||||||
effectiveAvailableHours,
|
effectiveAvailableHours,
|
||||||
effectiveHoursPerWorkingDay,
|
effectiveHoursPerWorkingDay,
|
||||||
totalWorkingDaysInMonth: assignmentSummary.totalWorkingDaysInMonth,
|
totalWorkingDaysInMonth: allocationSummary.totalWorkingDaysInMonth,
|
||||||
totalAllocHours: assignmentSummary.totalAllocHours,
|
totalAllocHours: allocationSummary.totalAllocHours,
|
||||||
totalAllocCostCents: assignmentSummary.totalAllocCostCents,
|
totalAllocCostCents: allocationSummary.totalAllocCostCents,
|
||||||
totalChargeableHours: assignmentSummary.totalChargeableHours,
|
totalChargeableHours: allocationSummary.totalChargeableHours,
|
||||||
totalProjectCostCents: assignmentSummary.totalProjectCostCents,
|
totalProjectCostCents: allocationSummary.totalProjectCostCents,
|
||||||
hasRulesEffect: assignmentSummary.hasRulesEffect,
|
hasRulesEffect: allocationSummary.hasRulesEffect,
|
||||||
dailyCostCents: assignmentSummary.dailyCostCents,
|
dailyCostCents: allocationSummary.dailyCostCents,
|
||||||
avgHoursPerDay: assignmentSummary.avgHoursPerDay,
|
avgHoursPerDay: allocationSummary.avgHoursPerDay,
|
||||||
utilizationPct: assignmentSummary.utilizationPct,
|
utilizationPct: allocationSummary.utilizationPct,
|
||||||
forecast: assignmentSummary.forecast,
|
forecast: allocationSummary.forecast,
|
||||||
chargeableHours: assignmentSummary.chargeableHours,
|
chargeableHours: allocationSummary.chargeableHours,
|
||||||
budgetNodes,
|
budgetNodes,
|
||||||
budgetLinks,
|
budgetLinks,
|
||||||
resolvedHolidays: resolvedHolidays.map((holiday) => ({
|
resolvedHolidays: resolvedHolidays.map((holiday) => ({
|
||||||
@@ -196,7 +108,7 @@ export async function readResourceGraphSnapshot(
|
|||||||
calendarName: holiday.calendarName,
|
calendarName: holiday.calendarName,
|
||||||
sourceType: holiday.sourceType ?? null,
|
sourceType: holiday.sourceType ?? null,
|
||||||
})),
|
})),
|
||||||
assignmentBreakdown: assignmentSummary.assignmentBreakdown,
|
assignmentBreakdown: allocationSummary.assignmentBreakdown,
|
||||||
absenceDayEquivalent,
|
absenceDayEquivalent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user