refactor(api): extract timeline project context response support
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildTimelineProjectContextDetailResponse,
|
||||||
|
buildTimelineProjectContextResponse,
|
||||||
|
buildTimelineProjectContextSummary,
|
||||||
|
} from "../router/timeline-project-context-response-support.js";
|
||||||
|
|
||||||
|
describe("timeline project context response support", () => {
|
||||||
|
it("combines timeline counts with holiday overlay summary data", () => {
|
||||||
|
expect(buildTimelineProjectContextSummary({
|
||||||
|
allocations: [{ projectId: "project_1", resourceId: "resource_1" }],
|
||||||
|
demands: [{ projectId: "project_1" }],
|
||||||
|
assignments: [{ projectId: "project_1", resourceId: "resource_1" }],
|
||||||
|
resourceIds: ["resource_1", "resource_2"],
|
||||||
|
allResourceAllocations: [{ id: "booking_1" }, { id: "booking_2" }],
|
||||||
|
assignmentConflicts: [
|
||||||
|
{ crossProjectOverlapCount: 0 },
|
||||||
|
{ crossProjectOverlapCount: 2 },
|
||||||
|
],
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "overlay_2",
|
||||||
|
resourceId: "resource_2",
|
||||||
|
startDate: "2026-04-04",
|
||||||
|
endDate: "2026-04-04",
|
||||||
|
note: "Holiday",
|
||||||
|
scope: "CITY",
|
||||||
|
calendarName: "Berlin",
|
||||||
|
sourceType: "CALENDAR",
|
||||||
|
countryCode: "DE",
|
||||||
|
countryName: "Germany",
|
||||||
|
federalState: "BE",
|
||||||
|
metroCityName: "Berlin",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})).toEqual({
|
||||||
|
allocationCount: 1,
|
||||||
|
demandCount: 1,
|
||||||
|
assignmentCount: 1,
|
||||||
|
projectCount: 1,
|
||||||
|
resourceCount: 1,
|
||||||
|
resourceIds: 2,
|
||||||
|
allResourceAllocationCount: 2,
|
||||||
|
conflictedAssignmentCount: 1,
|
||||||
|
overlayCount: 2,
|
||||||
|
holidayResourceCount: 2,
|
||||||
|
byScope: [
|
||||||
|
{ scope: "CITY", count: 1 },
|
||||||
|
{ scope: "COUNTRY", count: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds anonymized project context responses and detail summaries", () => {
|
||||||
|
const directory = {
|
||||||
|
config: { enabled: true, mode: "alias", domain: "example.test" },
|
||||||
|
byResourceId: new Map([
|
||||||
|
[
|
||||||
|
"resource_1",
|
||||||
|
{
|
||||||
|
displayName: "Anon Alice",
|
||||||
|
eid: "ANON-001",
|
||||||
|
email: "anon-alice@example.test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
byAliasEid: new Map([["anon-001", "resource_1"]]),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(buildTimelineProjectContextResponse({
|
||||||
|
project: { id: "project_1" },
|
||||||
|
allocations: [
|
||||||
|
{
|
||||||
|
id: "allocation_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
demands: [{ id: "demand_1", projectId: "project_1" }],
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allResourceAllocations: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
directory: directory as never,
|
||||||
|
})).toEqual({
|
||||||
|
project: { id: "project_1" },
|
||||||
|
allocations: [
|
||||||
|
{
|
||||||
|
id: "allocation_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
demands: [{ id: "demand_1", projectId: "project_1" }],
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allResourceAllocations: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildTimelineProjectContextDetailResponse({
|
||||||
|
project: { id: "project_1" },
|
||||||
|
period: {
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
allocations: [
|
||||||
|
{
|
||||||
|
id: "allocation_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
demands: [{ id: "demand_1", projectId: "project_1" }],
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allResourceAllocations: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
assignmentConflicts: [{ crossProjectOverlapCount: 1, assignmentId: "assignment_1" }],
|
||||||
|
holidayOverlays: [],
|
||||||
|
directory: directory as never,
|
||||||
|
})).toEqual({
|
||||||
|
project: { id: "project_1" },
|
||||||
|
period: {
|
||||||
|
startDate: "2026-04-01",
|
||||||
|
endDate: "2026-04-05",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
allocationCount: 1,
|
||||||
|
demandCount: 1,
|
||||||
|
assignmentCount: 1,
|
||||||
|
projectCount: 1,
|
||||||
|
resourceCount: 1,
|
||||||
|
resourceIds: 1,
|
||||||
|
allResourceAllocationCount: 1,
|
||||||
|
conflictedAssignmentCount: 1,
|
||||||
|
overlayCount: 0,
|
||||||
|
holidayResourceCount: 0,
|
||||||
|
byScope: [],
|
||||||
|
},
|
||||||
|
allocations: [
|
||||||
|
{
|
||||||
|
id: "allocation_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
demands: [{ id: "demand_1", projectId: "project_1" }],
|
||||||
|
assignments: [
|
||||||
|
{
|
||||||
|
id: "assignment_1",
|
||||||
|
projectId: "project_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allResourceAllocations: [
|
||||||
|
{
|
||||||
|
id: "booking_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assignmentConflicts: [{ crossProjectOverlapCount: 1, assignmentId: "assignment_1" }],
|
||||||
|
holidayOverlays: [],
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,9 +14,6 @@ vi.mock("../router/timeline-holiday-read.js", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildTimelineProjectContextDetailResponse,
|
|
||||||
buildTimelineProjectContextResponse,
|
|
||||||
buildTimelineProjectContextSummary,
|
|
||||||
loadTimelineProjectContextDetailArtifacts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
resolveTimelineProjectContextPeriod,
|
resolveTimelineProjectContextPeriod,
|
||||||
} from "../router/timeline-project-context-support.js";
|
} from "../router/timeline-project-context-support.js";
|
||||||
@@ -147,222 +144,6 @@ describe("timeline project context support", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("combines timeline counts with holiday overlay summary data", () => {
|
|
||||||
expect(buildTimelineProjectContextSummary({
|
|
||||||
allocations: [{ projectId: "project_1", resourceId: "resource_1" }],
|
|
||||||
demands: [{ projectId: "project_1" }],
|
|
||||||
assignments: [{ projectId: "project_1", resourceId: "resource_1" }],
|
|
||||||
resourceIds: ["resource_1", "resource_2"],
|
|
||||||
allResourceAllocations: [{ id: "booking_1" }, { id: "booking_2" }],
|
|
||||||
assignmentConflicts: [
|
|
||||||
{ crossProjectOverlapCount: 0 },
|
|
||||||
{ crossProjectOverlapCount: 2 },
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "overlay_2",
|
|
||||||
resourceId: "resource_2",
|
|
||||||
startDate: "2026-04-04",
|
|
||||||
endDate: "2026-04-04",
|
|
||||||
note: "Holiday",
|
|
||||||
scope: "CITY",
|
|
||||||
calendarName: "Berlin",
|
|
||||||
sourceType: "CALENDAR",
|
|
||||||
countryCode: "DE",
|
|
||||||
countryName: "Germany",
|
|
||||||
federalState: "BE",
|
|
||||||
metroCityName: "Berlin",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})).toEqual({
|
|
||||||
allocationCount: 1,
|
|
||||||
demandCount: 1,
|
|
||||||
assignmentCount: 1,
|
|
||||||
projectCount: 1,
|
|
||||||
resourceCount: 1,
|
|
||||||
resourceIds: 2,
|
|
||||||
allResourceAllocationCount: 2,
|
|
||||||
conflictedAssignmentCount: 1,
|
|
||||||
overlayCount: 2,
|
|
||||||
holidayResourceCount: 2,
|
|
||||||
byScope: [
|
|
||||||
{ scope: "CITY", count: 1 },
|
|
||||||
{ scope: "COUNTRY", count: 1 },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds anonymized project context responses and detail summaries", () => {
|
|
||||||
const directory = {
|
|
||||||
config: { enabled: true, mode: "alias", domain: "example.test" },
|
|
||||||
byResourceId: new Map([
|
|
||||||
[
|
|
||||||
"resource_1",
|
|
||||||
{
|
|
||||||
displayName: "Anon Alice",
|
|
||||||
eid: "ANON-001",
|
|
||||||
email: "anon-alice@example.test",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
byAliasEid: new Map([["anon-001", "resource_1"]]),
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(buildTimelineProjectContextResponse({
|
|
||||||
project: { id: "project_1" },
|
|
||||||
allocations: [
|
|
||||||
{
|
|
||||||
id: "allocation_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
demands: [{ id: "demand_1", projectId: "project_1" }],
|
|
||||||
assignments: [
|
|
||||||
{
|
|
||||||
id: "assignment_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
allResourceAllocations: [
|
|
||||||
{
|
|
||||||
id: "booking_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
directory: directory as never,
|
|
||||||
})).toEqual({
|
|
||||||
project: { id: "project_1" },
|
|
||||||
allocations: [
|
|
||||||
{
|
|
||||||
id: "allocation_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
demands: [{ id: "demand_1", projectId: "project_1" }],
|
|
||||||
assignments: [
|
|
||||||
{
|
|
||||||
id: "assignment_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
allResourceAllocations: [
|
|
||||||
{
|
|
||||||
id: "booking_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(buildTimelineProjectContextDetailResponse({
|
|
||||||
project: { id: "project_1" },
|
|
||||||
period: {
|
|
||||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
||||||
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
||||||
},
|
|
||||||
allocations: [
|
|
||||||
{
|
|
||||||
id: "allocation_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
demands: [{ id: "demand_1", projectId: "project_1" }],
|
|
||||||
assignments: [
|
|
||||||
{
|
|
||||||
id: "assignment_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
allResourceAllocations: [
|
|
||||||
{
|
|
||||||
id: "booking_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Alice", eid: "E-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
assignmentConflicts: [{ crossProjectOverlapCount: 1, assignmentId: "assignment_1" }],
|
|
||||||
holidayOverlays: [],
|
|
||||||
directory: directory as never,
|
|
||||||
})).toEqual({
|
|
||||||
project: { id: "project_1" },
|
|
||||||
period: {
|
|
||||||
startDate: "2026-04-01",
|
|
||||||
endDate: "2026-04-05",
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
allocationCount: 1,
|
|
||||||
demandCount: 1,
|
|
||||||
assignmentCount: 1,
|
|
||||||
projectCount: 1,
|
|
||||||
resourceCount: 1,
|
|
||||||
resourceIds: 1,
|
|
||||||
allResourceAllocationCount: 1,
|
|
||||||
conflictedAssignmentCount: 1,
|
|
||||||
overlayCount: 0,
|
|
||||||
holidayResourceCount: 0,
|
|
||||||
byScope: [],
|
|
||||||
},
|
|
||||||
allocations: [
|
|
||||||
{
|
|
||||||
id: "allocation_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
demands: [{ id: "demand_1", projectId: "project_1" }],
|
|
||||||
assignments: [
|
|
||||||
{
|
|
||||||
id: "assignment_1",
|
|
||||||
projectId: "project_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
allResourceAllocations: [
|
|
||||||
{
|
|
||||||
id: "booking_1",
|
|
||||||
resourceId: "resource_1",
|
|
||||||
resource: { id: "resource_1", displayName: "Anon Alice", eid: "ANON-001" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
assignmentConflicts: [{ crossProjectOverlapCount: 1, assignmentId: "assignment_1" }],
|
|
||||||
holidayOverlays: [],
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("loads detail artifacts with formatted holiday overlays only when resources exist", async () => {
|
it("loads detail artifacts with formatted holiday overlays only when resources exist", async () => {
|
||||||
loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([
|
loadTimelineHolidayOverlaysMock.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
anonymizeResourceOnEntry,
|
||||||
|
fmtDate,
|
||||||
|
summarizeTimelineEntries,
|
||||||
|
} from "./timeline-read-shared.js";
|
||||||
|
import {
|
||||||
|
formatHolidayOverlays,
|
||||||
|
summarizeHolidayOverlays,
|
||||||
|
} from "./timeline-holiday-read.js";
|
||||||
|
|
||||||
|
type TimelineAnonymizationDirectory = Parameters<typeof anonymizeResourceOnEntry>[1];
|
||||||
|
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTimelineProjectContextResponse<
|
||||||
|
TProject,
|
||||||
|
TAllocation extends { resource?: { id: string } | null },
|
||||||
|
TDemand,
|
||||||
|
TAssignment extends { resource?: { id: string } | null },
|
||||||
|
TBooking extends { resource?: { id: string } | null },
|
||||||
|
>(input: {
|
||||||
|
project: TProject;
|
||||||
|
allocations: TAllocation[];
|
||||||
|
demands: TDemand[];
|
||||||
|
assignments: TAssignment[];
|
||||||
|
allResourceAllocations: TBooking[];
|
||||||
|
resourceIds: string[];
|
||||||
|
directory: TimelineAnonymizationDirectory;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
project: input.project,
|
||||||
|
allocations: input.allocations.map((allocation) =>
|
||||||
|
anonymizeResourceOnEntry(allocation, input.directory),
|
||||||
|
),
|
||||||
|
demands: input.demands,
|
||||||
|
assignments: input.assignments.map((assignment) =>
|
||||||
|
anonymizeResourceOnEntry(assignment, input.directory),
|
||||||
|
),
|
||||||
|
allResourceAllocations: input.allResourceAllocations.map((allocation) =>
|
||||||
|
anonymizeResourceOnEntry(allocation, input.directory),
|
||||||
|
),
|
||||||
|
resourceIds: input.resourceIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTimelineProjectContextDetailResponse<
|
||||||
|
TProject,
|
||||||
|
TAllocation extends {
|
||||||
|
projectId: string | null;
|
||||||
|
resourceId: string | null;
|
||||||
|
resource?: { id: string } | null;
|
||||||
|
},
|
||||||
|
TDemand extends { projectId: string | null },
|
||||||
|
TAssignment extends {
|
||||||
|
projectId: string | null;
|
||||||
|
resourceId: string | null;
|
||||||
|
resource?: { id: string } | null;
|
||||||
|
},
|
||||||
|
TBooking extends { resource?: { id: string } | null },
|
||||||
|
TConflict extends { crossProjectOverlapCount: number },
|
||||||
|
>(input: {
|
||||||
|
project: TProject;
|
||||||
|
period: { startDate: Date; endDate: Date };
|
||||||
|
allocations: TAllocation[];
|
||||||
|
demands: TDemand[];
|
||||||
|
assignments: TAssignment[];
|
||||||
|
allResourceAllocations: TBooking[];
|
||||||
|
resourceIds: string[];
|
||||||
|
assignmentConflicts: TConflict[];
|
||||||
|
holidayOverlays: ReturnType<typeof formatHolidayOverlays>;
|
||||||
|
directory: TimelineAnonymizationDirectory;
|
||||||
|
}) {
|
||||||
|
const base = buildTimelineProjectContextResponse({
|
||||||
|
project: input.project,
|
||||||
|
allocations: input.allocations,
|
||||||
|
demands: input.demands,
|
||||||
|
assignments: input.assignments,
|
||||||
|
allResourceAllocations: input.allResourceAllocations,
|
||||||
|
resourceIds: input.resourceIds,
|
||||||
|
directory: input.directory,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
period: {
|
||||||
|
startDate: fmtDate(input.period.startDate),
|
||||||
|
endDate: fmtDate(input.period.endDate),
|
||||||
|
},
|
||||||
|
summary: buildTimelineProjectContextSummary({
|
||||||
|
allocations: input.allocations,
|
||||||
|
demands: input.demands,
|
||||||
|
assignments: input.assignments,
|
||||||
|
resourceIds: input.resourceIds,
|
||||||
|
allResourceAllocations: input.allResourceAllocations,
|
||||||
|
assignmentConflicts: input.assignmentConflicts,
|
||||||
|
holidayOverlays: input.holidayOverlays,
|
||||||
|
}),
|
||||||
|
assignmentConflicts: input.assignmentConflicts,
|
||||||
|
holidayOverlays: input.holidayOverlays,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,23 +1,17 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import {
|
import {
|
||||||
buildTimelineProjectAssignmentConflicts,
|
buildTimelineProjectAssignmentConflicts,
|
||||||
type TimelineProjectAssignmentConflict,
|
|
||||||
} from "./timeline-project-conflict-support.js";
|
} from "./timeline-project-conflict-support.js";
|
||||||
import {
|
import {
|
||||||
formatHolidayOverlays,
|
formatHolidayOverlays,
|
||||||
loadTimelineHolidayOverlays,
|
loadTimelineHolidayOverlays,
|
||||||
summarizeHolidayOverlays,
|
|
||||||
} from "./timeline-holiday-read.js";
|
} from "./timeline-holiday-read.js";
|
||||||
import {
|
import {
|
||||||
anonymizeResourceOnEntry,
|
|
||||||
createTimelineDateRange,
|
createTimelineDateRange,
|
||||||
fmtDate,
|
fmtDate,
|
||||||
summarizeTimelineEntries,
|
|
||||||
toDate,
|
toDate,
|
||||||
} from "./timeline-read-shared.js";
|
} from "./timeline-read-shared.js";
|
||||||
|
|
||||||
type TimelineAnonymizationDirectory = Parameters<typeof anonymizeResourceOnEntry>[1];
|
|
||||||
|
|
||||||
export interface ResolveTimelineProjectContextPeriodInput {
|
export interface ResolveTimelineProjectContextPeriodInput {
|
||||||
requestedStartDate?: string | undefined;
|
requestedStartDate?: string | undefined;
|
||||||
requestedEndDate?: string | undefined;
|
requestedEndDate?: string | undefined;
|
||||||
@@ -62,110 +56,6 @@ export function resolveTimelineProjectContextPeriod(
|
|||||||
return { startDate, endDate };
|
return { startDate, endDate };
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTimelineProjectContextResponse<
|
|
||||||
TProject,
|
|
||||||
TAllocation extends { resource?: { id: string } | null },
|
|
||||||
TDemand,
|
|
||||||
TAssignment extends { resource?: { id: string } | null },
|
|
||||||
TBooking extends { resource?: { id: string } | null },
|
|
||||||
>(input: {
|
|
||||||
project: TProject;
|
|
||||||
allocations: TAllocation[];
|
|
||||||
demands: TDemand[];
|
|
||||||
assignments: TAssignment[];
|
|
||||||
allResourceAllocations: TBooking[];
|
|
||||||
resourceIds: string[];
|
|
||||||
directory: TimelineAnonymizationDirectory;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
project: input.project,
|
|
||||||
allocations: input.allocations.map((allocation) =>
|
|
||||||
anonymizeResourceOnEntry(allocation, input.directory),
|
|
||||||
),
|
|
||||||
demands: input.demands,
|
|
||||||
assignments: input.assignments.map((assignment) =>
|
|
||||||
anonymizeResourceOnEntry(assignment, input.directory),
|
|
||||||
),
|
|
||||||
allResourceAllocations: input.allResourceAllocations.map((allocation) =>
|
|
||||||
anonymizeResourceOnEntry(allocation, input.directory),
|
|
||||||
),
|
|
||||||
resourceIds: input.resourceIds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTimelineProjectContextDetailResponse<
|
|
||||||
TProject,
|
|
||||||
TAllocation extends { projectId: string | null; resourceId: string | null; resource?: { id: string } | null },
|
|
||||||
TDemand extends { projectId: string | null },
|
|
||||||
TAssignment extends { projectId: string | null; resourceId: string | null; resource?: { id: string } | null },
|
|
||||||
TBooking extends { resource?: { id: string } | null },
|
|
||||||
TConflict extends { crossProjectOverlapCount: number },
|
|
||||||
>(input: {
|
|
||||||
project: TProject;
|
|
||||||
period: { startDate: Date; endDate: Date };
|
|
||||||
allocations: TAllocation[];
|
|
||||||
demands: TDemand[];
|
|
||||||
assignments: TAssignment[];
|
|
||||||
allResourceAllocations: TBooking[];
|
|
||||||
resourceIds: string[];
|
|
||||||
assignmentConflicts: TConflict[];
|
|
||||||
holidayOverlays: ReturnType<typeof formatHolidayOverlays>;
|
|
||||||
directory: TimelineAnonymizationDirectory;
|
|
||||||
}) {
|
|
||||||
const base = buildTimelineProjectContextResponse({
|
|
||||||
project: input.project,
|
|
||||||
allocations: input.allocations,
|
|
||||||
demands: input.demands,
|
|
||||||
assignments: input.assignments,
|
|
||||||
allResourceAllocations: input.allResourceAllocations,
|
|
||||||
resourceIds: input.resourceIds,
|
|
||||||
directory: input.directory,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
period: {
|
|
||||||
startDate: fmtDate(input.period.startDate),
|
|
||||||
endDate: fmtDate(input.period.endDate),
|
|
||||||
},
|
|
||||||
summary: buildTimelineProjectContextSummary({
|
|
||||||
allocations: input.allocations,
|
|
||||||
demands: input.demands,
|
|
||||||
assignments: input.assignments,
|
|
||||||
resourceIds: input.resourceIds,
|
|
||||||
allResourceAllocations: input.allResourceAllocations,
|
|
||||||
assignmentConflicts: input.assignmentConflicts,
|
|
||||||
holidayOverlays: input.holidayOverlays,
|
|
||||||
}),
|
|
||||||
assignmentConflicts: input.assignmentConflicts,
|
|
||||||
holidayOverlays: input.holidayOverlays,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTimelineProjectContextDetailArtifacts(
|
export async function loadTimelineProjectContextDetailArtifacts(
|
||||||
db: Parameters<typeof loadTimelineHolidayOverlays>[0],
|
db: Parameters<typeof loadTimelineHolidayOverlays>[0],
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import { getAnonymizationDirectory } from "../lib/anonymization.js";
|
|||||||
import { controllerProcedure } from "../trpc.js";
|
import { controllerProcedure } from "../trpc.js";
|
||||||
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
|
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
|
||||||
import {
|
import {
|
||||||
buildTimelineProjectContextDetailResponse,
|
|
||||||
buildTimelineProjectContextResponse,
|
|
||||||
loadTimelineProjectContextDetailArtifacts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
} from "./timeline-project-context-support.js";
|
} from "./timeline-project-context-support.js";
|
||||||
|
import {
|
||||||
|
buildTimelineProjectContextDetailResponse,
|
||||||
|
buildTimelineProjectContextResponse,
|
||||||
|
} from "./timeline-project-context-response-support.js";
|
||||||
import {
|
import {
|
||||||
buildTimelineBudgetStatusAllocations,
|
buildTimelineBudgetStatusAllocations,
|
||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user