import { SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, getDashboardOverview: vi.fn(), getDashboardPeakTimes: vi.fn(), getDashboardDemand: vi.fn(), getDashboardTopValueResources: vi.fn(), getDashboardChargeabilityOverview: vi.fn(), getDashboardBudgetForecast: vi.fn(), getDashboardSkillGapSummary: vi.fn(), getDashboardProjectHealth: vi.fn(), }; }); vi.mock("../lib/cache.js", () => ({ cacheGet: vi.fn().mockResolvedValue(null), cacheSet: vi.fn().mockResolvedValue(undefined), })); vi.mock("../lib/anonymization.js", () => ({ anonymizeResources: vi.fn((resources: unknown[]) => resources), getAnonymizationDirectory: vi.fn().mockResolvedValue(null), })); import { getDashboardOverview, getDashboardPeakTimes, getDashboardDemand, getDashboardTopValueResources, getDashboardChargeabilityOverview, getDashboardBudgetForecast, getDashboardProjectHealth, getDashboardSkillGapSummary, } from "@capakraken/application"; import { dashboardRouter } from "../router/dashboard.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(dashboardRouter); function createProtectedCaller(db: Record) { return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createControllerCaller(db: Record) { return createCaller({ session: { user: { email: "controller@example.com", name: "Controller", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_2", systemRole: SystemRole.CONTROLLER, permissionOverrides: null, }, }); } function createUnauthenticatedCaller(db: Record) { return createCaller({ session: null, db: db as never, dbUser: null, }); } describe("dashboard router", () => { beforeEach(() => { vi.clearAllMocks(); }); // ─── getOverview ────────────────────────────────────────────────────────── describe("getOverview", () => { it("returns expected shape with resource and project counts", async () => { const overview = { totalResources: 42, activeResources: 38, totalProjects: 15, activeProjects: 10, draftProjects: 3, completedProjects: 2, totalBudgetCents: 5_000_000_00, avgWinProbability: 78, }; vi.mocked(getDashboardOverview).mockResolvedValue(overview); const caller = createControllerCaller({}); const result = await caller.getOverview(); expect(result).toMatchObject({ totalResources: 42, activeResources: 38, totalProjects: 15, activeProjects: 10, }); expect(getDashboardOverview).toHaveBeenCalledTimes(1); }); it("rejects unauthenticated users", async () => { const caller = createUnauthenticatedCaller({}); await expect(caller.getOverview()).rejects.toThrow("Authentication required"); }); }); describe("getStatisticsDetail", () => { it("returns assistant-friendly statistics derived from the canonical dashboard overview", async () => { vi.mocked(getDashboardOverview).mockResolvedValue({ totalResources: 12, activeResources: 10, inactiveResources: 2, totalProjects: 7, activeProjects: 4, inactiveProjects: 3, totalAllocations: 21, activeAllocations: 18, cancelledAllocations: 3, approvedVacations: 6, totalEstimates: 9, budgetSummary: { totalBudgetCents: 1_234_56, totalCostCents: 654_32, avgUtilizationPercent: 53, }, budgetBasis: { remainingBudgetCents: 58_024, budgetedProjects: 5, unbudgetedProjects: 2, trackedAssignmentCount: 18, windowStart: null, windowEnd: null, }, projectsByStatus: [ { status: "ACTIVE", count: 4 }, { status: "DRAFT", count: 2 }, { status: "DONE", count: 1 }, ], chapterUtilization: [ { chapter: "CGI", resourceCount: 5, avgChargeabilityTarget: 78 }, { chapter: "Compositing", resourceCount: 3, avgChargeabilityTarget: 74 }, { chapter: "Unassigned", resourceCount: 2, avgChargeabilityTarget: 0 }, ], recentActivity: [], }); const caller = createControllerCaller({}); const result = await caller.getStatisticsDetail(); expect(getDashboardOverview).toHaveBeenCalledTimes(1); expect(result).toEqual({ activeResources: 10, totalProjects: 7, activeProjects: 4, totalAllocations: 21, approvedVacations: 6, totalEstimates: 9, totalBudget: "1.234,56 EUR", projectsByStatus: { ACTIVE: 4, DRAFT: 2, DONE: 1, }, topChapters: [ { chapter: "CGI", count: 5 }, { chapter: "Compositing", count: 3 }, { chapter: "Unassigned", count: 2 }, ], }); }); }); // ─── getPeakTimes ───────────────────────────────────────────────────────── describe("getPeakTimes", () => { it("returns array of time periods", async () => { const peakData = [ { period: "2026-03", totalHours: 1200, entries: 15 }, { period: "2026-04", totalHours: 1400, entries: 18 }, ]; vi.mocked(getDashboardPeakTimes).mockResolvedValue(peakData); const caller = createControllerCaller({}); const result = await caller.getPeakTimes({ startDate: "2026-03-01T00:00:00.000Z", endDate: "2026-06-30T00:00:00.000Z", granularity: "month", groupBy: "project", }); expect(result).toHaveLength(2); expect(result[0]).toHaveProperty("period", "2026-03"); expect(getDashboardPeakTimes).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ granularity: "month", groupBy: "project", }), ); }); it("passes week granularity to application layer", async () => { vi.mocked(getDashboardPeakTimes).mockResolvedValue([]); const caller = createControllerCaller({}); await caller.getPeakTimes({ startDate: "2026-03-01T00:00:00.000Z", endDate: "2026-03-31T00:00:00.000Z", granularity: "week", groupBy: "chapter", }); expect(getDashboardPeakTimes).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ granularity: "week", groupBy: "chapter", }), ); }); }); // ─── getDemand ──────────────────────────────────────────────────────────── describe("getDemand", () => { it("returns demand entries grouped by project", async () => { const demandData = [ { groupKey: "Project Alpha", totalHours: 500, headcount: 3 }, { groupKey: "Project Beta", totalHours: 300, headcount: 2 }, ]; vi.mocked(getDashboardDemand).mockResolvedValue(demandData); const caller = createControllerCaller({}); const result = await caller.getDemand({ startDate: "2026-01-01T00:00:00.000Z", endDate: "2026-12-31T00:00:00.000Z", groupBy: "project", }); expect(result).toHaveLength(2); expect(getDashboardDemand).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ groupBy: "project" }), ); }); it("supports grouping by chapter", async () => { vi.mocked(getDashboardDemand).mockResolvedValue([]); const caller = createControllerCaller({}); await caller.getDemand({ startDate: "2026-06-01T00:00:00.000Z", endDate: "2026-06-30T00:00:00.000Z", groupBy: "chapter", }); expect(getDashboardDemand).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ groupBy: "chapter" }), ); }); }); describe("getProjectHealthDetail", () => { it("returns assistant-friendly health detail derived from the canonical dashboard read model", async () => { vi.mocked(getDashboardProjectHealth).mockResolvedValue([ { id: "project_critical", projectName: "Critical Project", shortCode: "CRIT", status: "ACTIVE", clientId: "client_1", clientName: "Acme", budgetHealth: 25, staffingHealth: 40, timelineHealth: 30, compositeScore: 35, budgetCents: 100_000, spentCents: 82_000, remainingBudgetCents: 18_000, budgetUtilizationPercent: 82, demandHeadcountTotal: 5, demandHeadcountFilled: 2, demandHeadcountOpen: 3, demandRequirementCount: 2, plannedEndDate: new Date("2026-06-30T00:00:00.000Z"), daysUntilEndDate: 12, timelineStatus: "DUE_SOON", calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", assignmentCount: 2, spentCents: 52_000, }, ], derivation: { periodStart: "2026-06-01", periodEnd: "2026-06-30", calendarContextCount: 1, holidayAwareAssignmentCount: 2, fallbackAssignmentCount: 0, baseSpentCents: 90_000, adjustedSpentCents: 82_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 5_000, absenceDayEquivalent: 0.6, absenceCostDeductionCents: 3_000, }, }, { id: "project_healthy", projectName: "Healthy Project", shortCode: "HLTH", status: "ACTIVE", clientId: "client_1", clientName: "Acme", budgetHealth: 90, staffingHealth: 92, timelineHealth: 86, compositeScore: 89, budgetCents: 200_000, spentCents: 20_000, remainingBudgetCents: 180_000, budgetUtilizationPercent: 10, demandHeadcountTotal: 4, demandHeadcountFilled: 4, demandHeadcountOpen: 0, demandRequirementCount: 1, plannedEndDate: new Date("2026-09-15T00:00:00.000Z"), daysUntilEndDate: 89, timelineStatus: "ON_TRACK", calendarLocations: [], }, ]); const caller = createControllerCaller({}); const result = await caller.getProjectHealthDetail(); expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1); expect(result).toEqual({ projects: [ { projectId: "project_critical", projectName: "Critical Project", shortCode: "CRIT", status: "ACTIVE", overall: 35, budget: 25, staffing: 40, timeline: 30, rating: "critical", budgetBasis: { budgetCents: 100_000, spentCents: 82_000, remainingBudgetCents: 18_000, budgetUtilizationPercent: 82, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", assignmentCount: 2, spentCents: 52_000, }, ], derivation: { periodStart: "2026-06-01", periodEnd: "2026-06-30", calendarContextCount: 1, holidayAwareAssignmentCount: 2, fallbackAssignmentCount: 0, baseSpentCents: 90_000, adjustedSpentCents: 82_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 5_000, absenceDayEquivalent: 0.6, absenceCostDeductionCents: 3_000, }, }, staffingBasis: { demandHeadcountTotal: 5, demandHeadcountFilled: 2, demandHeadcountOpen: 3, demandRequirementCount: 2, }, timelineBasis: { plannedEndDate: "2026-06-30T00:00:00.000Z", daysUntilEndDate: 12, timelineStatus: "DUE_SOON", }, context: { clientId: "client_1", clientName: "Acme", }, }, { projectId: "project_healthy", projectName: "Healthy Project", shortCode: "HLTH", status: "ACTIVE", overall: 89, budget: 90, staffing: 92, timeline: 86, rating: "healthy", budgetBasis: { budgetCents: 200_000, spentCents: 20_000, remainingBudgetCents: 180_000, budgetUtilizationPercent: 10, calendarLocations: [], derivation: null, }, staffingBasis: { demandHeadcountTotal: 4, demandHeadcountFilled: 4, demandHeadcountOpen: 0, demandRequirementCount: 1, }, timelineBasis: { plannedEndDate: "2026-09-15T00:00:00.000Z", daysUntilEndDate: 89, timelineStatus: "ON_TRACK", }, context: { clientId: "client_1", clientName: "Acme", }, }, ], summary: { healthy: 1, atRisk: 0, critical: 1, }, }); }); }); // ─── getTopValueResources ───────────────────────────────────────────────── describe("getTopValueResources", () => { it("returns sorted resources with default limit", async () => { const resources = [ { id: "res_1", eid: "alice", displayName: "Alice", chapter: "Delivery", valueScore: 95, valueScoreBreakdown: { skillDepth: 88, skillBreadth: 79, costEfficiency: 84, chargeability: 76, experience: 90, total: 95, }, valueScoreUpdatedAt: new Date("2026-03-01T00:00:00.000Z"), lcrCents: 12_300, countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", }, { id: "res_2", eid: "bob", displayName: "Bob", chapter: "Data", valueScore: 88, valueScoreBreakdown: { skillDepth: 80, skillBreadth: 77, costEfficiency: 91, chargeability: 72, experience: 81, total: 88, }, valueScoreUpdatedAt: new Date("2026-03-02T00:00:00.000Z"), lcrCents: 10_800, countryCode: "US", countryName: "United States", federalState: "CA", metroCityName: "San Francisco", }, ]; vi.mocked(getDashboardTopValueResources).mockResolvedValue(resources); const caller = createControllerCaller({}); const result = await caller.getTopValueResources({ limit: 10 }); expect(result).toEqual(resources); expect(getDashboardTopValueResources).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ limit: 10 }), ); }); it("respects custom limit", async () => { vi.mocked(getDashboardTopValueResources).mockResolvedValue([]); const caller = createControllerCaller({}); await caller.getTopValueResources({ limit: 5 }); expect(getDashboardTopValueResources).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ limit: 5 }), ); }); }); // ─── getChargeabilityOverview ───────────────────────────────────────────── describe("getChargeabilityOverview", () => { it("returns chargeability data with top and watchlist arrays", async () => { const overview = { avgChargeability: 72, top: [{ id: "res_1", displayName: "Alice", chargeability: 95 }], watchlist: [{ id: "res_3", displayName: "Carol", chargeability: 30 }], }; vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue(overview); const caller = createControllerCaller({}); const result = await caller.getChargeabilityOverview({ includeProposed: false, topN: 10, watchlistThreshold: 15, }); expect(result).toHaveProperty("top"); expect(result).toHaveProperty("watchlist"); expect(result.top).toHaveLength(1); expect(result.watchlist).toHaveLength(1); }); it("passes includeProposed flag to application layer", async () => { vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({ avgChargeability: 60, top: [], watchlist: [], }); const caller = createControllerCaller({}); await caller.getChargeabilityOverview({ includeProposed: true, topN: 5, watchlistThreshold: 20, }); expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ includeProposed: true, topN: 5, watchlistThreshold: 20, }), ); }); it("requires controller role — blocks USER", async () => { const caller = createProtectedCaller({}); await expect( caller.getChargeabilityOverview({ includeProposed: false, topN: 10, watchlistThreshold: 15, }), ).rejects.toThrow( expect.objectContaining({ code: "FORBIDDEN" }), ); }); }); describe("getBudgetForecast", () => { it("returns budget forecast rows with calendar location context", async () => { vi.mocked(getDashboardBudgetForecast).mockResolvedValue([ { projectId: "project_1", projectName: "Alpha", shortCode: "ALPHA", clientId: "client_1", clientName: "Client One", budgetCents: 100_000, spentCents: 40_000, remainingCents: 60_000, burnRate: 10_000, estimatedExhaustionDate: "2026-06-30", pctUsed: 40, activeAssignmentCount: 2, derivation: { periodStart: "2026-03-01", periodEnd: "2026-03-31", calendarContextCount: 1, holidayAwareAssignmentCount: 2, fallbackAssignmentCount: 0, baseBurnRateCents: 12_000, adjustedBurnRateCents: 10_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 1_000, absenceDayEquivalent: 0.5, absenceCostDeductionCents: 1_000, }, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", activeAssignmentCount: 2, burnRateCents: 10_000, }, ], }, ]); const caller = createControllerCaller({}); const result = await caller.getBudgetForecast(); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ projectName: "Alpha", activeAssignmentCount: 2, derivation: { holidayAwareAssignmentCount: 2, baseBurnRateCents: 12_000, adjustedBurnRateCents: 10_000, }, calendarLocations: [ expect.objectContaining({ countryCode: "DE", federalState: "BY", metroCityName: "Munich", }), ], }); expect(getDashboardBudgetForecast).toHaveBeenCalledTimes(1); }); it("returns assistant-friendly budget forecast detail derived from the canonical dashboard read model", async () => { vi.mocked(getDashboardBudgetForecast).mockResolvedValue([ { projectId: "project_1", projectName: "Alpha", shortCode: "ALPHA", clientId: "client_1", clientName: "Client One", budgetCents: 100_000, spentCents: 40_000, remainingCents: 60_000, burnRate: 10_000, estimatedExhaustionDate: "2026-06-30", pctUsed: 40, activeAssignmentCount: 2, derivation: { periodStart: "2026-03-01", periodEnd: "2026-03-31", calendarContextCount: 1, holidayAwareAssignmentCount: 2, fallbackAssignmentCount: 0, baseBurnRateCents: 12_000, adjustedBurnRateCents: 10_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 1_000, absenceDayEquivalent: 0.5, absenceCostDeductionCents: 1_000, }, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Munich", activeAssignmentCount: 2, burnRateCents: 10_000, }, ], }, ]); const caller = createControllerCaller({}); const result = await caller.getBudgetForecastDetail(); expect(getDashboardBudgetForecast).toHaveBeenCalledTimes(1); expect(result).toEqual({ forecasts: [ expect.objectContaining({ projectId: "project_1", projectName: "Alpha", shortCode: "ALPHA", budgetCents: 100_000, spentCents: 40_000, remainingCents: 60_000, projectedCents: 100_000, burnRateCents: 10_000, utilization: "40%", derivation: expect.objectContaining({ holidayAwareAssignmentCount: 2, baseBurnRateCents: 12_000, adjustedBurnRateCents: 10_000, publicHolidayCostDeductionCents: 1_000, absenceCostDeductionCents: 1_000, }), burnStatus: "on_track", calendarLocations: [ expect.objectContaining({ countryCode: "DE", federalState: "BY", metroCityName: "Munich", }), ], }), ], }); }); }); describe("getDetail", () => { it("returns the canonical assistant dashboard detail payload", async () => { vi.mocked(getDashboardOverview).mockResolvedValue({ budgetBasis: { windowStart: "2026-01-01T00:00:00.000Z", windowEnd: "2026-06-30T00:00:00.000Z", }, chapterUtilization: [ { chapter: "Delivery", resourceCount: 4, avgChargeabilityTarget: 78, }, ], }); vi.mocked(getDashboardPeakTimes).mockResolvedValue([ { period: "2026-03", totalHours: 320.4, capacityHours: 400.2, utilizationPct: 80, derivation: { periodStart: "2026-03-01", periodEnd: "2026-03-31", calendarContextCount: 1, resourceCount: 4, groupCount: 1, baseAvailableHours: 416.2, effectiveAvailableHours: 400.2, publicHolidayHoursDeduction: 16, absenceDayEquivalent: 1.5, absenceHoursDeduction: 12.5, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", resourceCount: 4, effectiveAvailableHours: 400.2, }, ], bookedHours: 320.4, capacityHours: 400.2, remainingCapacityHours: 79.8, overbookedHours: 0, utilizationPct: 80, }, }, ]); vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({ roleGaps: [ { role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 }, { role: "Lighting TD", needed: 2, filled: 1, gap: 1, fillRate: 50 }, ], totalOpenPositions: 4, skillSupplyTop10: [ { skill: "houdini", resourceCount: 5 }, { skill: "nuke", resourceCount: 4 }, ], resourcesByRole: [ { role: "Compositor", count: 6 }, { role: "Pipeline TD", count: 2 }, ], }); vi.mocked(getDashboardTopValueResources).mockResolvedValue([ { id: "res_1", eid: "pparker", displayName: "Peter Parker", chapter: "Delivery", valueScore: 91, valueScoreBreakdown: { skillDepth: 85, skillBreadth: 74, costEfficiency: 93, chargeability: 78, experience: 88, total: 91, }, valueScoreUpdatedAt: new Date("2026-03-03T00:00:00.000Z"), lcrCents: 9_500, countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", }, ]); vi.mocked(getDashboardDemand).mockResolvedValue([ { id: "project_1", name: "Gelddruckmaschine", shortCode: "GDM", allocatedHours: 120, requiredFTEs: 4, resourceCount: 2, derivation: { periodStart: "2026-01-01", periodEnd: "2026-06-30", periodWorkingHoursBase: 1040, requiredHours: 2080, requiredFTEs: 4, fillPct: 50, demandSource: "DEMAND_REQUIREMENTS", calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", resourceCount: 2, allocatedHours: 120, }, ], }, }, ]); vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({ rows: [ { id: "res_1", eid: "pparker", displayName: "Peter Parker", chapter: "Delivery", chargeabilityTarget: 78, actualChargeability: 70, expectedChargeability: 76, derivation: { weeklyAvailabilityHours: 40, baseWorkingDays: 22, effectiveWorkingDayEquivalent: 21, baseAvailableHours: 176, effectiveAvailableHours: 168, publicHolidayCount: 1, publicHolidayWorkdayCount: 1, publicHolidayHoursDeduction: 8, absenceDayEquivalent: 0, absenceHoursDeduction: 0, actualBookedHours: 117.6, expectedBookedHours: 127.7, targetBookedHours: 131, unassignedHours: 40.3, }, }, ], top: [], watchlist: [], month: "2026-03", }); vi.mocked(getDashboardProjectHealth).mockResolvedValue([ { id: "project_1", projectName: "Gelddruckmaschine", shortCode: "GDM", status: "ACTIVE", clientId: "client_1", clientName: "Acme", budgetHealth: 58, staffingHealth: 46, timelineHealth: 61, compositeScore: 55, budgetUtilizationPercent: 73, remainingBudgetCents: 88_000, demandHeadcountTotal: 4, demandHeadcountFilled: 2, demandHeadcountOpen: 2, demandRequirementCount: 2, plannedEndDate: new Date("2026-09-30T00:00:00.000Z"), daysUntilEndDate: 183, timelineStatus: "DUE_SOON", derivation: { periodStart: "2026-01-01", periodEnd: "2026-06-30", calendarContextCount: 1, holidayAwareAssignmentCount: 2, fallbackAssignmentCount: 0, baseSpentCents: 140_000, adjustedSpentCents: 132_000, publicHolidayDayEquivalent: 1, publicHolidayCostDeductionCents: 5_000, absenceDayEquivalent: 0.5, absenceCostDeductionCents: 3_000, }, }, ]); const caller = createControllerCaller({}); const result = await caller.getDetail({ section: "all" }); expect(result).toEqual({ peakTimes: [ { month: "2026-03", totalHours: 320.4, totalHoursPerDay: 320.4, capacityHours: 400.2, utilizationPct: 80, calendarContextCount: 1, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", resourceCount: 4, effectiveAvailableHours: 400.2, }, ], explainability: { periodStart: "2026-03-01", periodEnd: "2026-03-31", resourceCount: 4, groupCount: 1, baseAvailableHours: 416.2, effectiveAvailableHours: 400.2, publicHolidayHoursDeduction: 16, absenceDayEquivalent: 1.5, absenceHoursDeduction: 12.5, remainingCapacityHours: 79.8, overbookedHours: 0, }, }, ], topResources: [ { name: "Peter Parker", eid: "pparker", chapter: "Delivery", lcr: "95,00 EUR", valueScore: 91, valueScoreBreakdown: { skillDepth: 85, skillBreadth: 74, costEfficiency: 93, chargeability: 78, experience: 88, total: 91, }, valueScoreUpdatedAt: "2026-03-03T00:00:00.000Z", countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", }, ], demandPipeline: [ { project: "Gelddruckmaschine (GDM)", needed: 2, requiredFTEs: 4, allocatedResources: 2, allocatedHours: 120, calendarLocations: [ { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Augsburg", resourceCount: 2, allocatedHours: 120, }, ], explainability: { periodStart: "2026-01-01", periodEnd: "2026-06-30", periodWorkingHoursBase: 1040, requiredHours: 2080, fillPct: 50, demandSource: "DEMAND_REQUIREMENTS", calendarContextCount: 1, }, }, ], chargeabilityByChapter: [ { chapter: "Delivery", headcount: 1, avgTargetPct: 78, avgActualPct: 70, avgExpectedPct: 76, gapToTargetPct: 8, avgTarget: "78%", avgActual: "70%", avgExpected: "76%", explainability: { month: "2026-03", resourceCount: 1, derivedHeadcount: 1, baseAvailableHours: 176, effectiveAvailableHours: 168, actualBookedHours: 117.6, expectedBookedHours: 127.7, targetBookedHours: 131, publicHolidayHoursDeduction: 8, absenceDayEquivalent: 0, absenceHoursDeduction: 0, unassignedHours: 40.3, }, }, ], projectHealth: [ { project: "Gelddruckmaschine (GDM)", status: "ACTIVE", overall: 55, rating: "at_risk", budget: 58, staffing: 46, timeline: 61, timelineStatus: "DUE_SOON", daysUntilEndDate: 183, demandHeadcountOpen: 2, explainability: { demandHeadcountTotal: 4, demandHeadcountFilled: 2, demandHeadcountOpen: 2, demandRequirementCount: 2, plannedEndDate: "2026-09-30T00:00:00.000Z", budgetUtilizationPercent: 73, remainingBudgetCents: 88_000, calendarContextCount: 1, holidayAwareAssignmentCount: 2, publicHolidayCostDeductionCents: 5_000, absenceCostDeductionCents: 3_000, }, }, ], skillGaps: { totalOpenPositions: 4, roleGaps: [ { role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 }, { role: "Lighting TD", gap: 1, needed: 2, filled: 1, fillRate: 50 }, ], topSkillsInSupply: [ { skill: "houdini", resourceCount: 5 }, { skill: "nuke", resourceCount: 4 }, ], resourcesByRole: [ { role: "Compositor", count: 6 }, { role: "Pipeline TD", count: 2 }, ], }, }); expect(getDashboardPeakTimes).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-06-30T00:00:00.000Z"), granularity: "month", groupBy: "project", }), ); expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ includeProposed: false, topN: 10, watchlistThreshold: 15, }), ); expect(getDashboardSkillGapSummary).toHaveBeenCalledWith(expect.anything()); expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1); }); }); });