import { isChargeabilityActualBooking, isChargeabilityRelevantProject, listAssignmentBookings, } from "@capakraken/application"; import type { WeekdayAvailability } from "@capakraken/shared"; import { calculateEffectiveAvailableHours, calculateEffectiveBookedHours, countEffectiveWorkingDays, getAvailabilityHoursForDate, loadResourceDailyAvailabilityContexts, } from "../lib/resource-capacity.js"; import type { ReportInput, ReportQueryResult } from "./report-query-config.js"; import { RESOURCE_MONTH_COLUMNS } from "./report-columns.js"; import { buildReportGroups, matchesInMemoryFilter, pickColumns, sortInMemoryRows, } from "./report-query-utils.js"; export async function executeResourceMonthReport( db: any, input: ReportInput, ): Promise { const periodMonth = input.periodMonth ?? new Date().toISOString().slice(0, 7); const [year, month] = periodMonth.split("-").map(Number) as [number, number]; const periodStart = new Date(Date.UTC(year, month - 1, 1)); const periodEnd = new Date(Date.UTC(year, month, 0)); const resources = await db.resource.findMany({ select: { id: true, eid: true, displayName: true, email: true, chapter: true, resourceType: true, isActive: true, chgResponsibility: true, rolledOff: true, departed: true, lcrCents: true, ucrCents: true, currency: true, fte: true, availability: true, chargeabilityTarget: true, federalState: true, countryId: true, metroCityId: true, country: { select: { code: true, name: true } }, metroCity: { select: { name: true } }, orgUnit: { select: { name: true } }, managementLevelGroup: { select: { name: true, targetPercentage: true } }, managementLevel: { select: { name: true } }, }, orderBy: { displayName: "asc" }, }); const resourceIds = resources.map((resource: any) => resource.id); const [bookings, contexts] = await Promise.all([ resourceIds.length > 0 ? listAssignmentBookings(db, { startDate: periodStart, endDate: periodEnd, resourceIds, }) : Promise.resolve([]), loadResourceDailyAvailabilityContexts( db, resources.map((resource: any) => ({ id: resource.id, availability: resource.availability as WeekdayAvailability, countryId: resource.countryId, countryCode: resource.country?.code, federalState: resource.federalState, metroCityId: resource.metroCityId, metroCityName: resource.metroCity?.name, })), periodStart, periodEnd, ), ]); const rows = resources.map((resource: any) => { const availability = resource.availability as WeekdayAvailability; const context = contexts.get(resource.id); const resourceBookings = bookings.filter((booking) => booking.resourceId === resource.id); const baseWorkingDays = countEffectiveWorkingDays({ availability, periodStart, periodEnd, context: undefined, }); const effectiveWorkingDays = countEffectiveWorkingDays({ availability, periodStart, periodEnd, context, }); const baseAvailableHours = calculateEffectiveAvailableHours({ availability, periodStart, periodEnd, context: undefined, }); const sahHours = calculateEffectiveAvailableHours({ availability, periodStart, periodEnd, context, }); const holidayDates = [...(context?.holidayDates ?? new Set())]; const publicHolidayWorkdayCount = holidayDates.reduce((count, isoDate) => ( count + (getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0) ), 0); const publicHolidayHoursDeduction = holidayDates.reduce((sum, isoDate) => ( sum + getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`)) ), 0); let absenceDayEquivalent = 0; let absenceHoursDeduction = 0; for (const [isoDate, fraction] of context?.vacationFractionsByDate ?? []) { const dayHours = getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`)); if (dayHours <= 0 || context?.holidayDates.has(isoDate)) { continue; } absenceDayEquivalent += fraction; absenceHoursDeduction += dayHours * fraction; } const actualBookedHours = resourceBookings .filter((booking) => isChargeabilityActualBooking(booking, false)) .reduce((sum, booking) => sum + calculateEffectiveBookedHours({ availability, startDate: booking.startDate, endDate: booking.endDate, hoursPerDay: booking.hoursPerDay, periodStart, periodEnd, context, }), 0); const expectedBookedHours = resourceBookings .filter((booking) => isChargeabilityRelevantProject(booking.project, true)) .reduce((sum, booking) => sum + calculateEffectiveBookedHours({ availability, startDate: booking.startDate, endDate: booking.endDate, hoursPerDay: booking.hoursPerDay, periodStart, periodEnd, context, }), 0); const targetPct = resource.managementLevelGroup?.targetPercentage != null ? resource.managementLevelGroup.targetPercentage * 100 : resource.chargeabilityTarget; return { id: `${resource.id}:${periodMonth}`, resourceId: resource.id, monthKey: periodMonth, periodStart: periodStart.toISOString(), periodEnd: periodEnd.toISOString(), eid: resource.eid, displayName: resource.displayName, email: resource.email, chapter: resource.chapter, resourceType: resource.resourceType, isActive: resource.isActive, chgResponsibility: resource.chgResponsibility, rolledOff: resource.rolledOff, departed: resource.departed, countryCode: resource.country?.code ?? null, countryName: resource.country?.name ?? null, federalState: resource.federalState, metroCityName: resource.metroCity?.name ?? null, orgUnitName: resource.orgUnit?.name ?? null, managementLevelGroupName: resource.managementLevelGroup?.name ?? null, managementLevelName: resource.managementLevel?.name ?? null, fte: roundMetric(resource.fte), lcrCents: resource.lcrCents, ucrCents: resource.ucrCents, currency: resource.currency, monthlyChargeabilityTargetPct: roundMetric(targetPct), monthlyTargetHours: roundMetric((sahHours * targetPct) / 100), monthlyBaseWorkingDays: roundMetric(baseWorkingDays), monthlyEffectiveWorkingDays: roundMetric(effectiveWorkingDays), monthlyBaseAvailableHours: roundMetric(baseAvailableHours), monthlySahHours: roundMetric(sahHours), monthlyPublicHolidayCount: holidayDates.length, monthlyPublicHolidayWorkdayCount: publicHolidayWorkdayCount, monthlyPublicHolidayHoursDeduction: roundMetric(publicHolidayHoursDeduction), monthlyAbsenceDayEquivalent: roundMetric(absenceDayEquivalent), monthlyAbsenceHoursDeduction: roundMetric(absenceHoursDeduction), monthlyActualBookedHours: roundMetric(actualBookedHours), monthlyExpectedBookedHours: roundMetric(expectedBookedHours), monthlyActualChargeabilityPct: roundMetric(sahHours > 0 ? (actualBookedHours / sahHours) * 100 : 0), monthlyExpectedChargeabilityPct: roundMetric(sahHours > 0 ? (expectedBookedHours / sahHours) * 100 : 0), monthlyUnassignedHours: roundMetric(Math.max(0, sahHours - actualBookedHours)), }; }); const filteredRows = rows.filter((row: Record) => input.filters.every((filter) => matchesInMemoryFilter( row, filter, RESOURCE_MONTH_COLUMNS, ))); const sortedRows = sortInMemoryRows( filteredRows, input.groupBy, input.sortBy, input.sortDir, RESOURCE_MONTH_COLUMNS, ); const totalCount = sortedRows.length; const pagedRows = sortedRows.slice(input.offset, input.offset + input.limit); const outputColumns = ["id", ...input.columns.filter((column) => column !== "id")]; const groups = buildReportGroups(pagedRows, input.groupBy); return { rows: pagedRows.map((row) => pickColumns(row, outputColumns)), columns: outputColumns, totalCount, groups, }; } function roundMetric(value: number): number { return Math.round(value * 10) / 10; }