feat(dashboard): enrich demand calendar locations
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user