import { beforeEach, describe, expect, it, vi } from "vitest"; import { SystemRole } from "@capakraken/shared"; import { createToolContext, executeTool, getDashboardChargeabilityOverview, getDashboardDemand, getDashboardOverview, getDashboardPeakTimes, getDashboardProjectHealth, getDashboardSkillGapSummary, getDashboardTopValueResources, } from "./assistant-tools-dashboard-test-helpers.js"; describe("assistant dashboard tools detail aggregation", () => { beforeEach(() => { vi.clearAllMocks(); }); it("routes dashboard detail reads through dashboard router callers", async () => { vi.mocked(getDashboardOverview).mockResolvedValue({ totalResources: 12, activeResources: 10, inactiveResources: 2, totalProjects: 4, activeProjects: 3, inactiveProjects: 1, totalAllocations: 8, activeAllocations: 7, cancelledAllocations: 1, approvedVacations: 2, totalEstimates: 5, budgetSummary: { totalBudgetCents: 500_000, totalCostCents: 240_000, avgUtilizationPercent: 48, }, budgetBasis: { remainingBudgetCents: 260_000, budgetedProjects: 3, unbudgetedProjects: 1, trackedAssignmentCount: 8, windowStart: new Date("2026-01-01T00:00:00.000Z"), windowEnd: new Date("2026-06-30T00:00:00.000Z"), }, recentActivity: [], projectsByStatus: [], chapterUtilization: [ { chapter: "Delivery", resourceCount: 4, avgChargeabilityTarget: 78, }, ], }); vi.mocked(getDashboardPeakTimes).mockResolvedValue([ { period: "2026-03", groups: [], 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, 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, effectiveAvailableHours: 400.2, publicHolidayHoursDeduction: 16, absenceDayEquivalent: 1.5, absenceHoursDeduction: 12.5, }, }, ]); 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, }, }, ]); 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 }, ], }); const ctx = createToolContext( { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }, { userRole: SystemRole.CONTROLLER }, ); const result = await executeTool("get_dashboard_detail", JSON.stringify({ section: "all" }), ctx); expect(getDashboardOverview).toHaveBeenCalledTimes(1); expect(getDashboardPeakTimes).toHaveBeenCalledWith( ctx.db, 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(getDashboardTopValueResources).toHaveBeenCalledWith( ctx.db, expect.objectContaining({ limit: 10, userRole: SystemRole.CONTROLLER, }), ); expect(getDashboardDemand).toHaveBeenCalledWith( ctx.db, expect.objectContaining({ startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-06-30T00:00:00.000Z"), groupBy: "project", }), ); expect(JSON.parse(result.content)).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 }, ], }, }); }); it("routes the isolated skill gap detail section without unrelated dashboard reads", async () => { vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({ roleGaps: [ { role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 }, ], totalOpenPositions: 3, skillSupplyTop10: [{ skill: "houdini", resourceCount: 5 }], resourcesByRole: [{ role: "Pipeline TD", count: 2 }], }); const ctx = createToolContext( { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }, { userRole: SystemRole.CONTROLLER }, ); const result = await executeTool( "get_dashboard_detail", JSON.stringify({ section: "skill_gaps" }), ctx, ); expect(JSON.parse(result.content)).toEqual({ skillGaps: { totalOpenPositions: 3, roleGaps: [ { role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 }, ], topSkillsInSupply: [{ skill: "houdini", resourceCount: 5 }], resourcesByRole: [{ role: "Pipeline TD", count: 2 }], }, }); expect(getDashboardSkillGapSummary).toHaveBeenCalledTimes(1); expect(getDashboardOverview).not.toHaveBeenCalled(); expect(getDashboardPeakTimes).not.toHaveBeenCalled(); expect(getDashboardDemand).not.toHaveBeenCalled(); expect(getDashboardProjectHealth).not.toHaveBeenCalled(); expect(getDashboardChargeabilityOverview).not.toHaveBeenCalled(); expect(getDashboardTopValueResources).not.toHaveBeenCalled(); }); });