375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildTimelineProjectAssignmentConflicts,
|
|
buildTimelineProjectContextDetailResponse,
|
|
buildTimelineProjectContextResponse,
|
|
buildTimelineProjectContextSummary,
|
|
buildTimelineShiftPreviewDetailResponse,
|
|
resolveTimelineProjectContextPeriod,
|
|
} from "../router/timeline-project-context-support.js";
|
|
|
|
describe("timeline project context support", () => {
|
|
it("derives the detail period from explicit input and validates order", () => {
|
|
expect(resolveTimelineProjectContextPeriod({
|
|
requestedStartDate: "2026-04-03",
|
|
requestedEndDate: "2026-04-12",
|
|
projectStartDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
projectEndDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
})).toEqual({
|
|
startDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-12T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(() =>
|
|
resolveTimelineProjectContextPeriod({
|
|
requestedStartDate: "2026-04-12",
|
|
requestedEndDate: "2026-04-03",
|
|
}),
|
|
).toThrowError(new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "endDate must be on or after startDate.",
|
|
}));
|
|
});
|
|
|
|
it("builds assignment conflict summaries with cross-project overlap counts", () => {
|
|
expect(buildTimelineProjectAssignmentConflicts({
|
|
projectId: "project_1",
|
|
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,
|
|
},
|
|
{
|
|
id: "placeholder_1",
|
|
resourceId: null,
|
|
resource: null,
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 4,
|
|
},
|
|
],
|
|
allResourceAllocations: [
|
|
{
|
|
id: "assignment_1",
|
|
resourceId: "resource_1",
|
|
projectId: "project_1",
|
|
project: { name: "Current", shortCode: "CUR" },
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
hoursPerDay: 8,
|
|
status: "ACTIVE",
|
|
},
|
|
{
|
|
id: "booking_2",
|
|
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",
|
|
},
|
|
{
|
|
id: "booking_3",
|
|
resourceId: "resource_1",
|
|
projectId: "project_1",
|
|
project: { name: "Current", shortCode: "CUR" },
|
|
startDate: new Date("2026-04-02T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
hoursPerDay: 2,
|
|
status: "PROPOSED",
|
|
},
|
|
],
|
|
})).toEqual([
|
|
{
|
|
assignmentId: "assignment_1",
|
|
resourceId: "resource_1",
|
|
resourceName: "Alice",
|
|
startDate: "2026-04-01",
|
|
endDate: "2026-04-05",
|
|
hoursPerDay: 8,
|
|
overlapCount: 2,
|
|
crossProjectOverlapCount: 1,
|
|
overlaps: [
|
|
{
|
|
id: "booking_2",
|
|
projectId: "project_2",
|
|
projectName: "Other",
|
|
projectShortCode: "OTH",
|
|
startDate: "2026-04-04",
|
|
endDate: "2026-04-07",
|
|
hoursPerDay: 6,
|
|
status: "ACTIVE",
|
|
sameProject: false,
|
|
},
|
|
{
|
|
id: "booking_3",
|
|
projectId: "project_1",
|
|
projectName: "Current",
|
|
projectShortCode: "CUR",
|
|
startDate: "2026-04-02",
|
|
endDate: "2026-04-03",
|
|
hoursPerDay: 2,
|
|
status: "PROPOSED",
|
|
sameProject: true,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
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("builds shift preview detail payloads with formatted dates", () => {
|
|
expect(buildTimelineShiftPreviewDetailResponse({
|
|
project: {
|
|
id: "project_1",
|
|
name: "Project One",
|
|
shortCode: "PRJ",
|
|
status: "ACTIVE",
|
|
responsiblePerson: "Alice",
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
|
},
|
|
requestedShift: {
|
|
newStartDate: new Date("2026-04-03T00:00:00.000Z"),
|
|
newEndDate: new Date("2026-04-12T00:00:00.000Z"),
|
|
},
|
|
preview: { valid: true },
|
|
})).toEqual({
|
|
project: {
|
|
id: "project_1",
|
|
name: "Project One",
|
|
shortCode: "PRJ",
|
|
status: "ACTIVE",
|
|
responsiblePerson: "Alice",
|
|
startDate: "2026-04-01",
|
|
endDate: "2026-04-10",
|
|
},
|
|
requestedShift: {
|
|
newStartDate: "2026-04-03",
|
|
newEndDate: "2026-04-12",
|
|
},
|
|
preview: { valid: true },
|
|
});
|
|
});
|
|
});
|