feat(api): include skill gaps in dashboard detail

This commit is contained in:
2026-03-31 23:46:07 +02:00
parent 2de5a0eede
commit f2d511ebc8
5 changed files with 115 additions and 2 deletions
@@ -9,6 +9,7 @@ import {
getDashboardOverview, getDashboardOverview,
getDashboardPeakTimes, getDashboardPeakTimes,
getDashboardProjectHealth, getDashboardProjectHealth,
getDashboardSkillGapSummary,
getDashboardTopValueResources, getDashboardTopValueResources,
} from "./assistant-tools-dashboard-test-helpers.js"; } from "./assistant-tools-dashboard-test-helpers.js";
@@ -209,6 +210,21 @@ describe("assistant dashboard tools detail aggregation", () => {
}, },
}, },
]); ]);
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( const ctx = createToolContext(
{ {
@@ -384,6 +400,21 @@ describe("assistant dashboard tools detail aggregation", () => {
}, },
}, },
], ],
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 },
],
},
}); });
}); });
}); });
@@ -34,6 +34,7 @@ import {
getDashboardOverview, getDashboardOverview,
getDashboardPeakTimes, getDashboardPeakTimes,
getDashboardProjectHealth, getDashboardProjectHealth,
getDashboardSkillGapSummary,
getDashboardTopValueResources, getDashboardTopValueResources,
} from "@capakraken/application"; } from "@capakraken/application";
import { cacheGet } from "../lib/cache.js"; import { cacheGet } from "../lib/cache.js";
@@ -293,6 +294,21 @@ describe("dashboard procedure support", () => {
}, },
}, },
]); ]);
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 },
],
});
try { try {
const result = await getDashboardDetail(createContext(), { section: "all" }); const result = await getDashboardDetail(createContext(), { section: "all" });
@@ -417,6 +433,21 @@ describe("dashboard procedure support", () => {
}, },
}, },
], ],
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 },
],
},
}); });
expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith( expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith(
@@ -11,6 +11,7 @@ vi.mock("@capakraken/application", async (importOriginal) => {
getDashboardTopValueResources: vi.fn(), getDashboardTopValueResources: vi.fn(),
getDashboardChargeabilityOverview: vi.fn(), getDashboardChargeabilityOverview: vi.fn(),
getDashboardBudgetForecast: vi.fn(), getDashboardBudgetForecast: vi.fn(),
getDashboardSkillGapSummary: vi.fn(),
getDashboardProjectHealth: vi.fn(), getDashboardProjectHealth: vi.fn(),
}; };
}); });
@@ -33,6 +34,7 @@ import {
getDashboardChargeabilityOverview, getDashboardChargeabilityOverview,
getDashboardBudgetForecast, getDashboardBudgetForecast,
getDashboardProjectHealth, getDashboardProjectHealth,
getDashboardSkillGapSummary,
} from "@capakraken/application"; } from "@capakraken/application";
import { dashboardRouter } from "../router/dashboard.js"; import { dashboardRouter } from "../router/dashboard.js";
import { createCallerFactory } from "../trpc.js"; import { createCallerFactory } from "../trpc.js";
@@ -786,6 +788,21 @@ describe("dashboard router", () => {
}, },
}, },
]); ]);
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 },
],
});
vi.mocked(getDashboardTopValueResources).mockResolvedValue([ vi.mocked(getDashboardTopValueResources).mockResolvedValue([
{ {
id: "res_1", id: "res_1",
@@ -1048,6 +1065,21 @@ describe("dashboard router", () => {
}, },
}, },
], ],
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 },
],
},
}); });
expect(getDashboardPeakTimes).toHaveBeenCalledWith( expect(getDashboardPeakTimes).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
@@ -1066,6 +1098,7 @@ describe("dashboard router", () => {
watchlistThreshold: 15, watchlistThreshold: 15,
}), }),
); );
expect(getDashboardSkillGapSummary).toHaveBeenCalledWith(expect.anything());
expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1); expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1);
}); });
}); });
@@ -54,13 +54,13 @@ export const dashboardInsightsReportsToolDefinitions: ToolDef[] = withToolAccess
type: "function", type: "function",
function: { function: {
name: "get_dashboard_detail", name: "get_dashboard_detail",
description: "Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview, and project health risks.", description: "Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview, project health risks, and skill gap staffing pressure.",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
section: { section: {
type: "string", type: "string",
description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, project_health, or all", description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, project_health, skill_gaps, or all",
}, },
}, },
}, },
@@ -477,6 +477,24 @@ export async function getDashboardDetail(ctx: DashboardProcedureContext, input:
})); }));
} }
if (section === "all" || section === "skill_gaps") {
const skillGapSummary = await getDashboardSkillGapSummaryRead(ctx);
result.skillGaps = {
totalOpenPositions: skillGapSummary.totalOpenPositions,
roleGaps: skillGapSummary.roleGaps
.slice(0, 10)
.map((gap) => ({
role: gap.role,
gap: gap.gap,
needed: gap.needed,
filled: gap.filled,
fillRate: gap.fillRate,
})),
topSkillsInSupply: skillGapSummary.skillSupplyTop10.slice(0, 5),
resourcesByRole: skillGapSummary.resourcesByRole.slice(0, 5),
};
}
return result; return result;
} }