refactor(api): extract timeline entry response support
This commit is contained in:
@@ -0,0 +1,153 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
buildTimelineEntriesDetailResponse,
|
||||||
|
buildTimelineEntriesViewResponse,
|
||||||
|
} from "../router/timeline-entry-response-support.js";
|
||||||
|
|
||||||
|
describe("timeline entry response support", () => {
|
||||||
|
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"]]),
|
||||||
|
};
|
||||||
|
|
||||||
|
it("builds anonymized entries view responses", () => {
|
||||||
|
const readModel = {
|
||||||
|
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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(buildTimelineEntriesViewResponse(readModel, directory as never)).toEqual({
|
||||||
|
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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds detail responses with combined summary and anonymized entries", () => {
|
||||||
|
const readModel = {
|
||||||
|
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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const overlays = [{ id: "overlay_1", resourceId: "resource_1" }];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildTimelineEntriesDetailResponse({
|
||||||
|
period: {
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
projectIds: undefined,
|
||||||
|
clientIds: undefined,
|
||||||
|
chapters: undefined,
|
||||||
|
eids: undefined,
|
||||||
|
countryCodes: undefined,
|
||||||
|
},
|
||||||
|
readModel,
|
||||||
|
directory: directory as never,
|
||||||
|
holidayOverlays: overlays,
|
||||||
|
holidaySummary: {
|
||||||
|
overlayCount: 1,
|
||||||
|
holidayResourceCount: 1,
|
||||||
|
byScope: [{ scope: "COUNTRY", count: 1 }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
period: {
|
||||||
|
startDate: "2026-04-01",
|
||||||
|
endDate: "2026-04-05",
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
projectIds: undefined,
|
||||||
|
clientIds: undefined,
|
||||||
|
chapters: undefined,
|
||||||
|
eids: undefined,
|
||||||
|
countryCodes: undefined,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
allocationCount: 1,
|
||||||
|
demandCount: 1,
|
||||||
|
assignmentCount: 1,
|
||||||
|
projectCount: 1,
|
||||||
|
resourceCount: 1,
|
||||||
|
overlayCount: 1,
|
||||||
|
holidayResourceCount: 1,
|
||||||
|
byScope: [{ scope: "COUNTRY", count: 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" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
holidayOverlays: overlays,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,68 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildTimelineEntriesDetailInput,
|
buildTimelineEntriesDetailInput,
|
||||||
buildTimelineEntriesDetailResponse,
|
|
||||||
buildTimelineEntriesViewResponse,
|
|
||||||
} from "../router/timeline-read-shared.js";
|
} from "../router/timeline-read-shared.js";
|
||||||
|
|
||||||
describe("timeline read shared", () => {
|
describe("timeline read shared", () => {
|
||||||
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"]]),
|
|
||||||
};
|
|
||||||
|
|
||||||
it("builds anonymized entries view responses", () => {
|
|
||||||
const readModel = {
|
|
||||||
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" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(buildTimelineEntriesViewResponse(readModel, directory as never)).toEqual({
|
|
||||||
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" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds detail input from period and normalized filters", () => {
|
it("builds detail input from period and normalized filters", () => {
|
||||||
expect(
|
expect(
|
||||||
buildTimelineEntriesDetailInput({
|
buildTimelineEntriesDetailInput({
|
||||||
@@ -98,92 +39,4 @@ describe("timeline read shared", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds detail responses with combined summary and anonymized entries", () => {
|
|
||||||
const readModel = {
|
|
||||||
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" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const overlays = [{ id: "overlay_1", resourceId: "resource_1" }];
|
|
||||||
|
|
||||||
expect(
|
|
||||||
buildTimelineEntriesDetailResponse({
|
|
||||||
period: {
|
|
||||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
||||||
endDate: new Date("2026-04-05T00:00:00.000Z"),
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
projectIds: undefined,
|
|
||||||
clientIds: undefined,
|
|
||||||
chapters: undefined,
|
|
||||||
eids: undefined,
|
|
||||||
countryCodes: undefined,
|
|
||||||
},
|
|
||||||
readModel,
|
|
||||||
directory: directory as never,
|
|
||||||
holidayOverlays: overlays,
|
|
||||||
holidaySummary: {
|
|
||||||
overlayCount: 1,
|
|
||||||
holidayResourceCount: 1,
|
|
||||||
byScope: [{ scope: "COUNTRY", count: 1 }],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
period: {
|
|
||||||
startDate: "2026-04-01",
|
|
||||||
endDate: "2026-04-05",
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
resourceIds: ["resource_1"],
|
|
||||||
projectIds: undefined,
|
|
||||||
clientIds: undefined,
|
|
||||||
chapters: undefined,
|
|
||||||
eids: undefined,
|
|
||||||
countryCodes: undefined,
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
allocationCount: 1,
|
|
||||||
demandCount: 1,
|
|
||||||
assignmentCount: 1,
|
|
||||||
projectCount: 1,
|
|
||||||
resourceCount: 1,
|
|
||||||
overlayCount: 1,
|
|
||||||
holidayResourceCount: 1,
|
|
||||||
byScope: [{ scope: "COUNTRY", count: 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" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
holidayOverlays: overlays,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { controllerProcedure, protectedProcedure } from "../trpc.js";
|
|||||||
import {
|
import {
|
||||||
buildSelfServiceTimelineInput,
|
buildSelfServiceTimelineInput,
|
||||||
buildTimelineEntriesDetailInput,
|
buildTimelineEntriesDetailInput,
|
||||||
buildTimelineEntriesDetailResponse,
|
|
||||||
buildTimelineEntriesViewResponse,
|
|
||||||
createEmptyTimelineEntriesView,
|
createEmptyTimelineEntriesView,
|
||||||
loadTimelineEntriesReadModel,
|
loadTimelineEntriesReadModel,
|
||||||
TimelineWindowFiltersSchema,
|
TimelineWindowFiltersSchema,
|
||||||
} from "./timeline-read-shared.js";
|
} from "./timeline-read-shared.js";
|
||||||
|
import {
|
||||||
|
buildTimelineEntriesDetailResponse,
|
||||||
|
buildTimelineEntriesViewResponse,
|
||||||
|
} from "./timeline-entry-response-support.js";
|
||||||
import {
|
import {
|
||||||
formatHolidayOverlays,
|
formatHolidayOverlays,
|
||||||
loadTimelineHolidayOverlaysForReadModel,
|
loadTimelineHolidayOverlaysForReadModel,
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||||
|
import { fmtDate, type TimelineEntriesFilters } from "./timeline-read-shared.js";
|
||||||
|
|
||||||
|
type TimelineAnonymizationDirectory = Awaited<ReturnType<typeof getAnonymizationDirectory>>;
|
||||||
|
|
||||||
|
export function anonymizeResourceOnEntry<T extends { resource?: { id: string } | null }>(
|
||||||
|
entry: T,
|
||||||
|
directory: TimelineAnonymizationDirectory,
|
||||||
|
): T {
|
||||||
|
if (!entry.resource) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
resource: anonymizeResource(entry.resource, directory),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTimelineEntriesViewResponse<
|
||||||
|
TReadModel extends {
|
||||||
|
allocations: Array<{ resource?: { id: string } | null }>;
|
||||||
|
assignments: Array<{ resource?: { id: string } | null }>;
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
readModel: TReadModel,
|
||||||
|
directory: TimelineAnonymizationDirectory,
|
||||||
|
): TReadModel {
|
||||||
|
return {
|
||||||
|
...readModel,
|
||||||
|
allocations: readModel.allocations.map((allocation) =>
|
||||||
|
anonymizeResourceOnEntry(allocation, directory),
|
||||||
|
),
|
||||||
|
assignments: readModel.assignments.map((assignment) =>
|
||||||
|
anonymizeResourceOnEntry(assignment, directory),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeTimelineEntries(readModel: {
|
||||||
|
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||||
|
demands: Array<{ projectId: string | null }>;
|
||||||
|
assignments: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||||
|
}) {
|
||||||
|
const projectIds = new Set<string>();
|
||||||
|
const resourceIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const entry of [...readModel.allocations, ...readModel.demands, ...readModel.assignments]) {
|
||||||
|
if (entry.projectId) {
|
||||||
|
projectIds.add(entry.projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const assignment of [...readModel.allocations, ...readModel.assignments]) {
|
||||||
|
if (assignment.resourceId) {
|
||||||
|
resourceIds.add(assignment.resourceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allocationCount: readModel.allocations.length,
|
||||||
|
demandCount: readModel.demands.length,
|
||||||
|
assignmentCount: readModel.assignments.length,
|
||||||
|
projectCount: projectIds.size,
|
||||||
|
resourceCount: resourceIds.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTimelineEntriesDetailResponse<
|
||||||
|
TReadModel extends {
|
||||||
|
allocations: Array<{
|
||||||
|
projectId: string | null;
|
||||||
|
resourceId: string | null;
|
||||||
|
resource?: { id: string } | null;
|
||||||
|
}>;
|
||||||
|
demands: Array<{ projectId: string | null }>;
|
||||||
|
assignments: Array<{
|
||||||
|
projectId: string | null;
|
||||||
|
resourceId: string | null;
|
||||||
|
resource?: { id: string } | null;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
THolidayOverlay,
|
||||||
|
>(input: {
|
||||||
|
period: { startDate: Date; endDate: Date };
|
||||||
|
filters: Omit<TimelineEntriesFilters, "startDate" | "endDate">;
|
||||||
|
readModel: TReadModel;
|
||||||
|
directory: TimelineAnonymizationDirectory;
|
||||||
|
holidayOverlays: THolidayOverlay[];
|
||||||
|
holidaySummary: {
|
||||||
|
overlayCount: number;
|
||||||
|
holidayResourceCount: number;
|
||||||
|
byScope: Array<{ scope: string; count: number }>;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
const view = buildTimelineEntriesViewResponse(input.readModel, input.directory);
|
||||||
|
|
||||||
|
return {
|
||||||
|
period: {
|
||||||
|
startDate: fmtDate(input.period.startDate),
|
||||||
|
endDate: fmtDate(input.period.endDate),
|
||||||
|
},
|
||||||
|
filters: input.filters,
|
||||||
|
summary: {
|
||||||
|
...summarizeTimelineEntries(input.readModel),
|
||||||
|
...input.holidaySummary,
|
||||||
|
},
|
||||||
|
allocations: view.allocations,
|
||||||
|
demands: input.readModel.demands,
|
||||||
|
assignments: view.assignments,
|
||||||
|
holidayOverlays: input.holidayOverlays,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import {
|
||||||
|
fmtDate,
|
||||||
|
} from "./timeline-read-shared.js";
|
||||||
import {
|
import {
|
||||||
anonymizeResourceOnEntry,
|
anonymizeResourceOnEntry,
|
||||||
fmtDate,
|
|
||||||
summarizeTimelineEntries,
|
summarizeTimelineEntries,
|
||||||
} from "./timeline-read-shared.js";
|
} from "./timeline-entry-response-support.js";
|
||||||
import {
|
import {
|
||||||
summarizeHolidayOverlays,
|
summarizeHolidayOverlays,
|
||||||
} from "./timeline-holiday-read.js";
|
} from "./timeline-holiday-read.js";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { buildSplitAllocationReadModel } from "@capakraken/application";
|
import { buildSplitAllocationReadModel } from "@capakraken/application";
|
||||||
import type { PrismaClient } from "@capakraken/db";
|
import type { PrismaClient } from "@capakraken/db";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
|
|
||||||
import { loadTimelineEntryRecords } from "./timeline-entry-query-support.js";
|
import { loadTimelineEntryRecords } from "./timeline-entry-query-support.js";
|
||||||
export {
|
export {
|
||||||
buildSelfServiceTimelineInput,
|
buildSelfServiceTimelineInput,
|
||||||
@@ -43,7 +42,6 @@ export const TimelineWindowFiltersSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type TimelineWindowFiltersInput = z.infer<typeof TimelineWindowFiltersSchema>;
|
type TimelineWindowFiltersInput = z.infer<typeof TimelineWindowFiltersSchema>;
|
||||||
type TimelineAnonymizationDirectory = Awaited<ReturnType<typeof getAnonymizationDirectory>>;
|
|
||||||
|
|
||||||
export function getAssignmentResourceIds(
|
export function getAssignmentResourceIds(
|
||||||
readModel: ReturnType<typeof buildSplitAllocationReadModel>,
|
readModel: ReturnType<typeof buildSplitAllocationReadModel>,
|
||||||
@@ -71,89 +69,6 @@ export function createEmptyTimelineEntriesView() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTimelineEntriesViewResponse<
|
|
||||||
TReadModel extends {
|
|
||||||
allocations: Array<{ resource?: { id: string } | null }>;
|
|
||||||
assignments: Array<{ resource?: { id: string } | null }>;
|
|
||||||
},
|
|
||||||
>(
|
|
||||||
readModel: TReadModel,
|
|
||||||
directory: TimelineAnonymizationDirectory,
|
|
||||||
): TReadModel {
|
|
||||||
return {
|
|
||||||
...readModel,
|
|
||||||
allocations: readModel.allocations.map((allocation) => anonymizeResourceOnEntry(allocation, directory)),
|
|
||||||
assignments: readModel.assignments.map((assignment) => anonymizeResourceOnEntry(assignment, directory)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function summarizeTimelineEntries(readModel: {
|
|
||||||
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
|
||||||
demands: Array<{ projectId: string | null }>;
|
|
||||||
assignments: Array<{ projectId: string | null; resourceId: string | null }>;
|
|
||||||
}) {
|
|
||||||
const projectIds = new Set<string>();
|
|
||||||
const resourceIds = new Set<string>();
|
|
||||||
|
|
||||||
for (const entry of [...readModel.allocations, ...readModel.demands, ...readModel.assignments]) {
|
|
||||||
if (entry.projectId) {
|
|
||||||
projectIds.add(entry.projectId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const assignment of [...readModel.allocations, ...readModel.assignments]) {
|
|
||||||
if (assignment.resourceId) {
|
|
||||||
resourceIds.add(assignment.resourceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allocationCount: readModel.allocations.length,
|
|
||||||
demandCount: readModel.demands.length,
|
|
||||||
assignmentCount: readModel.assignments.length,
|
|
||||||
projectCount: projectIds.size,
|
|
||||||
resourceCount: resourceIds.size,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTimelineEntriesDetailResponse<
|
|
||||||
TReadModel extends {
|
|
||||||
allocations: Array<{ projectId: string | null; resourceId: string | null; resource?: { id: string } | null }>;
|
|
||||||
demands: Array<{ projectId: string | null }>;
|
|
||||||
assignments: Array<{ projectId: string | null; resourceId: string | null; resource?: { id: string } | null }>;
|
|
||||||
},
|
|
||||||
THolidayOverlay,
|
|
||||||
>(input: {
|
|
||||||
period: { startDate: Date; endDate: Date };
|
|
||||||
filters: Omit<TimelineEntriesFilters, "startDate" | "endDate">;
|
|
||||||
readModel: TReadModel;
|
|
||||||
directory: TimelineAnonymizationDirectory;
|
|
||||||
holidayOverlays: THolidayOverlay[];
|
|
||||||
holidaySummary: {
|
|
||||||
overlayCount: number;
|
|
||||||
holidayResourceCount: number;
|
|
||||||
byScope: Array<{ scope: string; count: number }>;
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
const view = buildTimelineEntriesViewResponse(input.readModel, input.directory);
|
|
||||||
|
|
||||||
return {
|
|
||||||
period: {
|
|
||||||
startDate: fmtDate(input.period.startDate),
|
|
||||||
endDate: fmtDate(input.period.endDate),
|
|
||||||
},
|
|
||||||
filters: input.filters,
|
|
||||||
summary: {
|
|
||||||
...summarizeTimelineEntries(input.readModel),
|
|
||||||
...input.holidaySummary,
|
|
||||||
},
|
|
||||||
allocations: view.allocations,
|
|
||||||
demands: input.readModel.demands,
|
|
||||||
assignments: view.assignments,
|
|
||||||
holidayOverlays: input.holidayOverlays,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rangesOverlap(
|
export function rangesOverlap(
|
||||||
leftStart: Date,
|
leftStart: Date,
|
||||||
leftEnd: Date,
|
leftEnd: Date,
|
||||||
@@ -167,19 +82,6 @@ export function toDate(value: Date | string): Date {
|
|||||||
return value instanceof Date ? value : new Date(value);
|
return value instanceof Date ? value : new Date(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function anonymizeResourceOnEntry<T extends { resource?: { id: string } | null }>(
|
|
||||||
entry: T,
|
|
||||||
directory: Awaited<ReturnType<typeof getAnonymizationDirectory>>,
|
|
||||||
): T {
|
|
||||||
if (!entry.resource) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...entry,
|
|
||||||
resource: anonymizeResource(entry.resource, directory),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTimelineEntriesReadModel(
|
export async function loadTimelineEntriesReadModel(
|
||||||
db: TimelineEntriesDbClient,
|
db: TimelineEntriesDbClient,
|
||||||
input: TimelineEntriesFilters,
|
input: TimelineEntriesFilters,
|
||||||
|
|||||||
Reference in New Issue
Block a user