feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
FillDemandRequirementSchema,
|
||||
FillOpenDemandByAllocationSchema,
|
||||
PermissionKey,
|
||||
type WeekdayAvailability,
|
||||
UpdateAssignmentSchema,
|
||||
UpdateAllocationSchema,
|
||||
UpdateDemandRequirementSchema,
|
||||
@@ -34,6 +35,13 @@ import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
|
||||
import { emitAllocationCreated, emitAllocationDeleted, emitAllocationUpdated, emitNotificationCreated } from "../sse/event-bus.js";
|
||||
import { generateAutoSuggestions } from "../lib/auto-staffing.js";
|
||||
import { invalidateDashboardCache } from "../lib/cache.js";
|
||||
import {
|
||||
calculateEffectiveAvailableHours,
|
||||
calculateEffectiveBookedHours,
|
||||
calculateEffectiveDayAvailability,
|
||||
countEffectiveWorkingDays,
|
||||
loadResourceDailyAvailabilityContexts,
|
||||
} from "../lib/resource-capacity.js";
|
||||
import { createTRPCRouter, managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
|
||||
import { PROJECT_BRIEF_SELECT, RESOURCE_BRIEF_SELECT, ROLE_BRIEF_SELECT } from "../db/selects.js";
|
||||
|
||||
@@ -328,12 +336,26 @@ export const allocationRouter = createTRPCRouter({
|
||||
where: { id: input.resourceId },
|
||||
select: {
|
||||
id: true, displayName: true, eid: true, fte: true,
|
||||
country: { select: { dailyWorkingHours: true } },
|
||||
availability: true,
|
||||
countryId: true,
|
||||
federalState: true,
|
||||
metroCityId: true,
|
||||
country: { select: { dailyWorkingHours: true, code: true } },
|
||||
metroCity: { select: { name: true } },
|
||||
},
|
||||
});
|
||||
if (!resource) throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
||||
|
||||
const dailyCapacity = (resource.country?.dailyWorkingHours ?? 8) * (resource.fte ?? 1);
|
||||
const fallbackDailyHours = (resource.country?.dailyWorkingHours ?? 8) * (resource.fte ?? 1);
|
||||
const availability = (resource.availability as WeekdayAvailability | null) ?? {
|
||||
monday: fallbackDailyHours,
|
||||
tuesday: fallbackDailyHours,
|
||||
wednesday: fallbackDailyHours,
|
||||
thursday: fallbackDailyHours,
|
||||
friday: fallbackDailyHours,
|
||||
saturday: 0,
|
||||
sunday: 0,
|
||||
};
|
||||
|
||||
// Get existing assignments in the date range
|
||||
const existingAssignments = await ctx.db.assignment.findMany({
|
||||
@@ -350,19 +372,29 @@ export const allocationRouter = createTRPCRouter({
|
||||
orderBy: { startDate: "asc" },
|
||||
});
|
||||
|
||||
// Get vacations in the date range
|
||||
const vacations = await ctx.db.vacation.findMany({
|
||||
where: {
|
||||
resourceId: input.resourceId,
|
||||
status: "APPROVED",
|
||||
startDate: { lte: input.endDate },
|
||||
endDate: { gte: input.startDate },
|
||||
},
|
||||
select: { startDate: true, endDate: true, isHalfDay: true },
|
||||
});
|
||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||
ctx.db,
|
||||
[{
|
||||
id: resource.id,
|
||||
availability,
|
||||
countryId: resource.countryId,
|
||||
countryCode: resource.country?.code,
|
||||
federalState: resource.federalState,
|
||||
metroCityId: resource.metroCityId,
|
||||
metroCityName: resource.metroCity?.name,
|
||||
}],
|
||||
input.startDate,
|
||||
input.endDate,
|
||||
);
|
||||
const context = contexts.get(resource.id);
|
||||
|
||||
// Calculate day-by-day availability
|
||||
let totalWorkingDays = 0;
|
||||
const totalWorkingDays = countEffectiveWorkingDays({
|
||||
availability,
|
||||
periodStart: input.startDate,
|
||||
periodEnd: input.endDate,
|
||||
context,
|
||||
});
|
||||
let availableDays = 0;
|
||||
let conflictDays = 0;
|
||||
let partialDays = 0;
|
||||
@@ -372,36 +404,27 @@ export const allocationRouter = createTRPCRouter({
|
||||
const d = new Date(input.startDate);
|
||||
const end = new Date(input.endDate);
|
||||
while (d <= end) {
|
||||
const dow = d.getDay();
|
||||
if (dow !== 0 && dow !== 6) {
|
||||
totalWorkingDays++;
|
||||
const effectiveDayCapacity = calculateEffectiveDayAvailability({
|
||||
availability,
|
||||
date: d,
|
||||
context,
|
||||
});
|
||||
|
||||
// Check vacation
|
||||
const isVacation = vacations.some((v) => {
|
||||
const vs = new Date(v.startDate); vs.setHours(0, 0, 0, 0);
|
||||
const ve = new Date(v.endDate); ve.setHours(0, 0, 0, 0);
|
||||
const dc = new Date(d); dc.setHours(0, 0, 0, 0);
|
||||
return dc >= vs && dc <= ve;
|
||||
});
|
||||
|
||||
if (isVacation) {
|
||||
conflictDays++;
|
||||
d.setDate(d.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sum existing hours on this day
|
||||
if (effectiveDayCapacity > 0) {
|
||||
let bookedHours = 0;
|
||||
for (const a of existingAssignments) {
|
||||
const as2 = new Date(a.startDate); as2.setHours(0, 0, 0, 0);
|
||||
const ae = new Date(a.endDate); ae.setHours(0, 0, 0, 0);
|
||||
const dc = new Date(d); dc.setHours(0, 0, 0, 0);
|
||||
if (dc >= as2 && dc <= ae) {
|
||||
bookedHours += a.hoursPerDay;
|
||||
}
|
||||
bookedHours += calculateEffectiveBookedHours({
|
||||
availability,
|
||||
startDate: a.startDate,
|
||||
endDate: a.endDate,
|
||||
hoursPerDay: a.hoursPerDay,
|
||||
periodStart: d,
|
||||
periodEnd: d,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
const remainingCapacity = Math.max(0, dailyCapacity - bookedHours);
|
||||
const remainingCapacity = Math.max(0, effectiveDayCapacity - bookedHours);
|
||||
if (remainingCapacity >= requestedHpd) {
|
||||
availableDays++;
|
||||
totalAvailableHours += requestedHpd;
|
||||
@@ -416,6 +439,15 @@ export const allocationRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const totalRequestedHours = totalWorkingDays * requestedHpd;
|
||||
const totalPeriodCapacity = calculateEffectiveAvailableHours({
|
||||
availability,
|
||||
periodStart: input.startDate,
|
||||
periodEnd: input.endDate,
|
||||
context,
|
||||
});
|
||||
const dailyCapacity = totalWorkingDays > 0
|
||||
? Math.round((totalPeriodCapacity / totalWorkingDays) * 10) / 10
|
||||
: 0;
|
||||
|
||||
return {
|
||||
resource: { id: resource.id, name: resource.displayName, eid: resource.eid },
|
||||
|
||||
Reference in New Issue
Block a user