refactor(api): extract timeline project detail artifact loader
This commit is contained in:
@@ -1,5 +1,18 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-holiday-read.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../router/timeline-holiday-read.js")>(
|
||||||
|
"../router/timeline-holiday-read.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
loadTimelineHolidayOverlays: vi.fn(),
|
||||||
|
formatHolidayOverlays: vi.fn(actual.formatHolidayOverlays),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildTimelineBudgetStatusAllocations,
|
buildTimelineBudgetStatusAllocations,
|
||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
@@ -9,10 +22,22 @@ import {
|
|||||||
buildTimelineProjectContextSummary,
|
buildTimelineProjectContextSummary,
|
||||||
buildTimelineShiftValidationBookings,
|
buildTimelineShiftValidationBookings,
|
||||||
buildTimelineShiftPreviewDetailResponse,
|
buildTimelineShiftPreviewDetailResponse,
|
||||||
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
resolveTimelineProjectContextPeriod,
|
resolveTimelineProjectContextPeriod,
|
||||||
} from "../router/timeline-project-context-support.js";
|
} from "../router/timeline-project-context-support.js";
|
||||||
|
import {
|
||||||
|
formatHolidayOverlays,
|
||||||
|
loadTimelineHolidayOverlays,
|
||||||
|
} from "../router/timeline-holiday-read.js";
|
||||||
|
|
||||||
|
const loadTimelineHolidayOverlaysMock = vi.mocked(loadTimelineHolidayOverlays);
|
||||||
|
const formatHolidayOverlaysMock = vi.mocked(formatHolidayOverlays);
|
||||||
|
|
||||||
describe("timeline project context support", () => {
|
describe("timeline project context support", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it("derives the detail period from explicit input and validates order", () => {
|
it("derives the detail period from explicit input and validates order", () => {
|
||||||
expect(resolveTimelineProjectContextPeriod({
|
expect(resolveTimelineProjectContextPeriod({
|
||||||
requestedStartDate: "2026-04-03",
|
requestedStartDate: "2026-04-03",
|
||||||
@@ -451,4 +476,127 @@ describe("timeline project context support", () => {
|
|||||||
budgetCents: 120000,
|
budgetCents: 120000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads detail artifacts with formatted holiday overlays only when resources exist", async () => {
|
||||||
|
loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([
|
||||||
|
{
|
||||||
|
id: "overlay_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-04-03T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-03T00:00:00.000Z"),
|
||||||
|
note: "Holiday",
|
||||||
|
scope: "COUNTRY",
|
||||||
|
calendarName: "DE",
|
||||||
|
sourceType: "CALENDAR",
|
||||||
|
countryCode: "DE",
|
||||||
|
countryName: "Germany",
|
||||||
|
federalState: null,
|
||||||
|
metroCityName: null,
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
const artifacts = await loadTimelineProjectContextDetailArtifacts({} as never, {
|
||||||
|
projectId: "project_1",
|
||||||
|
requestedStartDate: "2026-04-01",
|
||||||
|
durationDays: 5,
|
||||||
|
projectStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
projectEndDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||||
|
firstAssignmentStartDate: new Date("2026-04-02T00:00:00.000Z"),
|
||||||
|
firstDemandStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { displayName: "Alice" },
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
||||||
|
hoursPerDay: 8,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allResourceAllocations: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
projectId: "project_2",
|
||||||
|
project: { name: "Other", shortCode: "OTH" },
|
||||||
|
startDate: new Date("2026-04-04T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-07T00:00:00.000Z"),
|
||||||
|
hoursPerDay: 6,
|
||||||
|
status: "ACTIVE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledWith(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
projectIds: ["project_1"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(formatHolidayOverlaysMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(artifacts).toEqual({
|
||||||
|
period: {
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
holidayOverlays: [
|
||||||
|
{
|
||||||
|
id: "overlay_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: "2026-04-03",
|
||||||
|
endDate: "2026-04-03",
|
||||||
|
note: "Holiday",
|
||||||
|
scope: "COUNTRY",
|
||||||
|
calendarName: "DE",
|
||||||
|
sourceType: "CALENDAR",
|
||||||
|
countryCode: "DE",
|
||||||
|
countryName: "Germany",
|
||||||
|
federalState: null,
|
||||||
|
metroCityName: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assignmentConflicts: [
|
||||||
|
{
|
||||||
|
assignmentId: "assignment_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resourceName: "Alice",
|
||||||
|
startDate: "2026-04-01",
|
||||||
|
endDate: "2026-04-05",
|
||||||
|
hoursPerDay: 8,
|
||||||
|
overlapCount: 1,
|
||||||
|
crossProjectOverlapCount: 1,
|
||||||
|
overlaps: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
projectId: "project_2",
|
||||||
|
projectName: "Other",
|
||||||
|
projectShortCode: "OTH",
|
||||||
|
startDate: "2026-04-04",
|
||||||
|
endDate: "2026-04-07",
|
||||||
|
hoursPerDay: 6,
|
||||||
|
status: "ACTIVE",
|
||||||
|
sameProject: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyArtifacts = await loadTimelineProjectContextDetailArtifacts({} as never, {
|
||||||
|
projectId: "project_1",
|
||||||
|
requestedStartDate: "2026-04-01",
|
||||||
|
durationDays: 5,
|
||||||
|
assignments: [],
|
||||||
|
allResourceAllocations: [],
|
||||||
|
resourceIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadTimelineHolidayOverlaysMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(emptyArtifacts.holidayOverlays).toEqual([]);
|
||||||
|
expect(emptyArtifacts.assignmentConflicts).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import type { Allocation } from "@capakraken/shared";
|
import type { Allocation } from "@capakraken/shared";
|
||||||
import { summarizeHolidayOverlays, type formatHolidayOverlays } from "./timeline-holiday-read.js";
|
import {
|
||||||
|
formatHolidayOverlays,
|
||||||
|
loadTimelineHolidayOverlays,
|
||||||
|
summarizeHolidayOverlays,
|
||||||
|
} from "./timeline-holiday-read.js";
|
||||||
import {
|
import {
|
||||||
anonymizeResourceOnEntry,
|
anonymizeResourceOnEntry,
|
||||||
createTimelineDateRange,
|
createTimelineDateRange,
|
||||||
@@ -316,6 +320,70 @@ export function buildTimelineProjectContextDetailResponse<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadTimelineProjectContextDetailArtifacts(
|
||||||
|
db: Parameters<typeof loadTimelineHolidayOverlays>[0],
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}>;
|
||||||
|
resourceIds: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const period = resolveTimelineProjectContextPeriod({
|
||||||
|
requestedStartDate: input.requestedStartDate,
|
||||||
|
requestedEndDate: input.requestedEndDate,
|
||||||
|
durationDays: input.durationDays,
|
||||||
|
projectStartDate: input.projectStartDate,
|
||||||
|
projectEndDate: input.projectEndDate,
|
||||||
|
firstAssignmentStartDate: input.firstAssignmentStartDate,
|
||||||
|
firstDemandStartDate: input.firstDemandStartDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
const holidayOverlays = input.resourceIds.length > 0
|
||||||
|
? formatHolidayOverlays(await loadTimelineHolidayOverlays(db, {
|
||||||
|
startDate: period.startDate,
|
||||||
|
endDate: period.endDate,
|
||||||
|
resourceIds: input.resourceIds,
|
||||||
|
projectIds: [input.projectId],
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const assignmentConflicts = buildTimelineProjectAssignmentConflicts({
|
||||||
|
projectId: input.projectId,
|
||||||
|
assignments: input.assignments,
|
||||||
|
allResourceAllocations: input.allResourceAllocations,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
period,
|
||||||
|
holidayOverlays,
|
||||||
|
assignmentConflicts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildTimelineShiftPreviewDetailResponse<TPreview>(input: {
|
export function buildTimelineShiftPreviewDetailResponse<TPreview>(input: {
|
||||||
project: {
|
project: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import {
|
|||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
buildTimelineProjectContextDetailResponse,
|
buildTimelineProjectContextDetailResponse,
|
||||||
buildTimelineProjectContextResponse,
|
buildTimelineProjectContextResponse,
|
||||||
buildTimelineProjectAssignmentConflicts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
buildTimelineShiftValidationBookings,
|
buildTimelineShiftValidationBookings,
|
||||||
buildTimelineShiftPreviewDetailResponse,
|
buildTimelineShiftPreviewDetailResponse,
|
||||||
resolveTimelineProjectContextPeriod,
|
|
||||||
} 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 { loadTimelineHolidayOverlays, formatHolidayOverlays } from "./timeline-holiday-read.js";
|
|
||||||
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
||||||
import { buildTimelineProjectShiftValidation } from "./timeline-shift-support.js";
|
import { buildTimelineProjectShiftValidation } from "./timeline-shift-support.js";
|
||||||
|
|
||||||
@@ -163,30 +161,20 @@ export const timelineProjectReadProcedures = {
|
|||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const projectContext = await loadTimelineProjectContext(ctx.db, input.projectId);
|
const projectContext = await loadTimelineProjectContext(ctx.db, input.projectId);
|
||||||
const directory = await getAnonymizationDirectory(ctx.db);
|
const directory = await getAnonymizationDirectory(ctx.db);
|
||||||
const period = resolveTimelineProjectContextPeriod({
|
const { period, holidayOverlays, assignmentConflicts } =
|
||||||
requestedStartDate: input.startDate,
|
await loadTimelineProjectContextDetailArtifacts(ctx.db, {
|
||||||
requestedEndDate: input.endDate,
|
projectId: input.projectId,
|
||||||
durationDays: input.durationDays,
|
requestedStartDate: input.startDate,
|
||||||
projectStartDate: projectContext.project.startDate,
|
requestedEndDate: input.endDate,
|
||||||
projectEndDate: projectContext.project.endDate,
|
durationDays: input.durationDays,
|
||||||
firstAssignmentStartDate: projectContext.assignments[0]?.startDate,
|
projectStartDate: projectContext.project.startDate,
|
||||||
firstDemandStartDate: projectContext.demands[0]?.startDate,
|
projectEndDate: projectContext.project.endDate,
|
||||||
});
|
firstAssignmentStartDate: projectContext.assignments[0]?.startDate,
|
||||||
|
firstDemandStartDate: projectContext.demands[0]?.startDate,
|
||||||
const holidayOverlays = projectContext.resourceIds.length > 0
|
assignments: projectContext.assignments,
|
||||||
? await loadTimelineHolidayOverlays(ctx.db, {
|
allResourceAllocations: projectContext.allResourceAllocations,
|
||||||
startDate: period.startDate,
|
resourceIds: projectContext.resourceIds,
|
||||||
endDate: period.endDate,
|
});
|
||||||
resourceIds: projectContext.resourceIds,
|
|
||||||
projectIds: [input.projectId],
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
const formattedHolidayOverlays = formatHolidayOverlays(holidayOverlays);
|
|
||||||
const assignmentConflicts = buildTimelineProjectAssignmentConflicts({
|
|
||||||
projectId: input.projectId,
|
|
||||||
assignments: projectContext.assignments,
|
|
||||||
allResourceAllocations: projectContext.allResourceAllocations,
|
|
||||||
});
|
|
||||||
|
|
||||||
return buildTimelineProjectContextDetailResponse({
|
return buildTimelineProjectContextDetailResponse({
|
||||||
project: projectContext.project,
|
project: projectContext.project,
|
||||||
@@ -197,7 +185,7 @@ export const timelineProjectReadProcedures = {
|
|||||||
allResourceAllocations: projectContext.allResourceAllocations,
|
allResourceAllocations: projectContext.allResourceAllocations,
|
||||||
resourceIds: projectContext.resourceIds,
|
resourceIds: projectContext.resourceIds,
|
||||||
assignmentConflicts,
|
assignmentConflicts,
|
||||||
holidayOverlays: formattedHolidayOverlays,
|
holidayOverlays,
|
||||||
directory,
|
directory,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user