diff --git a/packages/api/src/__tests__/assistant-tools-dashboard-detail.test.ts b/packages/api/src/__tests__/assistant-tools-dashboard-detail.test.ts index e5aca4b..edb5d15 100644 --- a/packages/api/src/__tests__/assistant-tools-dashboard-detail.test.ts +++ b/packages/api/src/__tests__/assistant-tools-dashboard-detail.test.ts @@ -9,6 +9,7 @@ import { getDashboardOverview, getDashboardPeakTimes, getDashboardProjectHealth, + getDashboardSkillGapSummary, getDashboardTopValueResources, } 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( { @@ -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 }, + ], + }, }); }); }); diff --git a/packages/api/src/__tests__/dashboard-procedure-support.test.ts b/packages/api/src/__tests__/dashboard-procedure-support.test.ts index 611e54f..2a4c07e 100644 --- a/packages/api/src/__tests__/dashboard-procedure-support.test.ts +++ b/packages/api/src/__tests__/dashboard-procedure-support.test.ts @@ -34,6 +34,7 @@ import { getDashboardOverview, getDashboardPeakTimes, getDashboardProjectHealth, + getDashboardSkillGapSummary, getDashboardTopValueResources, } from "@capakraken/application"; 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 { 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( diff --git a/packages/api/src/__tests__/dashboard-router.test.ts b/packages/api/src/__tests__/dashboard-router.test.ts index 879ed10..87a3884 100644 --- a/packages/api/src/__tests__/dashboard-router.test.ts +++ b/packages/api/src/__tests__/dashboard-router.test.ts @@ -11,6 +11,7 @@ vi.mock("@capakraken/application", async (importOriginal) => { getDashboardTopValueResources: vi.fn(), getDashboardChargeabilityOverview: vi.fn(), getDashboardBudgetForecast: vi.fn(), + getDashboardSkillGapSummary: vi.fn(), getDashboardProjectHealth: vi.fn(), }; }); @@ -33,6 +34,7 @@ import { getDashboardChargeabilityOverview, getDashboardBudgetForecast, getDashboardProjectHealth, + getDashboardSkillGapSummary, } from "@capakraken/application"; import { dashboardRouter } from "../router/dashboard.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([ { 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.anything(), @@ -1066,6 +1098,7 @@ describe("dashboard router", () => { watchlistThreshold: 15, }), ); + expect(getDashboardSkillGapSummary).toHaveBeenCalledWith(expect.anything()); expect(getDashboardProjectHealth).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/api/src/router/assistant-tools/dashboard-insights-reports.ts b/packages/api/src/router/assistant-tools/dashboard-insights-reports.ts index 2c5fa9a..72d86fe 100644 --- a/packages/api/src/router/assistant-tools/dashboard-insights-reports.ts +++ b/packages/api/src/router/assistant-tools/dashboard-insights-reports.ts @@ -54,13 +54,13 @@ export const dashboardInsightsReportsToolDefinitions: ToolDef[] = withToolAccess type: "function", function: { 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: { type: "object", properties: { section: { 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", }, }, }, diff --git a/packages/api/src/router/dashboard-procedure-support.ts b/packages/api/src/router/dashboard-procedure-support.ts index a19d2b6..a7e3ce8 100644 --- a/packages/api/src/router/dashboard-procedure-support.ts +++ b/packages/api/src/router/dashboard-procedure-support.ts @@ -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; }