refactor(api): split resource graph availability
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
import { VacationStatus } from "@capakraken/db";
|
||||||
|
import type { WeekdayAvailability } from "@capakraken/shared";
|
||||||
|
import {
|
||||||
|
asHolidayResolverDb,
|
||||||
|
collectHolidayAvailability,
|
||||||
|
getResolvedCalendarHolidays,
|
||||||
|
} from "../lib/holiday-availability.js";
|
||||||
|
import {
|
||||||
|
calculateEffectiveAvailableHours,
|
||||||
|
countEffectiveWorkingDays,
|
||||||
|
loadResourceDailyAvailabilityContexts,
|
||||||
|
} from "../lib/resource-capacity.js";
|
||||||
|
|
||||||
|
type ResourceLocation = {
|
||||||
|
countryId: string | null;
|
||||||
|
federalState: string | null;
|
||||||
|
metroCityId: string | null;
|
||||||
|
country?: { code?: string | null; name?: string | null } | null;
|
||||||
|
metroCity?: { name?: string | null } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResourceAvailabilityInput = {
|
||||||
|
db: any;
|
||||||
|
resource: ResourceLocation & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
weeklyAvailability: WeekdayAvailability;
|
||||||
|
monthStart: Date;
|
||||||
|
monthEnd: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAvailabilityHoursForDate(
|
||||||
|
availability: WeekdayAvailability,
|
||||||
|
date: Date,
|
||||||
|
): number {
|
||||||
|
const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability;
|
||||||
|
return availability[dayKey] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumAvailabilityHoursForDates(
|
||||||
|
availability: WeekdayAvailability,
|
||||||
|
dates: Date[],
|
||||||
|
): number {
|
||||||
|
return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadResourceGraphAvailability(input: ResourceAvailabilityInput) {
|
||||||
|
const { db, resource, weeklyAvailability, monthStart, monthEnd } = input;
|
||||||
|
|
||||||
|
const vacations = await db.vacation.findMany({
|
||||||
|
where: {
|
||||||
|
resourceId: resource.id,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
startDate: { lte: monthEnd },
|
||||||
|
endDate: { gte: monthStart },
|
||||||
|
},
|
||||||
|
select: { startDate: true, endDate: true, type: true, isHalfDay: true },
|
||||||
|
});
|
||||||
|
const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(db), {
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
countryId: resource.countryId,
|
||||||
|
countryCode: resource.country?.code,
|
||||||
|
federalState: resource.federalState,
|
||||||
|
metroCityId: resource.metroCityId,
|
||||||
|
metroCityName: resource.metroCity?.name,
|
||||||
|
});
|
||||||
|
const holidayAvailability = collectHolidayAvailability({
|
||||||
|
vacations,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
countryCode: resource.country?.code,
|
||||||
|
federalState: resource.federalState,
|
||||||
|
metroCityName: resource.metroCity?.name,
|
||||||
|
resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date),
|
||||||
|
});
|
||||||
|
const publicHolidayStrings = holidayAvailability.publicHolidayStrings;
|
||||||
|
const absenceDateStrings = holidayAvailability.absenceDateStrings;
|
||||||
|
const absenceDays = holidayAvailability.absenceDays;
|
||||||
|
const halfDayCount = absenceDays.filter((absence) => absence.isHalfDay).length;
|
||||||
|
const vacationDayCount = absenceDays.filter((absence) => absence.type === "VACATION").length;
|
||||||
|
const sickDayCount = absenceDays.filter((absence) => absence.type === "SICK").length;
|
||||||
|
const publicHolidayCount = resolvedHolidays.length;
|
||||||
|
const absenceDayEquivalent = absenceDays.reduce((sum, absence) => {
|
||||||
|
if (absence.type === "PUBLIC_HOLIDAY") {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
return sum + (absence.isHalfDay ? 0.5 : 1);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||||
|
db,
|
||||||
|
[{
|
||||||
|
id: resource.id,
|
||||||
|
availability: weeklyAvailability,
|
||||||
|
countryId: resource.countryId,
|
||||||
|
countryCode: resource.country?.code,
|
||||||
|
federalState: resource.federalState,
|
||||||
|
metroCityId: resource.metroCityId,
|
||||||
|
metroCityName: resource.metroCity?.name,
|
||||||
|
}],
|
||||||
|
monthStart,
|
||||||
|
monthEnd,
|
||||||
|
);
|
||||||
|
const availabilityContext = contexts.get(resource.id);
|
||||||
|
|
||||||
|
const baseWorkingDays = countEffectiveWorkingDays({
|
||||||
|
availability: weeklyAvailability,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
context: undefined,
|
||||||
|
});
|
||||||
|
const effectiveWorkingDays = countEffectiveWorkingDays({
|
||||||
|
availability: weeklyAvailability,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
context: availabilityContext,
|
||||||
|
});
|
||||||
|
const baseAvailableHours = calculateEffectiveAvailableHours({
|
||||||
|
availability: weeklyAvailability,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
context: undefined,
|
||||||
|
});
|
||||||
|
const effectiveAvailableHours = calculateEffectiveAvailableHours({
|
||||||
|
availability: weeklyAvailability,
|
||||||
|
periodStart: monthStart,
|
||||||
|
periodEnd: monthEnd,
|
||||||
|
context: availabilityContext,
|
||||||
|
});
|
||||||
|
const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`));
|
||||||
|
const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => (
|
||||||
|
count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0)
|
||||||
|
), 0);
|
||||||
|
const publicHolidayHoursDeduction = sumAvailabilityHoursForDates(
|
||||||
|
weeklyAvailability,
|
||||||
|
publicHolidayDates,
|
||||||
|
);
|
||||||
|
const absenceHoursDeduction = absenceDays.reduce((sum, absence) => {
|
||||||
|
if (absence.type === "PUBLIC_HOLIDAY") {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date);
|
||||||
|
return sum + baseHours * (absence.isHalfDay ? 0.5 : 1);
|
||||||
|
}, 0);
|
||||||
|
const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0
|
||||||
|
? effectiveAvailableHours / effectiveWorkingDays
|
||||||
|
: 0;
|
||||||
|
const holidayScopeSummary = [
|
||||||
|
resource.country?.code ?? "—",
|
||||||
|
resource.federalState ?? "—",
|
||||||
|
resource.metroCity?.name ?? "—",
|
||||||
|
].join(" / ");
|
||||||
|
const holidayExamples = resolvedHolidays.length > 0
|
||||||
|
? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ")
|
||||||
|
: "none";
|
||||||
|
const holidayScopeBreakdown = resolvedHolidays.reduce<Record<string, number>>((counts, holiday) => {
|
||||||
|
counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1;
|
||||||
|
return counts;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
absenceDateStrings,
|
||||||
|
absenceDayEquivalent,
|
||||||
|
absenceDays,
|
||||||
|
absenceHoursDeduction,
|
||||||
|
availabilityContext,
|
||||||
|
baseAvailableHours,
|
||||||
|
baseWorkingDays,
|
||||||
|
effectiveAvailableHours,
|
||||||
|
effectiveHoursPerWorkingDay,
|
||||||
|
effectiveWorkingDays,
|
||||||
|
halfDayCount,
|
||||||
|
holidayExamples,
|
||||||
|
holidayScopeBreakdown,
|
||||||
|
holidayScopeSummary,
|
||||||
|
publicHolidayCount,
|
||||||
|
publicHolidayHoursDeduction,
|
||||||
|
publicHolidayStrings,
|
||||||
|
publicHolidayWorkdayCount,
|
||||||
|
resolvedHolidays,
|
||||||
|
sickDayCount,
|
||||||
|
vacationDayCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,19 +7,9 @@ import {
|
|||||||
type AssignmentSlice,
|
type AssignmentSlice,
|
||||||
} from "@capakraken/engine";
|
} from "@capakraken/engine";
|
||||||
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
|
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
|
||||||
import { VacationStatus } from "@capakraken/db";
|
|
||||||
import type { TRPCContext } from "../trpc.js";
|
import type { TRPCContext } from "../trpc.js";
|
||||||
import { fmtEur } from "../lib/format-utils.js";
|
import { fmtEur } from "../lib/format-utils.js";
|
||||||
import {
|
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
|
||||||
asHolidayResolverDb,
|
|
||||||
collectHolidayAvailability,
|
|
||||||
getResolvedCalendarHolidays,
|
|
||||||
} from "../lib/holiday-availability.js";
|
|
||||||
import {
|
|
||||||
calculateEffectiveAvailableHours,
|
|
||||||
countEffectiveWorkingDays,
|
|
||||||
loadResourceDailyAvailabilityContexts,
|
|
||||||
} from "../lib/resource-capacity.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";
|
||||||
|
|
||||||
@@ -28,21 +18,6 @@ type ResourceGraphInput = {
|
|||||||
month: string;
|
month: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAvailabilityHoursForDate(
|
|
||||||
availability: WeekdayAvailability,
|
|
||||||
date: Date,
|
|
||||||
): number {
|
|
||||||
const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability;
|
|
||||||
return availability[dayKey] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sumAvailabilityHoursForDates(
|
|
||||||
availability: WeekdayAvailability,
|
|
||||||
dates: Date[],
|
|
||||||
): number {
|
|
||||||
return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readResourceGraphSnapshot(
|
export async function readResourceGraphSnapshot(
|
||||||
ctx: { db: TRPCContext["db"] },
|
ctx: { db: TRPCContext["db"] },
|
||||||
input: ResourceGraphInput,
|
input: ResourceGraphInput,
|
||||||
@@ -106,62 +81,34 @@ export async function readResourceGraphSnapshot(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const vacations = await ctx.db.vacation.findMany({
|
const {
|
||||||
where: {
|
absenceDateStrings,
|
||||||
resourceId: input.resourceId,
|
absenceDayEquivalent,
|
||||||
status: VacationStatus.APPROVED,
|
absenceDays,
|
||||||
startDate: { lte: monthEnd },
|
absenceHoursDeduction,
|
||||||
endDate: { gte: monthStart },
|
baseAvailableHours,
|
||||||
},
|
baseWorkingDays,
|
||||||
select: { startDate: true, endDate: true, type: true, isHalfDay: true },
|
effectiveAvailableHours,
|
||||||
});
|
effectiveHoursPerWorkingDay,
|
||||||
const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
|
effectiveWorkingDays,
|
||||||
periodStart: monthStart,
|
halfDayCount,
|
||||||
periodEnd: monthEnd,
|
holidayExamples,
|
||||||
countryId: resource.countryId,
|
holidayScopeBreakdown,
|
||||||
countryCode: resource.country?.code,
|
holidayScopeSummary,
|
||||||
federalState: resource.federalState,
|
publicHolidayCount,
|
||||||
metroCityId: resource.metroCityId,
|
publicHolidayHoursDeduction,
|
||||||
metroCityName: resource.metroCity?.name,
|
publicHolidayStrings,
|
||||||
});
|
publicHolidayWorkdayCount,
|
||||||
const holidayAvailability = collectHolidayAvailability({
|
resolvedHolidays,
|
||||||
vacations,
|
sickDayCount,
|
||||||
periodStart: monthStart,
|
vacationDayCount,
|
||||||
periodEnd: monthEnd,
|
} = await loadResourceGraphAvailability({
|
||||||
countryCode: resource.country?.code,
|
db: ctx.db,
|
||||||
federalState: resource.federalState,
|
resource,
|
||||||
metroCityName: resource.metroCity?.name,
|
weeklyAvailability,
|
||||||
resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date),
|
|
||||||
});
|
|
||||||
const publicHolidayStrings = holidayAvailability.publicHolidayStrings;
|
|
||||||
const absenceDateStrings = holidayAvailability.absenceDateStrings;
|
|
||||||
const absenceDays = holidayAvailability.absenceDays;
|
|
||||||
const halfDayCount = absenceDays.filter((absence) => absence.isHalfDay).length;
|
|
||||||
const vacationDayCount = absenceDays.filter((absence) => absence.type === "VACATION").length;
|
|
||||||
const sickDayCount = absenceDays.filter((absence) => absence.type === "SICK").length;
|
|
||||||
const publicHolidayCount = resolvedHolidays.length;
|
|
||||||
const absenceDayEquivalent = absenceDays.reduce((sum, absence) => {
|
|
||||||
if (absence.type === "PUBLIC_HOLIDAY") {
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
return sum + (absence.isHalfDay ? 0.5 : 1);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
|
||||||
ctx.db,
|
|
||||||
[{
|
|
||||||
id: resource.id,
|
|
||||||
availability: weeklyAvailability,
|
|
||||||
countryId: resource.countryId,
|
|
||||||
countryCode: resource.country?.code,
|
|
||||||
federalState: resource.federalState,
|
|
||||||
metroCityId: resource.metroCityId,
|
|
||||||
metroCityName: resource.metroCity?.name,
|
|
||||||
}],
|
|
||||||
monthStart,
|
monthStart,
|
||||||
monthEnd,
|
monthEnd,
|
||||||
);
|
});
|
||||||
const availabilityContext = contexts.get(resource.id);
|
|
||||||
|
|
||||||
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
|
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
|
||||||
try {
|
try {
|
||||||
@@ -185,60 +132,6 @@ export async function readResourceGraphSnapshot(
|
|||||||
publicHolidays: publicHolidayStrings,
|
publicHolidays: publicHolidayStrings,
|
||||||
absenceDays: absenceDateStrings,
|
absenceDays: absenceDateStrings,
|
||||||
});
|
});
|
||||||
const baseWorkingDays = countEffectiveWorkingDays({
|
|
||||||
availability: weeklyAvailability,
|
|
||||||
periodStart: monthStart,
|
|
||||||
periodEnd: monthEnd,
|
|
||||||
context: undefined,
|
|
||||||
});
|
|
||||||
const effectiveWorkingDays = countEffectiveWorkingDays({
|
|
||||||
availability: weeklyAvailability,
|
|
||||||
periodStart: monthStart,
|
|
||||||
periodEnd: monthEnd,
|
|
||||||
context: availabilityContext,
|
|
||||||
});
|
|
||||||
const baseAvailableHours = calculateEffectiveAvailableHours({
|
|
||||||
availability: weeklyAvailability,
|
|
||||||
periodStart: monthStart,
|
|
||||||
periodEnd: monthEnd,
|
|
||||||
context: undefined,
|
|
||||||
});
|
|
||||||
const effectiveAvailableHours = calculateEffectiveAvailableHours({
|
|
||||||
availability: weeklyAvailability,
|
|
||||||
periodStart: monthStart,
|
|
||||||
periodEnd: monthEnd,
|
|
||||||
context: availabilityContext,
|
|
||||||
});
|
|
||||||
const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`));
|
|
||||||
const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => (
|
|
||||||
count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0)
|
|
||||||
), 0);
|
|
||||||
const publicHolidayHoursDeduction = sumAvailabilityHoursForDates(
|
|
||||||
weeklyAvailability,
|
|
||||||
publicHolidayDates,
|
|
||||||
);
|
|
||||||
const absenceHoursDeduction = absenceDays.reduce((sum, absence) => {
|
|
||||||
if (absence.type === "PUBLIC_HOLIDAY") {
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date);
|
|
||||||
return sum + baseHours * (absence.isHalfDay ? 0.5 : 1);
|
|
||||||
}, 0);
|
|
||||||
const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0
|
|
||||||
? effectiveAvailableHours / effectiveWorkingDays
|
|
||||||
: 0;
|
|
||||||
const holidayScopeSummary = [
|
|
||||||
resource.country?.code ?? "—",
|
|
||||||
resource.federalState ?? "—",
|
|
||||||
resource.metroCity?.name ?? "—",
|
|
||||||
].join(" / ");
|
|
||||||
const holidayExamples = resolvedHolidays.length > 0
|
|
||||||
? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ")
|
|
||||||
: "none";
|
|
||||||
const holidayScopeBreakdown = resolvedHolidays.reduce<Record<string, number>>((counts, holiday) => {
|
|
||||||
counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1;
|
|
||||||
return counts;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const slices: AssignmentSlice[] = [];
|
const slices: AssignmentSlice[] = [];
|
||||||
const assignmentBreakdown: Array<{
|
const assignmentBreakdown: Array<{
|
||||||
|
|||||||
Reference in New Issue
Block a user