From 153b90cc11ac4f332c8132735a21fb987256a49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 15:52:31 +0200 Subject: [PATCH] refactor(api): extract timeline project conflict support --- .../timeline-project-context-support.test.ts | 2 +- .../timeline-project-conflict-support.ts | 84 ++++++++++++++++++ .../timeline-project-context-support.ts | 88 +------------------ .../api/src/router/timeline-project-read.ts | 4 +- 4 files changed, 91 insertions(+), 87 deletions(-) create mode 100644 packages/api/src/router/timeline-project-conflict-support.ts diff --git a/packages/api/src/__tests__/timeline-project-context-support.test.ts b/packages/api/src/__tests__/timeline-project-context-support.test.ts index 6a2bc77..7b45ba1 100644 --- a/packages/api/src/__tests__/timeline-project-context-support.test.ts +++ b/packages/api/src/__tests__/timeline-project-context-support.test.ts @@ -16,7 +16,6 @@ vi.mock("../router/timeline-holiday-read.js", async () => { import { buildTimelineBudgetStatusAllocations, buildTimelineBudgetStatusResponse, - buildTimelineProjectAssignmentConflicts, buildTimelineProjectContextDetailResponse, buildTimelineProjectContextResponse, buildTimelineProjectContextSummary, @@ -25,6 +24,7 @@ import { loadTimelineProjectContextDetailArtifacts, resolveTimelineProjectContextPeriod, } from "../router/timeline-project-context-support.js"; +import { buildTimelineProjectAssignmentConflicts } from "../router/timeline-project-conflict-support.js"; import { formatHolidayOverlays, loadTimelineHolidayOverlays, diff --git a/packages/api/src/router/timeline-project-conflict-support.ts b/packages/api/src/router/timeline-project-conflict-support.ts new file mode 100644 index 0000000..f006eb4 --- /dev/null +++ b/packages/api/src/router/timeline-project-conflict-support.ts @@ -0,0 +1,84 @@ +import { fmtDate, rangesOverlap, toDate } from "./timeline-read-shared.js"; + +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, + }; + }); +} diff --git a/packages/api/src/router/timeline-project-context-support.ts b/packages/api/src/router/timeline-project-context-support.ts index 8c3d083..f613eb3 100644 --- a/packages/api/src/router/timeline-project-context-support.ts +++ b/packages/api/src/router/timeline-project-context-support.ts @@ -1,5 +1,9 @@ import { TRPCError } from "@trpc/server"; import type { Allocation } from "@capakraken/shared"; +import { + buildTimelineProjectAssignmentConflicts, + type TimelineProjectAssignmentConflict, +} from "./timeline-project-conflict-support.js"; import { formatHolidayOverlays, loadTimelineHolidayOverlays, @@ -9,7 +13,6 @@ import { anonymizeResourceOnEntry, createTimelineDateRange, fmtDate, - rangesOverlap, summarizeTimelineEntries, toDate, } from "./timeline-read-shared.js"; @@ -60,89 +63,6 @@ export function resolveTimelineProjectContextPeriod( 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 }>; diff --git a/packages/api/src/router/timeline-project-read.ts b/packages/api/src/router/timeline-project-read.ts index 9203f57..253f72c 100644 --- a/packages/api/src/router/timeline-project-read.ts +++ b/packages/api/src/router/timeline-project-read.ts @@ -9,11 +9,11 @@ import { loadProjectPlanningReadModel } from "./project-planning-read-model.js"; import { buildTimelineBudgetStatusAllocations, buildTimelineBudgetStatusResponse, + buildTimelineShiftValidationBookings, + buildTimelineShiftPreviewDetailResponse, buildTimelineProjectContextDetailResponse, buildTimelineProjectContextResponse, loadTimelineProjectContextDetailArtifacts, - buildTimelineShiftValidationBookings, - buildTimelineShiftPreviewDetailResponse, } from "./timeline-project-context-support.js"; import { buildTimelineShiftPlan } from "./timeline-shift-planning.js"; import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";