refactor(api): extract timeline project context support
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user