refactor(api): extract timeline project conflict support
This commit is contained in:
@@ -16,7 +16,6 @@ vi.mock("../router/timeline-holiday-read.js", async () => {
|
|||||||
import {
|
import {
|
||||||
buildTimelineBudgetStatusAllocations,
|
buildTimelineBudgetStatusAllocations,
|
||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
buildTimelineProjectAssignmentConflicts,
|
|
||||||
buildTimelineProjectContextDetailResponse,
|
buildTimelineProjectContextDetailResponse,
|
||||||
buildTimelineProjectContextResponse,
|
buildTimelineProjectContextResponse,
|
||||||
buildTimelineProjectContextSummary,
|
buildTimelineProjectContextSummary,
|
||||||
@@ -25,6 +24,7 @@ import {
|
|||||||
loadTimelineProjectContextDetailArtifacts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
resolveTimelineProjectContextPeriod,
|
resolveTimelineProjectContextPeriod,
|
||||||
} from "../router/timeline-project-context-support.js";
|
} from "../router/timeline-project-context-support.js";
|
||||||
|
import { buildTimelineProjectAssignmentConflicts } from "../router/timeline-project-conflict-support.js";
|
||||||
import {
|
import {
|
||||||
formatHolidayOverlays,
|
formatHolidayOverlays,
|
||||||
loadTimelineHolidayOverlays,
|
loadTimelineHolidayOverlays,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type { Allocation } from "@capakraken/shared";
|
import type { Allocation } from "@capakraken/shared";
|
||||||
|
import {
|
||||||
|
buildTimelineProjectAssignmentConflicts,
|
||||||
|
type TimelineProjectAssignmentConflict,
|
||||||
|
} from "./timeline-project-conflict-support.js";
|
||||||
import {
|
import {
|
||||||
formatHolidayOverlays,
|
formatHolidayOverlays,
|
||||||
loadTimelineHolidayOverlays,
|
loadTimelineHolidayOverlays,
|
||||||
@@ -9,7 +13,6 @@ import {
|
|||||||
anonymizeResourceOnEntry,
|
anonymizeResourceOnEntry,
|
||||||
createTimelineDateRange,
|
createTimelineDateRange,
|
||||||
fmtDate,
|
fmtDate,
|
||||||
rangesOverlap,
|
|
||||||
summarizeTimelineEntries,
|
summarizeTimelineEntries,
|
||||||
toDate,
|
toDate,
|
||||||
} from "./timeline-read-shared.js";
|
} from "./timeline-read-shared.js";
|
||||||
@@ -60,89 +63,6 @@ export function resolveTimelineProjectContextPeriod(
|
|||||||
return { startDate, endDate };
|
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: {
|
export function buildTimelineProjectContextSummary(input: {
|
||||||
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||||
demands: Array<{ projectId: string | null }>;
|
demands: Array<{ projectId: string | null }>;
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
|
|||||||
import {
|
import {
|
||||||
buildTimelineBudgetStatusAllocations,
|
buildTimelineBudgetStatusAllocations,
|
||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
|
buildTimelineShiftValidationBookings,
|
||||||
|
buildTimelineShiftPreviewDetailResponse,
|
||||||
buildTimelineProjectContextDetailResponse,
|
buildTimelineProjectContextDetailResponse,
|
||||||
buildTimelineProjectContextResponse,
|
buildTimelineProjectContextResponse,
|
||||||
loadTimelineProjectContextDetailArtifacts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
buildTimelineShiftValidationBookings,
|
|
||||||
buildTimelineShiftPreviewDetailResponse,
|
|
||||||
} from "./timeline-project-context-support.js";
|
} from "./timeline-project-context-support.js";
|
||||||
import { buildTimelineShiftPlan } from "./timeline-shift-planning.js";
|
import { buildTimelineShiftPlan } from "./timeline-shift-planning.js";
|
||||||
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user