import { PermissionKey, SystemRole } from "@capakraken/shared"; import type { TRPCContext } from "../../trpc.js"; import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; type RateLookupCandidate = { rateCardName: string; clientId: string | null; clientName: string | null; lineId: string; chapter: string | null; seniority: string | null; roleName: string | null; costRateCents: number; billRateCents: number | null; score: number; }; type ScenarioSimulationResult = { baseline: { totalCostCents: number; totalHours: number; headcount: number; skillCount: number; }; scenario: { totalCostCents: number; totalHours: number; headcount: number; skillCount: number; }; delta: { costCents: number; hours: number; headcount: number; skillCoveragePct: number; }; resourceImpacts: unknown[]; warnings: string[]; budgetCents: number; }; type ScenarioRateAnalysisDeps = { assertPermission: (ctx: ToolContext, perm: PermissionKey) => void; createRateCardCaller: (ctx: TRPCContext) => { lookupBestMatch: (params: { clientId?: string; chapter?: string; managementLevelId?: string; roleName?: string; seniority?: string; }) => Promise<{ message?: string; bestMatch: RateLookupCandidate | null; alternatives: RateLookupCandidate[]; totalCandidates: number; }>; }; createScenarioCaller: (ctx: TRPCContext) => { simulate: (params: { projectId: string; changes: Array<{ assignmentId?: string; resourceId?: string; roleId?: string; startDate: Date; endDate: Date; hoursPerDay: number; remove?: boolean; }>; }) => Promise; }; createInsightsCaller: (ctx: TRPCContext) => { generateProjectNarrative: (params: { projectId: string }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; fmtEur: (value: number) => string; }; export const scenarioRateAnalysisToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "lookup_rate", description: "Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).", parameters: { type: "object", properties: { clientId: { type: "string", description: "Client ID to find rate card for" }, chapter: { type: "string", description: "Chapter to match" }, managementLevelId: { type: "string", description: "Management level ID to match" }, roleName: { type: "string", description: "Role name to match" }, seniority: { type: "string", description: "Seniority level to match" }, }, }, }, }, { type: "function", function: { name: "simulate_scenario", description: "Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.", parameters: { type: "object", properties: { projectId: { type: "string", description: "Project ID" }, changes: { type: "array", items: { type: "object", properties: { assignmentId: { type: "string", description: "Existing assignment ID to modify (omit for new)" }, resourceId: { type: "string", description: "Resource ID" }, roleId: { type: "string", description: "Role ID" }, startDate: { type: "string", description: "Start date YYYY-MM-DD" }, endDate: { type: "string", description: "End date YYYY-MM-DD" }, hoursPerDay: { type: "number", description: "Hours per day" }, remove: { type: "boolean", description: "Set true to remove an existing assignment" }, }, required: ["startDate", "endDate", "hoursPerDay"], }, description: "Array of staffing changes to simulate", }, }, required: ["projectId", "changes"], }, }, }, { type: "function", function: { name: "generate_project_narrative", description: "Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.", parameters: { type: "object", properties: { projectId: { type: "string", description: "Project ID" }, }, required: ["projectId"], }, }, }, ], { lookup_rate: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, simulate_scenario: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, generate_project_narrative: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, }); export function createScenarioRateAnalysisExecutors( deps: ScenarioRateAnalysisDeps, ): Record { return { async lookup_rate( params: { clientId?: string; chapter?: string; managementLevelId?: string; roleName?: string; seniority?: string; }, ctx: ToolContext, ) { deps.assertPermission(ctx, PermissionKey.VIEW_COSTS); const caller = deps.createRateCardCaller(deps.createScopedCallerContext(ctx)); const result = await caller.lookupBestMatch(params); return { ...(result.message ? { message: result.message } : {}), bestMatch: result.bestMatch ? { ...result.bestMatch, costRate: deps.fmtEur(result.bestMatch.costRateCents), billRate: result.bestMatch.billRateCents ? deps.fmtEur(result.bestMatch.billRateCents) : null, } : null, alternatives: result.alternatives.map((alternative) => ({ ...alternative, costRate: deps.fmtEur(alternative.costRateCents), billRate: alternative.billRateCents ? deps.fmtEur(alternative.billRateCents) : null, })), totalCandidates: result.totalCandidates, }; }, async simulate_scenario( params: { projectId: string; changes: Array<{ assignmentId?: string; resourceId?: string; roleId?: string; startDate: string; endDate: string; hoursPerDay: number; remove?: boolean; }>; }, ctx: ToolContext, ) { const caller = deps.createScenarioCaller(deps.createScopedCallerContext(ctx)); const result = await caller.simulate({ projectId: params.projectId, changes: params.changes.map((change) => ({ ...change, startDate: new Date(change.startDate), endDate: new Date(change.endDate), })), }); return { baseline: { ...result.baseline, totalCost: deps.fmtEur(result.baseline.totalCostCents), }, scenario: { ...result.scenario, totalCost: deps.fmtEur(result.scenario.totalCostCents), }, delta: { ...result.delta, cost: deps.fmtEur(result.delta.costCents), }, resourceImpacts: result.resourceImpacts, warnings: result.warnings, budgetCents: result.budgetCents, }; }, async generate_project_narrative( params: { projectId: string }, ctx: ToolContext, ) { const caller = deps.createInsightsCaller(deps.createScopedCallerContext(ctx)); return caller.generateProjectNarrative({ projectId: params.projectId }); }, }; }