feat(planning): ship holiday-aware planning and assistant upgrades

This commit is contained in:
2026-03-28 22:49:28 +01:00
parent 2a005794e7
commit 4f48afe7b4
151 changed files with 17738 additions and 1940 deletions
+48 -11
View File
@@ -1,6 +1,11 @@
import { listAssignmentBookings } from "@capakraken/application";
import { rankResources } from "@capakraken/staffing";
import type { SkillEntry } from "@capakraken/shared";
import type { SkillEntry, WeekdayAvailability } from "@capakraken/shared";
import {
calculateEffectiveAvailableHours,
calculateEffectiveBookedHours,
loadResourceDailyAvailabilityContexts,
} from "./resource-capacity.js";
import { createNotificationsForUsers } from "./create-notification.js";
/**
@@ -58,6 +63,11 @@ type DbClient = Parameters<typeof listAssignmentBookings>[0] & {
chargeabilityTarget: number;
availability: unknown;
valueScore: number | null;
countryId: string | null;
federalState: string | null;
metroCityId: string | null;
country: { code: string | null } | null;
metroCity: { name: string | null } | null;
}>>;
};
notification: {
@@ -154,27 +164,54 @@ export async function generateAutoSuggestions(
endDate: demand.endDate,
resourceIds: resources.map((r) => r.id),
});
const contexts = await loadResourceDailyAvailabilityContexts(
db as Parameters<typeof loadResourceDailyAvailabilityContexts>[0],
resources.map((resource) => ({
id: resource.id,
availability: resource.availability as unknown as WeekdayAvailability,
countryId: resource.countryId,
countryCode: resource.country?.code,
federalState: resource.federalState,
metroCityId: resource.metroCityId,
metroCityName: resource.metroCity?.name,
})),
demand.startDate,
demand.endDate,
);
// 5. Enrich resources with utilization data for the demand's date range
const enrichedResources = resources.map((resource) => {
const avail = resource.availability as
| { monday?: number; tuesday?: number; wednesday?: number; thursday?: number; friday?: number }
| null;
const totalAvailableHours = avail?.monday ?? 8;
const availability = resource.availability as unknown as WeekdayAvailability;
const context = contexts.get(resource.id);
const resourceBookings = bookings.filter((b) => b.resourceId === resource.id);
const allocatedHoursPerDay = resourceBookings.reduce(
(sum, b) => sum + b.hoursPerDay,
const totalAvailableHours = calculateEffectiveAvailableHours({
availability,
periodStart: demand.startDate,
periodEnd: demand.endDate,
context,
});
const allocatedHours = resourceBookings.reduce(
(sum, booking) =>
sum + calculateEffectiveBookedHours({
availability,
startDate: booking.startDate,
endDate: booking.endDate,
hoursPerDay: booking.hoursPerDay,
periodStart: demand.startDate,
periodEnd: demand.endDate,
context,
}),
0,
);
const utilizationPercent =
totalAvailableHours > 0
? Math.min(100, (allocatedHoursPerDay / totalAvailableHours) * 100)
? Math.min(100, (allocatedHours / totalAvailableHours) * 100)
: 0;
const wouldExceedCapacity =
allocatedHoursPerDay + demand.hoursPerDay > totalAvailableHours;
const wouldExceedCapacity = totalAvailableHours > 0
? allocatedHours + demand.hoursPerDay > totalAvailableHours
: demand.hoursPerDay > 0;
return {
id: resource.id,