import type { WeekdayAvailability } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { calculateEffectiveAvailableHours, calculateEffectiveBookedHours, calculateEffectiveDayAvailability, countEffectiveWorkingDays, loadResourceDailyAvailabilityContexts, } from "../lib/resource-capacity.js"; import { averagePerWorkingDay, round1, toIsoDate } from "./allocation-shared.js"; export async function buildResourceAvailabilityView( db: Pick, input: { resourceId: string; startDate: Date; endDate: Date; hoursPerDay: number; }, ) { const resource = await db.resource.findUnique({ where: { id: input.resourceId }, select: { id: true, displayName: true, eid: true, fte: 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 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, }; const [existingAssignments, vacations] = await Promise.all([ db.assignment.findMany({ where: { resourceId: input.resourceId, status: { not: "CANCELLED" }, startDate: { lte: input.endDate }, endDate: { gte: input.startDate }, }, select: { id: true, startDate: true, endDate: true, hoursPerDay: true, status: true, project: { select: { name: true, shortCode: true } }, }, orderBy: { startDate: "asc" }, }), db.vacation.findMany({ where: { resourceId: input.resourceId, status: { in: ["APPROVED", "PENDING"] }, startDate: { lte: input.endDate }, endDate: { gte: input.startDate }, }, select: { id: true, type: true, startDate: true, endDate: true, isHalfDay: true, halfDayPart: true, status: true, }, orderBy: { startDate: "asc" }, }), ]); const contexts = await loadResourceDailyAvailabilityContexts( 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); const totalWorkingDays = countEffectiveWorkingDays({ availability, periodStart: input.startDate, periodEnd: input.endDate, context, }); let availableDays = 0; let conflictDays = 0; let partialDays = 0; let totalAvailableHours = 0; const requestedHpd = input.hoursPerDay; const currentDate = new Date(input.startDate); const endDate = new Date(input.endDate); while (currentDate <= endDate) { const effectiveDayCapacity = calculateEffectiveDayAvailability({ availability, date: currentDate, context, }); if (effectiveDayCapacity > 0) { let bookedHours = 0; for (const assignment of existingAssignments) { bookedHours += calculateEffectiveBookedHours({ availability, startDate: assignment.startDate, endDate: assignment.endDate, hoursPerDay: assignment.hoursPerDay, periodStart: currentDate, periodEnd: currentDate, context, }); } const remainingCapacity = Math.max(0, effectiveDayCapacity - bookedHours); if (remainingCapacity >= requestedHpd) { availableDays++; totalAvailableHours += requestedHpd; } else if (remainingCapacity > 0) { partialDays++; totalAvailableHours += remainingCapacity; } else { conflictDays++; } } currentDate.setDate(currentDate.getDate() + 1); } const totalRequestedHours = totalWorkingDays * requestedHpd; const totalPeriodCapacity = calculateEffectiveAvailableHours({ availability, periodStart: input.startDate, periodEnd: input.endDate, context, }); const dailyCapacity = totalWorkingDays > 0 ? round1(totalPeriodCapacity / totalWorkingDays) : 0; return { resource: { id: resource.id, name: resource.displayName, eid: resource.eid }, dailyCapacity, totalWorkingDays, availableDays, partialDays, conflictDays, totalAvailableHours: round1(totalAvailableHours), totalRequestedHours, coveragePercent: totalRequestedHours > 0 ? Math.round((totalAvailableHours / totalRequestedHours) * 100) : 0, existingAssignments: existingAssignments.map((assignment) => ({ project: assignment.project.name, code: assignment.project.shortCode, hoursPerDay: assignment.hoursPerDay, start: toIsoDate(assignment.startDate), end: toIsoDate(assignment.endDate), status: assignment.status, })), vacations: vacations.map((vacation) => ({ id: vacation.id, type: vacation.type, status: vacation.status, start: toIsoDate(vacation.startDate), end: toIsoDate(vacation.endDate), isHalfDay: vacation.isHalfDay, halfDayPart: vacation.halfDayPart, })), }; } export function buildResourceAvailabilitySummary( availability: Awaited>, period: { startDate: Date; endDate: Date }, ) { const periodAvailableHours = availability.totalRequestedHours > 0 ? round1(availability.dailyCapacity * availability.totalWorkingDays) : 0; const periodRemainingHours = round1(availability.totalAvailableHours); const periodBookedHours = round1(Math.max(0, periodAvailableHours - periodRemainingHours)); return { resource: availability.resource.name, period: `${toIsoDate(period.startDate)} to ${toIsoDate(period.endDate)}`, fte: null, workingDays: availability.totalWorkingDays, periodAvailableHours, periodBookedHours, periodRemainingHours, maxHoursPerDay: availability.dailyCapacity, currentBookedHoursPerDay: round1( Math.max( 0, availability.dailyCapacity - availability.totalAvailableHours / Math.max(availability.totalWorkingDays, 1), ), ), availableHoursPerDay: averagePerWorkingDay(availability.totalAvailableHours, availability.totalWorkingDays), isFullyAvailable: availability.existingAssignments.length === 0 && availability.vacations.length === 0, existingAllocations: availability.existingAssignments.map((assignment) => ({ project: `${assignment.project} (${assignment.code})`, hoursPerDay: assignment.hoursPerDay, status: assignment.status, start: assignment.start, end: assignment.end, })), vacations: availability.vacations.map((vacation) => ({ type: vacation.type, start: vacation.start, end: vacation.end, isHalfDay: vacation.isHalfDay, })), }; }