import { CreateResourceSchema, PermissionKey, UpdateResourceSchema } from "@capakraken/shared"; import type { TRPCContext } from "../../trpc.js"; import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; type AssistantToolErrorResult = { error: string }; type ResolvedResource = { id: string; eid: string; displayName: string; }; type ResolvedReference = { id: string; }; type ResourceRecord = { id: string; eid: string; displayName: string; }; type ParsedCreateResourceInput = ReturnType; type ParsedUpdateResourceInput = ReturnType; type ResourceToolsDeps = { assertPermission: (ctx: ToolContext, perm: PermissionKey) => void; createResourceCaller: (ctx: TRPCContext) => { listSummariesDetail: (params: { search?: string; country?: string; metroCity?: string; orgUnit?: string; roleName?: string; isActive?: boolean; limit: number; }) => Promise; getByIdentifierDetail: (params: { identifier: string }) => Promise; create: (params: ParsedCreateResourceInput) => Promise; update: (params: { id: string; data: ParsedUpdateResourceInput; }) => Promise; deactivate: (params: { id: string }) => Promise; }; createRoleCaller: (ctx: TRPCContext) => { resolveByIdentifier: (params: { identifier: string }) => Promise; }; createCountryCaller: (ctx: TRPCContext) => { resolveByIdentifier: (params: { identifier: string }) => Promise; }; createOrgUnitCaller: (ctx: TRPCContext) => { resolveByIdentifier: (params: { identifier: string }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; resolveResourceIdentifier: ( ctx: ToolContext, identifier: string, ) => Promise; resolveEntityOrAssistantError: ( resolve: () => Promise, notFoundMessage: string, ) => Promise; toAssistantResourceMutationError: ( error: unknown, ) => AssistantToolErrorResult | null; toAssistantResourceCreationError: ( error: unknown, ) => AssistantToolErrorResult | null; }; export const resourceReadToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "search_resources", description: "Search for resources (employees) by name, employee ID, chapter, country, metro city, org unit, or role. Resource overview access required. Returns a list of matching resources with key details.", parameters: { type: "object", properties: { query: { type: "string", description: "Search term (matches displayName, eid, chapter)" }, country: { type: "string", description: "Filter by country name or code (e.g. 'Spain', 'ES', 'Deutschland', 'DE')" }, metroCity: { type: "string", description: "Filter by metro city name (e.g. 'Madrid', 'München')" }, orgUnit: { type: "string", description: "Filter by org unit name (partial match)" }, roleName: { type: "string", description: "Filter by role name (partial match)" }, isActive: { type: "boolean", description: "Filter by active status. Default: true" }, limit: { type: "integer", description: "Max results. Default: 50" }, }, }, }, }, { type: "function", function: { name: "get_resource", description: "Get detailed information about a single resource by ID, employee ID (eid), or name.", parameters: { type: "object", properties: { identifier: { type: "string", description: "Resource ID, employee ID (eid like EMP-001), or display name" }, }, required: ["identifier"], }, }, }, ], { search_resources: { requiresResourceOverview: true, }, }); export const resourceMutationToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "update_resource", description: "Update a resource's details. Requires manageResources permission. Always confirm with the user before calling this.", parameters: { type: "object", properties: { id: { type: "string", description: "Resource ID, EID, or display name" }, displayName: { type: "string", description: "New display name" }, fte: { type: "number", description: "New FTE (0.0-1.0)" }, lcrCents: { type: "integer", description: "New LCR in cents (e.g. 8500 = 85.00 EUR/h)" }, chapter: { type: "string", description: "New chapter" }, chargeabilityTarget: { type: "number", description: "New chargeability target (0-100)" }, }, required: ["id"], }, }, }, { type: "function", function: { name: "create_resource", description: "Create a new resource/employee. Requires manageResources permission. Always confirm with the user before calling this.", parameters: { type: "object", properties: { eid: { type: "string", description: "Employee ID (e.g. EMP-001)" }, displayName: { type: "string", description: "Full name" }, email: { type: "string", description: "Email address" }, fte: { type: "number", description: "Full-time equivalent 0.0-1.0. Default: 1.0" }, lcrCents: { type: "integer", description: "Labor cost rate in cents/hour (e.g. 8500 = 85.00 EUR/h)" }, ucrCents: { type: "integer", description: "Utilization cost rate in cents/hour (optional; defaults to 70% of LCR)" }, chapter: { type: "string", description: "Chapter/department" }, chargeabilityTarget: { type: "number", description: "Target utilization percentage 0-100. Default: 80" }, roleName: { type: "string", description: "Role name to assign (e.g. 'Designer')" }, countryCode: { type: "string", description: "Country code or name (e.g. 'DE', 'Germany')" }, orgUnitName: { type: "string", description: "Organizational unit name" }, postalCode: { type: "string", description: "Postal code. If provided without federalState, state may be inferred." }, }, required: ["eid", "displayName", "lcrCents"], }, }, }, { type: "function", function: { name: "deactivate_resource", description: "Deactivate a resource (soft delete). Requires manageResources permission. Always confirm with the user before calling this.", parameters: { type: "object", properties: { identifier: { type: "string", description: "Resource ID, EID, or display name" }, }, required: ["identifier"], }, }, }, ], { update_resource: { requiredPermissions: [PermissionKey.MANAGE_RESOURCES], }, create_resource: { requiredPermissions: [PermissionKey.MANAGE_RESOURCES], }, deactivate_resource: { requiredPermissions: [PermissionKey.MANAGE_RESOURCES], }, }); export function createResourceExecutors( deps: ResourceToolsDeps, ): Record { return { async search_resources( params: { query?: string; country?: string; metroCity?: string; orgUnit?: string; roleName?: string; isActive?: boolean; limit?: number; }, ctx: ToolContext, ) { const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); return caller.listSummariesDetail({ ...(params.query !== undefined ? { search: params.query } : {}), ...(params.country !== undefined ? { country: params.country } : {}), ...(params.metroCity !== undefined ? { metroCity: params.metroCity } : {}), ...(params.orgUnit !== undefined ? { orgUnit: params.orgUnit } : {}), ...(params.roleName !== undefined ? { roleName: params.roleName } : {}), isActive: params.isActive ?? true, limit: Math.min(params.limit ?? 50, 100), }); }, async get_resource(params: { identifier: string }, ctx: ToolContext) { const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); return caller.getByIdentifierDetail({ identifier: params.identifier }); }, async update_resource( params: { id: string; displayName?: string; fte?: number; lcrCents?: number; chapter?: string; chargeabilityTarget?: number; }, ctx: ToolContext, ) { deps.assertPermission(ctx, PermissionKey.MANAGE_RESOURCES); const resource = await deps.resolveResourceIdentifier(ctx, params.id); if ("error" in resource) { return resource; } const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); const data = UpdateResourceSchema.parse({ ...(params.displayName !== undefined ? { displayName: params.displayName } : {}), ...(params.fte !== undefined ? { fte: params.fte } : {}), ...(params.lcrCents !== undefined ? { lcrCents: params.lcrCents } : {}), ...(params.chapter !== undefined ? { chapter: params.chapter } : {}), ...(params.chargeabilityTarget !== undefined ? { chargeabilityTarget: params.chargeabilityTarget } : {}), }); const updatedFields = Object.keys(data); if (updatedFields.length === 0) { return { error: "No fields to update" }; } let updated; try { updated = await caller.update({ id: resource.id, data }); } catch (error) { const mapped = deps.toAssistantResourceMutationError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["resource"], success: true, message: `Updated resource ${updated.displayName} (${updated.eid})`, updatedFields, }; }, async create_resource( params: { eid: string; displayName: string; email?: string; fte?: number; lcrCents: number; ucrCents?: number; chapter?: string; chargeabilityTarget?: number; roleName?: string; countryCode?: string; orgUnitName?: string; postalCode?: string; }, ctx: ToolContext, ) { deps.assertPermission(ctx, PermissionKey.MANAGE_RESOURCES); if (!params.email?.trim()) { return { error: "email is required to create a resource." }; } const scopedContext = deps.createScopedCallerContext(ctx); const roleCaller = deps.createRoleCaller(scopedContext); const countryCaller = deps.createCountryCaller(scopedContext); const orgUnitCaller = deps.createOrgUnitCaller(scopedContext); const data: Record = { eid: params.eid, displayName: params.displayName, email: params.email, lcrCents: params.lcrCents, ucrCents: params.ucrCents ?? Math.round(params.lcrCents * 0.7), chargeabilityTarget: params.chargeabilityTarget ?? 80, currency: "EUR", availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }, skills: [], dynamicFields: {}, ...(params.fte !== undefined ? { fte: params.fte } : {}), ...(params.chapter ? { chapter: params.chapter } : {}), ...(params.postalCode ? { postalCode: params.postalCode } : {}), }; const roleIdentifier = params.roleName?.trim(); if (roleIdentifier) { const role = await deps.resolveEntityOrAssistantError( () => roleCaller.resolveByIdentifier({ identifier: roleIdentifier }), `Role not found: "${roleIdentifier}"`, ); if ("error" in role) { return role; } data.roleId = role.id; } const countryIdentifier = params.countryCode?.trim(); if (countryIdentifier) { const country = await deps.resolveEntityOrAssistantError( () => countryCaller.resolveByIdentifier({ identifier: countryIdentifier }), `Country not found: "${countryIdentifier}"`, ); if ("error" in country) { return country; } data.countryId = country.id; } const orgUnitIdentifier = params.orgUnitName?.trim(); if (orgUnitIdentifier) { const orgUnit = await deps.resolveEntityOrAssistantError( () => orgUnitCaller.resolveByIdentifier({ identifier: orgUnitIdentifier }), `Org unit not found: "${orgUnitIdentifier}"`, ); if ("error" in orgUnit) { return orgUnit; } data.orgUnitId = orgUnit.id; } const input = CreateResourceSchema.parse(data); const caller = deps.createResourceCaller(scopedContext); let resource; try { resource = await caller.create(input); } catch (error) { const mapped = deps.toAssistantResourceCreationError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["resource"], success: true, message: `Created resource: ${resource.displayName} (${resource.eid})`, resourceId: resource.id, }; }, async deactivate_resource( params: { identifier: string }, ctx: ToolContext, ) { deps.assertPermission(ctx, PermissionKey.MANAGE_RESOURCES); const resource = await deps.resolveResourceIdentifier(ctx, params.identifier); if ("error" in resource) { return resource; } const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx)); try { await caller.deactivate({ id: resource.id }); } catch (error) { const mapped = deps.toAssistantResourceMutationError(error); if (mapped) { return mapped; } throw error; } return { __action: "invalidate", scope: ["resource"], success: true, message: `Deactivated resource: ${resource.displayName} (${resource.eid})`, }; }, }; }