feat(dashboard): enrich demand calendar locations

This commit is contained in:
2026-03-31 23:12:47 +02:00
parent 79e0fd82f5
commit 92e94f43a7
4 changed files with 255 additions and 2 deletions
@@ -0,0 +1,245 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SystemRole } from "@capakraken/shared";
import {
createToolContext,
executeTool,
getDashboardDemand,
getDashboardOverview,
getDashboardPeakTimes,
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,
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(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,
},
],
},
},
]);
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,
},
],
},
],
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,
},
],
},
],
chargeabilityByChapter: [
{
chapter: "Delivery",
headcount: 4,
avgTarget: "78%",
},
],
});
});
});
@@ -202,7 +202,7 @@ describe("dashboard procedure support", () => {
requiredFTEs: 3, requiredFTEs: 3,
resourceCount: 1, resourceCount: 1,
allocatedHours: 80, allocatedHours: 80,
derivation: { calendarLocations: [{ countryCode: "DE" }] }, derivation: { calendarLocations: [{ countryCode: "DE", countryName: "Germany" }] },
}, },
]); ]);
@@ -250,7 +250,7 @@ describe("dashboard procedure support", () => {
requiredFTEs: 3, requiredFTEs: 3,
allocatedResources: 1, allocatedResources: 1,
allocatedHours: 80, allocatedHours: 80,
calendarLocations: [{ countryCode: "DE" }], calendarLocations: [{ countryCode: "DE", countryName: "Germany" }],
}, },
], ],
chargeabilityByChapter: [ chargeabilityByChapter: [
@@ -16,6 +16,7 @@ export interface GetDashboardDemandInput {
export interface DemandCalendarLocationSummary { export interface DemandCalendarLocationSummary {
countryCode: string | null; countryCode: string | null;
countryName: string | null;
federalState: string | null; federalState: string | null;
metroCityName: string | null; metroCityName: string | null;
resourceCount: number; resourceCount: number;
@@ -71,11 +72,13 @@ function toIsoDate(value: Date): string {
function buildLocationKey(input: { function buildLocationKey(input: {
countryCode: string | null | undefined; countryCode: string | null | undefined;
countryName: string | null | undefined;
federalState: string | null | undefined; federalState: string | null | undefined;
metroCityName: string | null | undefined; metroCityName: string | null | undefined;
}): string { }): string {
return JSON.stringify({ return JSON.stringify({
countryCode: input.countryCode ?? null, countryCode: input.countryCode ?? null,
countryName: input.countryName ?? null,
federalState: input.federalState ?? null, federalState: input.federalState ?? null,
metroCityName: input.metroCityName ?? null, metroCityName: input.metroCityName ?? null,
}); });
@@ -118,6 +121,7 @@ function summarizeCalendarLocations(
id: string; id: string;
availability: WeekdayAvailability; availability: WeekdayAvailability;
countryCode: string | null | undefined; countryCode: string | null | undefined;
countryName: string | null | undefined;
federalState: string | null | undefined; federalState: string | null | undefined;
metroCityName: string | null | undefined; metroCityName: string | null | undefined;
}>, }>,
@@ -145,11 +149,13 @@ function summarizeCalendarLocations(
const locationKey = buildLocationKey({ const locationKey = buildLocationKey({
countryCode: resource.countryCode, countryCode: resource.countryCode,
countryName: resource.countryName,
federalState: resource.federalState, federalState: resource.federalState,
metroCityName: resource.metroCityName, metroCityName: resource.metroCityName,
}); });
const existing = locationMap.get(locationKey) ?? { const existing = locationMap.get(locationKey) ?? {
countryCode: resource.countryCode ?? null, countryCode: resource.countryCode ?? null,
countryName: resource.countryName ?? null,
federalState: resource.federalState ?? null, federalState: resource.federalState ?? null,
metroCityName: resource.metroCityName ?? null, metroCityName: resource.metroCityName ?? null,
resourceCount: 0, resourceCount: 0,
@@ -197,6 +203,7 @@ export async function getDashboardDemand(
availability: resource.availability as unknown as WeekdayAvailability, availability: resource.availability as unknown as WeekdayAvailability,
countryId: resource.countryId, countryId: resource.countryId,
countryCode: resource.country?.code, countryCode: resource.country?.code,
countryName: resource.country?.name,
federalState: resource.federalState, federalState: resource.federalState,
metroCityId: resource.metroCityId, metroCityId: resource.metroCityId,
metroCityName: resource.metroCity?.name, metroCityName: resource.metroCity?.name,
@@ -25,6 +25,7 @@ export const DASHBOARD_PLANNING_ALLOCATION_INCLUDE = {
country: { country: {
select: { select: {
code: true, code: true,
name: true,
}, },
}, },
metroCity: { metroCity: {