235 lines
7.4 KiB
TypeScript
235 lines
7.4 KiB
TypeScript
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<typeof formatHolidayOverlays>,
|
|
) {
|
|
const resourceIds = new Set<string>();
|
|
const byScope = new Map<string, number>();
|
|
|
|
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<typeof buildSplitAllocationReadModel>,
|
|
) {
|
|
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,
|
|
};
|
|
}),
|
|
};
|