import { PermissionKey, SystemRole } from "@capakraken/shared"; import type { TRPCContext } from "../../trpc.js"; import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; type ReportEntity = "resource" | "project" | "assignment" | "resource_month"; type ReportFilterOperator = "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains" | "in"; type ReportFilter = { field: string; op: ReportFilterOperator; value: string; }; type ReportQueryResult = { rows: unknown[]; totalCount: number; columns: unknown; groups: unknown; }; type DashboardInsightsReportsDeps = { assertPermission: (ctx: ToolContext, perm: PermissionKey) => void; createDashboardCaller: (ctx: TRPCContext) => { getDetail: (params: { section?: string }) => Promise; getSkillGapSummary: () => Promise; getProjectHealthDetail: () => Promise; getBudgetForecastDetail: () => Promise; }; createInsightsCaller: (ctx: TRPCContext) => { getAnomalyDetail: () => Promise; getInsightsSummary: () => Promise; }; createReportCaller: (ctx: TRPCContext) => { getReportData: (params: { entity: ReportEntity; columns: string[]; filters: ReportFilter[]; periodMonth?: string; groupBy?: string; sortBy?: string; sortDir: "asc" | "desc"; limit: number; offset: number; }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; }; const REPORT_ENTITIES: ReportEntity[] = ["resource", "project", "assignment", "resource_month"]; 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.", parameters: { type: "object", properties: { section: { type: "string", description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, or all", }, }, }, }, }, { type: "function", function: { name: "detect_anomalies", description: "Detect anomalies across all active projects: budget burn rate issues, staffing gaps, utilization outliers, and timeline overruns.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "get_skill_gaps", description: "Analyze skill supply vs demand across all active projects. Returns which skills are in short supply relative to demand requirements.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "get_project_health", description: "Get health scores for all active projects based on budget utilization, staffing completeness, and timeline status.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "get_budget_forecast", description: "Get budget utilization and burn rate per active project. Shows total budget, spent, remaining, and whether burn is ahead or behind schedule.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "get_insights_summary", description: "Get a summary of anomaly counts by category (budget, staffing, timeline, utilization) plus critical count.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "run_report", description: "Run a dynamic report query on resources, projects, assignments, or resource-month rows with flexible column selection and filtering.", parameters: { type: "object", properties: { entity: { type: "string", enum: ["resource", "project", "assignment", "resource_month"], description: "Entity type to query", }, columns: { type: "array", items: { type: "string" }, description: "Column keys to include (e.g. 'displayName', 'chapter', 'country.name')", }, filters: { type: "array", items: { type: "object", properties: { field: { type: "string", description: "Field to filter on" }, op: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"], description: "Filter operator" }, value: { type: "string", description: "Filter value (string)" }, }, required: ["field", "op", "value"], }, description: "Filters to apply", }, periodMonth: { type: "string", description: "Required for resource_month reports. Format: YYYY-MM", }, groupBy: { type: "string", description: "Optional scalar field used to group result rows into labeled sections.", }, sortBy: { type: "string", description: "Optional scalar field used to sort rows within the grouped result.", }, sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction for sortBy. Default: asc", }, limit: { type: "integer", description: "Max results. Default: 50" }, }, required: ["entity", "columns"], }, }, }, ], { get_dashboard_detail: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, detect_anomalies: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, get_skill_gaps: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, get_project_health: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, get_budget_forecast: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, get_insights_summary: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, run_report: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, }); export function createDashboardInsightsReportsExecutors( deps: DashboardInsightsReportsDeps, ): Record { return { async get_dashboard_detail(params: { section?: string }, ctx: ToolContext) { const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx)); return caller.getDetail({ ...(params.section ? { section: params.section } : {}) }); }, async detect_anomalies(_params: Record, ctx: ToolContext) { const caller = deps.createInsightsCaller(deps.createScopedCallerContext(ctx)); return caller.getAnomalyDetail(); }, async get_skill_gaps(_params: Record, ctx: ToolContext) { const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx)); return caller.getSkillGapSummary(); }, async get_project_health(_params: Record, ctx: ToolContext) { const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx)); return caller.getProjectHealthDetail(); }, async get_budget_forecast(_params: Record, ctx: ToolContext) { deps.assertPermission(ctx, PermissionKey.VIEW_COSTS); const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx)); return caller.getBudgetForecastDetail(); }, async get_insights_summary(_params: Record, ctx: ToolContext) { const caller = deps.createInsightsCaller(deps.createScopedCallerContext(ctx)); return caller.getInsightsSummary(); }, async run_report(params: { entity: string; columns: string[]; filters?: ReportFilter[]; periodMonth?: string; groupBy?: string; sortBy?: string; sortDir?: "asc" | "desc"; limit?: number; }, ctx: ToolContext) { const entity = params.entity as ReportEntity; if (!REPORT_ENTITIES.includes(entity)) { return { error: `Unknown entity: ${params.entity}. Use resource, project, assignment, or resource_month.`, }; } const caller = deps.createReportCaller(deps.createScopedCallerContext(ctx)); const result = await caller.getReportData({ entity, columns: params.columns, filters: params.filters ?? [], ...(params.periodMonth !== undefined ? { periodMonth: params.periodMonth } : {}), ...(params.groupBy !== undefined ? { groupBy: params.groupBy } : {}), ...(params.sortBy !== undefined ? { sortBy: params.sortBy } : {}), sortDir: params.sortDir ?? "asc", limit: Math.min(params.limit ?? 50, 200), offset: 0, }); return { rows: result.rows, rowCount: result.rows.length, totalCount: result.totalCount, columns: result.columns, groups: result.groups, }; }, }; }