refactor(api): extract dashboard procedures
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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 },
|
||||
]);
|
||||
vi.mocked(getDashboardTopValueResources).mockResolvedValue([
|
||||
{
|
||||
id: "res_1",
|
||||
eid: "E-001",
|
||||
displayName: "Ada Lovelace",
|
||||
chapter: "CGI",
|
||||
lcrCents: 12345,
|
||||
valueScore: 98,
|
||||
countryCode: "DE",
|
||||
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,
|
||||
},
|
||||
],
|
||||
topResources: [
|
||||
{
|
||||
name: "Ada Lovelace",
|
||||
eid: "E-001",
|
||||
chapter: "CGI",
|
||||
lcr: "123,45 EUR",
|
||||
valueScore: 98,
|
||||
countryCode: "DE",
|
||||
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 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -348,8 +348,28 @@ describe("dashboard router", () => {
|
||||
describe("getTopValueResources", () => {
|
||||
it("returns sorted resources with default limit", async () => {
|
||||
const resources = [
|
||||
{ id: "res_1", displayName: "Alice", valueScore: 95 },
|
||||
{ id: "res_2", displayName: "Bob", valueScore: 88 },
|
||||
{
|
||||
id: "res_1",
|
||||
eid: "alice",
|
||||
displayName: "Alice",
|
||||
chapter: "Delivery",
|
||||
valueScore: 95,
|
||||
lcrCents: 12_300,
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Munich",
|
||||
},
|
||||
{
|
||||
id: "res_2",
|
||||
eid: "bob",
|
||||
displayName: "Bob",
|
||||
chapter: "Data",
|
||||
valueScore: 88,
|
||||
lcrCents: 10_800,
|
||||
countryCode: "US",
|
||||
federalState: "CA",
|
||||
metroCityName: "San Francisco",
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(getDashboardTopValueResources).mockResolvedValue(resources);
|
||||
@@ -357,7 +377,7 @@ describe("dashboard router", () => {
|
||||
const caller = createControllerCaller({});
|
||||
const result = await caller.getTopValueResources({ limit: 10 });
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(resources);
|
||||
expect(getDashboardTopValueResources).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ limit: 10 }),
|
||||
@@ -576,6 +596,9 @@ describe("dashboard router", () => {
|
||||
chapter: "Delivery",
|
||||
valueScore: 91,
|
||||
lcrCents: 9_500,
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Augsburg",
|
||||
},
|
||||
]);
|
||||
vi.mocked(getDashboardDemand).mockResolvedValue([
|
||||
@@ -620,6 +643,9 @@ describe("dashboard router", () => {
|
||||
chapter: "Delivery",
|
||||
lcr: "95,00 EUR",
|
||||
valueScore: 91,
|
||||
countryCode: "DE",
|
||||
federalState: "BY",
|
||||
metroCityName: "Augsburg",
|
||||
},
|
||||
],
|
||||
demandPipeline: [
|
||||
|
||||
Reference in New Issue
Block a user