refactor(api): extract timeline holiday support
This commit is contained in:
@@ -13,58 +13,25 @@ import {
|
||||
TimelineEntriesFilters,
|
||||
TimelineWindowFiltersSchema,
|
||||
} from "./timeline-read-shared.js";
|
||||
import {
|
||||
buildTimelineHolidayOverlayDetailResponse,
|
||||
buildTimelineHolidayResourceIds,
|
||||
buildTimelineHolidayResourceWhere,
|
||||
formatTimelineHolidayOverlays,
|
||||
summarizeTimelineHolidayOverlays,
|
||||
type TimelineHolidayOverlayRecord,
|
||||
} from "./timeline-holiday-support.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;
|
||||
}>,
|
||||
overlays: TimelineHolidayOverlayRecord[],
|
||||
) {
|
||||
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,
|
||||
}));
|
||||
return formatTimelineHolidayOverlays(overlays);
|
||||
}
|
||||
|
||||
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 })),
|
||||
};
|
||||
return summarizeTimelineHolidayOverlays(overlays);
|
||||
}
|
||||
|
||||
export async function loadTimelineHolidayOverlays(
|
||||
@@ -80,48 +47,22 @@ export async function loadTimelineHolidayOverlaysForReadModel(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
const resourceWhere = buildTimelineHolidayResourceWhere({
|
||||
chapters: input.chapters,
|
||||
eids: input.eids,
|
||||
countryCodes: input.countryCodes,
|
||||
});
|
||||
const matchingResources = resourceWhere
|
||||
? await db.resource.findMany({
|
||||
where: resourceWhere,
|
||||
select: { id: true },
|
||||
})
|
||||
: [];
|
||||
const resourceIds = buildTimelineHolidayResourceIds({
|
||||
assignmentResourceIds: readModel.assignments.map((assignment) => assignment.resourceId),
|
||||
requestedResourceIds: input.resourceIds,
|
||||
matchingFilterResourceIds: matchingResources.map((resource) => resource.id),
|
||||
});
|
||||
|
||||
if (resourceIds.length === 0) {
|
||||
return [];
|
||||
@@ -221,14 +162,11 @@ export const timelineHolidayReadProcedures = {
|
||||
});
|
||||
const formattedOverlays = formatHolidayOverlays(holidayOverlays);
|
||||
|
||||
return {
|
||||
period: {
|
||||
startDate: fmtDate(startDate),
|
||||
endDate: fmtDate(endDate),
|
||||
},
|
||||
return buildTimelineHolidayOverlayDetailResponse({
|
||||
startDate,
|
||||
endDate,
|
||||
filters,
|
||||
summary: summarizeHolidayOverlays(formattedOverlays),
|
||||
overlays: formattedOverlays,
|
||||
};
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import { fmtDate } from "./timeline-read-shared.js";
|
||||
|
||||
export interface TimelineHolidayOverlayRecord {
|
||||
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;
|
||||
}
|
||||
|
||||
export function formatTimelineHolidayOverlays(overlays: TimelineHolidayOverlayRecord[]) {
|
||||
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 summarizeTimelineHolidayOverlays(
|
||||
overlays: ReturnType<typeof formatTimelineHolidayOverlays>,
|
||||
) {
|
||||
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 function buildTimelineHolidayResourceWhere(input: {
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}): Prisma.ResourceWhereInput | null {
|
||||
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 } } });
|
||||
}
|
||||
|
||||
if (andConditions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return andConditions.length === 1 ? andConditions[0]! : { AND: andConditions };
|
||||
}
|
||||
|
||||
export function buildTimelineHolidayResourceIds(input: {
|
||||
assignmentResourceIds: Array<string | null>;
|
||||
requestedResourceIds?: string[] | undefined;
|
||||
matchingFilterResourceIds?: string[] | undefined;
|
||||
}) {
|
||||
const resourceIds = new Set(
|
||||
input.assignmentResourceIds.filter(
|
||||
(resourceId): resourceId is string => typeof resourceId === "string" && resourceId.length > 0,
|
||||
),
|
||||
);
|
||||
|
||||
for (const resourceId of input.requestedResourceIds ?? []) {
|
||||
if (resourceId) {
|
||||
resourceIds.add(resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const resourceId of input.matchingFilterResourceIds ?? []) {
|
||||
if (resourceId) {
|
||||
resourceIds.add(resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
return [...resourceIds];
|
||||
}
|
||||
|
||||
export function buildTimelineHolidayOverlayDetailResponse(input: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
filters: Record<string, unknown>;
|
||||
overlays: ReturnType<typeof formatTimelineHolidayOverlays>;
|
||||
}) {
|
||||
return {
|
||||
period: {
|
||||
startDate: fmtDate(input.startDate),
|
||||
endDate: fmtDate(input.endDate),
|
||||
},
|
||||
filters: input.filters,
|
||||
summary: summarizeTimelineHolidayOverlays(input.overlays),
|
||||
overlays: input.overlays,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user