import type { TRPCContext } from "../../trpc.js"; import { CreateRoleSchema, UpdateRoleSchema } from "@capakraken/shared"; import { z } from "zod"; import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js"; type AssistantToolErrorResult = { error: string }; type ResolvedResource = { id: string; }; type RolesAnalyticsDeps = { createRoleCaller: (ctx: TRPCContext) => { list: (params: Record) => Promise>; create: (params: z.input) => Promise<{ id: string; name: string; }>; update: (params: { id: string; data: z.input; }) => Promise<{ id: string; name: string; }>; getById: (params: { id: string }) => Promise<{ name: string }>; delete: (params: { id: string }) => Promise; }; createResourceCaller: (ctx: TRPCContext) => { searchBySkills: (params: { rules: Array<{ skill: string; minProficiency: number }>; operator: "OR"; }) => Promise; }>>; getChargeabilitySummary: (params: { resourceId: string; month: string; }) => Promise; }; createDashboardCaller: (ctx: TRPCContext) => { getStatisticsDetail: () => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; resolveResourceIdentifier: ( ctx: ToolContext, identifier: string, ) => Promise; toAssistantRoleMutationError: ( error: unknown, action: "create" | "update" | "delete", ) => AssistantToolErrorResult | null; }; export const rolesAnalyticsReadToolDefinitions: ToolDef[] = [ { type: "function", function: { name: "list_roles", description: "List all available roles with their colors.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "search_by_skill", description: "Find resources that have a specific skill. Controller/manager/admin access required.", parameters: { type: "object", properties: { skill: { type: "string", description: "Skill name to search for" }, }, required: ["skill"], }, }, }, { type: "function", function: { name: "get_statistics", description: "Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.", parameters: { type: "object", properties: {} }, }, }, { type: "function", function: { name: "get_chargeability", description: "Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.", parameters: { type: "object", properties: { resourceId: { type: "string", description: "Resource ID, eid, or name" }, month: { type: "string", description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month" }, }, required: ["resourceId"], }, }, }, ]; export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = [ { type: "function", function: { name: "create_role", description: "Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.", parameters: { type: "object", properties: { name: { type: "string", description: "Role name" }, description: { type: "string", description: "Optional role description" }, color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" }, }, required: ["name"], }, }, }, { type: "function", function: { name: "update_role", description: "Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.", parameters: { type: "object", properties: { id: { type: "string", description: "Role ID" }, name: { type: "string", description: "New name" }, description: { type: "string", description: "New description" }, color: { type: "string", description: "New hex color" }, isActive: { type: "boolean", description: "Set active state" }, }, required: ["id"], }, }, }, { type: "function", function: { name: "delete_role", description: "Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.", parameters: { type: "object", properties: { id: { type: "string", description: "Role ID" }, }, required: ["id"], }, }, }, ]; export function createRolesAnalyticsExecutors( deps: RolesAnalyticsDeps, ): Record { return { async list_roles(_params: Record, ctx: ToolContext) { const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx)); const roles = await caller.list({}); return roles.map((role) => ({ id: role.id, name: role.name, color: role.color ?? null, })); }, async search_by_skill(params: { skill: string }, ctx: ToolContext) { const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); const matched = await caller.searchBySkills({ rules: [{ skill: params.skill, minProficiency: 1 }], operator: "OR", }); return matched.slice(0, 20).map((resource) => ({ id: resource.id, eid: resource.eid, name: resource.displayName, matchedSkill: resource.matchedSkills[0]?.skill ?? null, level: resource.matchedSkills[0]?.proficiency ?? null, chapter: resource.chapter ?? null, })); }, async get_statistics(_params: Record, ctx: ToolContext) { const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx)); return caller.getStatisticsDetail(); }, async get_chargeability(params: { resourceId: string; month?: string }, ctx: ToolContext) { const now = new Date(); const month = params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId); if ("error" in resource) { return resource; } const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); return caller.getChargeabilitySummary({ resourceId: resource.id, month, }); }, async create_role(params: { name: string; description?: string; color?: string; }, ctx: ToolContext) { const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx)); let role; try { role = await caller.create(CreateRoleSchema.parse(params)); } catch (error) { const mapped = deps.toAssistantRoleMutationError(error, "create"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate" as const, scope: ["role"], success: true, message: `Created role: ${role.name}`, roleId: role.id, role, }; }, async update_role(params: { id: string; name?: string; description?: string; color?: string; isActive?: boolean; }, ctx: ToolContext) { const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx)); const data = UpdateRoleSchema.parse({ ...(params.name !== undefined ? { name: params.name } : {}), ...(params.description !== undefined ? { description: params.description } : {}), ...(params.color !== undefined ? { color: params.color } : {}), ...(params.isActive !== undefined ? { isActive: params.isActive } : {}), }); if (Object.keys(data).length === 0) { return { error: "No fields to update" }; } let role; try { role = await caller.update({ id: params.id, data }); } catch (error) { const mapped = deps.toAssistantRoleMutationError(error, "update"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate" as const, scope: ["role"], success: true, message: `Updated role: ${role.name}`, roleId: role.id, role, }; }, async delete_role(params: { id: string }, ctx: ToolContext) { const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx)); let role; try { role = await caller.getById({ id: params.id }); await caller.delete({ id: params.id }); } catch (error) { const mapped = deps.toAssistantRoleMutationError(error, "delete"); if (mapped) { return mapped; } throw error; } return { __action: "invalidate" as const, scope: ["role"], success: true, message: `Deleted role: ${role.name}`, }; }, }; }