import { buildSplitAllocationReadModel } from "@capakraken/application"; import { Prisma, VacationType } from "@capakraken/db"; import { z } from "zod"; import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js"; import { controllerProcedure, protectedProcedure } from "../trpc.js"; import { buildSelfServiceTimelineInput, createTimelineDateRange, createTimelineFilters, fmtDate, loadTimelineEntriesReadModel, TimelineEntriesDbClient, TimelineEntriesFilters, TimelineWindowFiltersSchema, } from "./timeline-read-shared.js"; export function formatHolidayOverlays( overlays: Array<{ id: string; resourceId: string; startDate: Date; endDate: Date; note?: string | null; scope?: string | null; calendarName?: string | null; sourceType?: string | null; countryCode?: string | null; countryName?: string | null; federalState?: string | null; metroCityName?: string | null; }>, ) { return overlays.map((overlay) => ({ id: overlay.id, resourceId: overlay.resourceId, startDate: fmtDate(overlay.startDate), endDate: fmtDate(overlay.endDate), note: overlay.note ?? null, scope: overlay.scope ?? null, calendarName: overlay.calendarName ?? null, sourceType: overlay.sourceType ?? null, countryCode: overlay.countryCode ?? null, countryName: overlay.countryName ?? null, federalState: overlay.federalState ?? null, metroCityName: overlay.metroCityName ?? null, })); } export function summarizeHolidayOverlays( overlays: ReturnType, ) { const resourceIds = new Set(); const byScope = new Map(); for (const overlay of overlays) { resourceIds.add(overlay.resourceId); const scope = overlay.scope ?? "UNKNOWN"; byScope.set(scope, (byScope.get(scope) ?? 0) + 1); } return { overlayCount: overlays.length, holidayResourceCount: resourceIds.size, byScope: [...byScope.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([scope, count]) => ({ scope, count })), }; } export async function loadTimelineHolidayOverlays( db: TimelineEntriesDbClient, input: TimelineEntriesFilters, ) { const readModel = await loadTimelineEntriesReadModel(db, input); return loadTimelineHolidayOverlaysForReadModel(db, input, readModel); } export async function loadTimelineHolidayOverlaysForReadModel( db: TimelineEntriesDbClient, input: TimelineEntriesFilters, readModel: ReturnType, ) { const resourceIds = [...new Set( readModel.assignments .map((assignment) => assignment.resourceId) .filter((resourceId): resourceId is string => typeof resourceId === "string" && resourceId.length > 0), )]; if (input.resourceIds && input.resourceIds.length > 0) { for (const resourceId of input.resourceIds) { if (resourceId && !resourceIds.includes(resourceId)) { resourceIds.push(resourceId); } } } const hasResourceFilters = (input.chapters?.length ?? 0) > 0 || (input.eids?.length ?? 0) > 0 || (input.countryCodes?.length ?? 0) > 0; if (hasResourceFilters) { const andConditions: Prisma.ResourceWhereInput[] = []; if (input.chapters && input.chapters.length > 0) { andConditions.push({ chapter: { in: input.chapters } }); } if (input.eids && input.eids.length > 0) { andConditions.push({ eid: { in: input.eids } }); } if (input.countryCodes && input.countryCodes.length > 0) { andConditions.push({ country: { code: { in: input.countryCodes } } }); } const matchingResources = await db.resource.findMany({ where: andConditions.length === 1 ? andConditions[0]! : { AND: andConditions }, select: { id: true }, }); for (const resource of matchingResources) { if (!resourceIds.includes(resource.id)) { resourceIds.push(resource.id); } } } if (resourceIds.length === 0) { return []; } const resources = await db.resource.findMany({ where: { id: { in: resourceIds } }, select: { id: true, countryId: true, federalState: true, metroCityId: true, country: { select: { code: true, name: true } }, metroCity: { select: { name: true } }, }, }); const overlays = await Promise.all( resources.map(async (resource) => { const holidays = await getResolvedCalendarHolidays(asHolidayResolverDb(db), { periodStart: input.startDate, periodEnd: input.endDate, countryId: resource.countryId, countryCode: resource.country?.code ?? null, federalState: resource.federalState, metroCityId: resource.metroCityId, metroCityName: resource.metroCity?.name ?? null, }); return holidays.map((holiday) => { const holidayDate = new Date(`${holiday.date}T00:00:00.000Z`); return { id: `calendar-holiday:${resource.id}:${holiday.date}`, resourceId: resource.id, type: VacationType.PUBLIC_HOLIDAY, status: "APPROVED" as const, startDate: holidayDate, endDate: holidayDate, note: holiday.name, scope: holiday.scope, calendarName: holiday.calendarName, sourceType: holiday.sourceType, countryCode: resource.country?.code ?? null, countryName: resource.country?.name ?? null, federalState: resource.federalState ?? null, metroCityName: resource.metroCity?.name ?? null, }; }); }), ); return overlays.flat().sort((left, right) => { if (left.resourceId !== right.resourceId) { return left.resourceId.localeCompare(right.resourceId); } return left.startDate.getTime() - right.startDate.getTime(); }); } export const timelineHolidayReadProcedures = { getHolidayOverlays: controllerProcedure .input(TimelineWindowFiltersSchema) .query(async ({ ctx, input }) => loadTimelineHolidayOverlays(ctx.db, input)), getMyHolidayOverlays: protectedProcedure .input(TimelineWindowFiltersSchema) .query(async ({ ctx, input }) => { const selfServiceInput = await buildSelfServiceTimelineInput(ctx, input); if (!selfServiceInput) { return []; } return loadTimelineHolidayOverlays(ctx.db, selfServiceInput); }), getHolidayOverlayDetail: controllerProcedure .input( z.object({ startDate: z.string().optional(), endDate: z.string().optional(), durationDays: z.number().int().min(1).max(366).optional(), resourceIds: z.array(z.string()).optional(), projectIds: z.array(z.string()).optional(), clientIds: z.array(z.string()).optional(), chapters: z.array(z.string()).optional(), eids: z.array(z.string()).optional(), countryCodes: z.array(z.string()).optional(), }), ) .query(async ({ ctx, input }) => { const { startDate, endDate } = createTimelineDateRange(input); const filters = createTimelineFilters(input); const holidayOverlays = await loadTimelineHolidayOverlays(ctx.db, { ...filters, startDate, endDate, }); const formattedOverlays = formatHolidayOverlays(holidayOverlays); return { period: { startDate: fmtDate(startDate), endDate: fmtDate(endDate), }, filters, summary: summarizeHolidayOverlays(formattedOverlays), overlays: formattedOverlays, }; }), };