refactor(api): extract timeline project context support

This commit is contained in:
2026-03-31 15:00:01 +02:00
parent b669de54e1
commit e1de9a3a98
3 changed files with 379 additions and 92 deletions
@@ -0,0 +1,160 @@
import { TRPCError } from "@trpc/server";
import { summarizeHolidayOverlays, type formatHolidayOverlays } from "./timeline-holiday-read.js";
import {
createTimelineDateRange,
fmtDate,
rangesOverlap,
summarizeTimelineEntries,
toDate,
} from "./timeline-read-shared.js";
export interface ResolveTimelineProjectContextPeriodInput {
requestedStartDate?: string | undefined;
requestedEndDate?: string | undefined;
durationDays?: number | undefined;
projectStartDate?: Date | null | undefined;
projectEndDate?: Date | null | undefined;
firstAssignmentStartDate?: Date | string | null | undefined;
firstDemandStartDate?: Date | string | null | undefined;
}
export function resolveTimelineProjectContextPeriod(
input: ResolveTimelineProjectContextPeriodInput,
) {
const startDate = input.requestedStartDate
? createTimelineDateRange({
startDate: input.requestedStartDate,
durationDays: 1,
}).startDate
: input.projectStartDate
?? (input.firstAssignmentStartDate ? toDate(input.firstAssignmentStartDate) : null)
?? (input.firstDemandStartDate ? toDate(input.firstDemandStartDate) : null)
?? createTimelineDateRange({ durationDays: 1 }).startDate;
const endDate = input.requestedEndDate
? createTimelineDateRange({
startDate: fmtDate(startDate) ?? undefined,
endDate: input.requestedEndDate,
}).endDate
: input.projectEndDate
?? createTimelineDateRange({
startDate: fmtDate(startDate) ?? undefined,
durationDays: input.durationDays ?? 21,
}).endDate;
if (endDate < startDate) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "endDate must be on or after startDate.",
});
}
return { startDate, endDate };
}
export interface TimelineProjectAssignmentConflict {
assignmentId: string;
resourceId: string;
resourceName: string | null;
startDate: string | null;
endDate: string | null;
hoursPerDay: number;
overlapCount: number;
crossProjectOverlapCount: number;
overlaps: Array<{
id: string;
projectId: string | null;
projectName: string | null;
projectShortCode: string | null;
startDate: string | null;
endDate: string | null;
hoursPerDay: number;
status: string;
sameProject: boolean;
}>;
}
export function buildTimelineProjectAssignmentConflicts(input: {
projectId: string;
assignments: Array<{
id: string;
resourceId: string | null;
resource?: { displayName?: string | null } | null;
startDate: Date | string;
endDate: Date | string;
hoursPerDay: number;
}>;
allResourceAllocations: Array<{
id: string;
resourceId: string | null;
projectId: string | null;
project?: { name?: string | null; shortCode?: string | null } | null;
startDate: Date | string;
endDate: Date | string;
hoursPerDay: number;
status: string;
}>;
}): TimelineProjectAssignmentConflict[] {
return input.assignments
.filter((assignment) => assignment.resourceId && assignment.resource)
.map((assignment) => {
const overlaps = input.allResourceAllocations
.filter((booking) => (
booking.resourceId === assignment.resourceId
&& booking.id !== assignment.id
&& rangesOverlap(
toDate(booking.startDate),
toDate(booking.endDate),
toDate(assignment.startDate),
toDate(assignment.endDate),
)
))
.map((booking) => ({
id: booking.id,
projectId: booking.projectId,
projectName: booking.project?.name ?? null,
projectShortCode: booking.project?.shortCode ?? null,
startDate: fmtDate(toDate(booking.startDate)),
endDate: fmtDate(toDate(booking.endDate)),
hoursPerDay: booking.hoursPerDay,
status: booking.status,
sameProject: booking.projectId === input.projectId,
}));
return {
assignmentId: assignment.id,
resourceId: assignment.resourceId!,
resourceName: assignment.resource?.displayName ?? null,
startDate: fmtDate(toDate(assignment.startDate)),
endDate: fmtDate(toDate(assignment.endDate)),
hoursPerDay: assignment.hoursPerDay,
overlapCount: overlaps.length,
crossProjectOverlapCount: overlaps.filter((booking) => !booking.sameProject).length,
overlaps,
};
});
}
export function buildTimelineProjectContextSummary(input: {
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
demands: Array<{ projectId: string | null }>;
assignments: Array<{ projectId: string | null; resourceId: string | null }>;
resourceIds: string[];
allResourceAllocations: unknown[];
assignmentConflicts: Array<{ crossProjectOverlapCount: number }>;
holidayOverlays: ReturnType<typeof formatHolidayOverlays>;
}) {
return {
...summarizeTimelineEntries({
allocations: input.allocations,
demands: input.demands,
assignments: input.assignments,
}),
resourceIds: input.resourceIds.length,
allResourceAllocationCount: input.allResourceAllocations.length,
conflictedAssignmentCount: input.assignmentConflicts.filter(
(item) => item.crossProjectOverlapCount > 0,
).length,
...summarizeHolidayOverlays(input.holidayOverlays),
};
}