285 lines
8.0 KiB
TypeScript
285 lines
8.0 KiB
TypeScript
import { SystemRole } from "@capakraken/shared";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
|
return {
|
|
...actual,
|
|
getDashboardOverview: vi.fn(),
|
|
getDashboardPeakTimes: vi.fn(),
|
|
getDashboardDemand: vi.fn(),
|
|
getDashboardTopValueResources: vi.fn(),
|
|
getDashboardChargeabilityOverview: vi.fn(),
|
|
getDashboardBudgetForecast: vi.fn(),
|
|
getDashboardSkillGaps: 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.map((resource) => ({ ...resource, anonymized: true }))),
|
|
getAnonymizationDirectory: vi.fn().mockResolvedValue({}),
|
|
}));
|
|
|
|
import {
|
|
getDashboardChargeabilityOverview,
|
|
getDashboardDemand,
|
|
getDashboardOverview,
|
|
getDashboardPeakTimes,
|
|
getDashboardTopValueResources,
|
|
} from "@capakraken/application";
|
|
import { anonymizeResources } from "../lib/anonymization.js";
|
|
import {
|
|
getDashboardChargeabilityOverviewRead,
|
|
getDashboardDetail,
|
|
getDashboardStatisticsDetail,
|
|
} from "../router/dashboard-procedure-support.js";
|
|
|
|
function createContext() {
|
|
return {
|
|
db: {} as never,
|
|
session: {
|
|
user: { email: "controller@example.com", name: "Controller", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: SystemRole.CONTROLLER,
|
|
permissionOverrides: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("dashboard procedure support", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("derives statistics detail from the canonical 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 },
|
|
],
|
|
recentActivity: [],
|
|
});
|
|
|
|
const result = await getDashboardStatisticsDetail(createContext());
|
|
|
|
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 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("builds the assistant-facing dashboard detail payload", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-03-15T00:00:00.000Z"));
|
|
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: "2026-03-01T00:00:00.000Z",
|
|
windowEnd: "2026-06-30T00:00:00.000Z",
|
|
},
|
|
projectsByStatus: [],
|
|
chapterUtilization: [
|
|
{ chapter: "CGI", resourceCount: 5, avgChargeabilityTarget: 78 },
|
|
],
|
|
recentActivity: [],
|
|
});
|
|
vi.mocked(getDashboardPeakTimes).mockResolvedValue([
|
|
{
|
|
period: "2026-03",
|
|
totalHours: 160.34,
|
|
capacityHours: 200.12,
|
|
utilizationPct: 80,
|
|
derivation: {
|
|
calendarContextCount: 1,
|
|
calendarLocations: [{ countryCode: "DE", federalState: "BY", metroCityName: "Munich" }],
|
|
},
|
|
},
|
|
]);
|
|
vi.mocked(getDashboardTopValueResources).mockResolvedValue([
|
|
{
|
|
id: "res_1",
|
|
eid: "E-001",
|
|
displayName: "Ada Lovelace",
|
|
chapter: "CGI",
|
|
lcrCents: 12345,
|
|
valueScore: 98,
|
|
valueScoreBreakdown: {
|
|
skillDepth: 94,
|
|
skillBreadth: 86,
|
|
costEfficiency: 82,
|
|
chargeability: 88,
|
|
experience: 96,
|
|
total: 98,
|
|
},
|
|
valueScoreUpdatedAt: new Date("2026-03-05T00:00:00.000Z"),
|
|
countryCode: "DE",
|
|
countryName: "Germany",
|
|
federalState: "BY",
|
|
metroCityName: "Munich",
|
|
},
|
|
]);
|
|
vi.mocked(getDashboardDemand).mockResolvedValue([
|
|
{
|
|
name: "Apollo",
|
|
shortCode: "APO",
|
|
requiredFTEs: 3,
|
|
resourceCount: 1,
|
|
allocatedHours: 80,
|
|
derivation: { calendarLocations: [{ countryCode: "DE" }] },
|
|
},
|
|
]);
|
|
|
|
try {
|
|
const result = await getDashboardDetail(createContext(), { section: "all" });
|
|
|
|
expect(result).toEqual({
|
|
peakTimes: [
|
|
{
|
|
month: "2026-03",
|
|
totalHours: 160.3,
|
|
totalHoursPerDay: 160.3,
|
|
capacityHours: 200.1,
|
|
utilizationPct: 80,
|
|
calendarContextCount: 1,
|
|
calendarLocations: [{ countryCode: "DE", federalState: "BY", metroCityName: "Munich" }],
|
|
},
|
|
],
|
|
topResources: [
|
|
{
|
|
name: "Ada Lovelace",
|
|
eid: "E-001",
|
|
chapter: "CGI",
|
|
lcr: "123,45 EUR",
|
|
valueScore: 98,
|
|
valueScoreBreakdown: {
|
|
skillDepth: 94,
|
|
skillBreadth: 86,
|
|
costEfficiency: 82,
|
|
chargeability: 88,
|
|
experience: 96,
|
|
total: 98,
|
|
},
|
|
valueScoreUpdatedAt: "2026-03-05T00:00:00.000Z",
|
|
countryCode: "DE",
|
|
countryName: "Germany",
|
|
federalState: "BY",
|
|
metroCityName: "Munich",
|
|
},
|
|
],
|
|
demandPipeline: [
|
|
{
|
|
project: "Apollo (APO)",
|
|
needed: 2,
|
|
requiredFTEs: 3,
|
|
allocatedResources: 1,
|
|
allocatedHours: 80,
|
|
calendarLocations: [{ countryCode: "DE" }],
|
|
},
|
|
],
|
|
chargeabilityByChapter: [
|
|
{
|
|
chapter: "CGI",
|
|
headcount: 5,
|
|
avgTarget: "78%",
|
|
},
|
|
],
|
|
});
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("anonymizes chargeability overview payloads before returning them", async () => {
|
|
vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({
|
|
avgChargeability: 72,
|
|
top: [{ id: "res_1" }],
|
|
watchlist: [{ id: "res_2" }],
|
|
});
|
|
|
|
const result = await getDashboardChargeabilityOverviewRead(createContext(), {
|
|
includeProposed: false,
|
|
topN: 10,
|
|
watchlistThreshold: 15,
|
|
});
|
|
|
|
expect(anonymizeResources).toHaveBeenCalledTimes(2);
|
|
expect(result).toEqual({
|
|
avgChargeability: 72,
|
|
top: [{ id: "res_1", anonymized: true }],
|
|
watchlist: [{ id: "res_2", anonymized: true }],
|
|
});
|
|
});
|
|
});
|