import { EstimateStatus, SystemRole } 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 ResolvedProject = { id: string; }; type PlanningNavigationDeps = { createEstimateCaller: (ctx: TRPCContext) => { list: (params: { query?: string; status?: EstimateStatus; projectId?: string; }) => Promise; }; createClientCaller: (ctx: TRPCContext) => { list: (params: { isActive: boolean; search?: string; }) => Promise>; }; createOrgUnitCaller: (ctx: TRPCContext) => { list: (params: { isActive: boolean; level?: number; }) => Promise>; getById: (params: { id: string }) => Promise<{ id: string; name: string; shortName: string | null; level: number; parent: { name: string } | null; _count: { resources: number }; }>; }; createTimelineCaller: (ctx: TRPCContext) => { getMyEntriesView: (params: { startDate: Date; endDate: Date; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }) => Promise; getMyHolidayOverlays: (params: { startDate: Date; endDate: Date; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }) => Promise; }; createScopedCallerContext: (ctx: ToolContext) => TRPCContext; resolveProjectIdentifier: ( ctx: ToolContext, identifier: string, ) => Promise; parseIsoDate: (value: string, fieldName: string) => Date; }; export const planningNavigationToolDefinitions: ToolDef[] = withToolAccess([ { type: "function", function: { name: "search_estimates", description: "Search for estimates (cost/effort estimates) by project or name. Returns estimate name, status, version count.", parameters: { type: "object", properties: { projectCode: { type: "string", description: "Project short code to filter by" }, query: { type: "string", description: "Search term (matches estimate name)" }, status: { type: "string", description: "Filter by status: DRAFT, IN_REVIEW, APPROVED, ARCHIVED" }, limit: { type: "integer", description: "Max results. Default: 20" }, }, }, }, }, { type: "function", function: { name: "list_clients", description: "List clients/customers. Can search by name or code.", parameters: { type: "object", properties: { query: { type: "string", description: "Search term (matches name or code)" }, limit: { type: "integer", description: "Max results. Default: 20" }, }, }, }, }, { type: "function", function: { name: "list_org_units", description: "List organizational units (departments, teams) with their hierarchy.", parameters: { type: "object", properties: { level: { type: "integer", description: "Filter by org level (5, 6, or 7)" }, }, }, }, }, { type: "function", function: { name: "get_my_timeline_entries_view", description: "Get the caller's own self-service timeline entries view for a date range using the real timeline self-service endpoint. Returns only data for the caller's linked resource.", parameters: { type: "object", properties: { startDate: { type: "string", description: "Start date in YYYY-MM-DD." }, endDate: { type: "string", description: "End date in YYYY-MM-DD." }, resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." }, projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own timeline view." }, clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own timeline view." }, chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." }, eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." }, countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." }, }, required: ["startDate", "endDate"], }, }, }, { type: "function", function: { name: "get_my_timeline_holiday_overlays", description: "Get the caller's own self-service holiday overlays for a date range using the real timeline self-service endpoint. Returns only holiday overlays for the caller's linked resource.", parameters: { type: "object", properties: { startDate: { type: "string", description: "Start date in YYYY-MM-DD." }, endDate: { type: "string", description: "End date in YYYY-MM-DD." }, resourceIds: { type: "array", items: { type: "string" }, description: "Optional filters are accepted but will be scoped to the caller's own linked resource." }, projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to narrow the caller's own holiday overlay view." }, clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to narrow the caller's own holiday overlay view." }, chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters. Self-service scoping still applies." }, eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs. Self-service scoping still applies." }, countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes. Self-service scoping still applies." }, }, required: ["startDate", "endDate"], }, }, }, { type: "function", function: { name: "navigate_to_page", description: "Navigate the user to a specific page in CapaKraken, optionally with filters. Use this when the user wants to see data on a specific page (e.g. 'show me on the timeline', 'open the resources page').", parameters: { type: "object", properties: { page: { type: "string", description: "Page name: timeline, dashboard, resources, projects, allocations, staffing, estimates, vacations, my-vacations, roles, skills-analytics, chargeability, computation-graph", }, eids: { type: "string", description: "Comma-separated employee IDs to filter (for timeline)" }, chapters: { type: "string", description: "Comma-separated chapters to filter (for timeline)" }, projectIds: { type: "string", description: "Comma-separated project IDs to filter (for timeline)" }, clientIds: { type: "string", description: "Comma-separated client IDs to filter (for timeline)" }, countryCodes: { type: "string", description: "Comma-separated country codes to filter (e.g. 'ES,DE' for Spain and Germany, for timeline)" }, startDate: { type: "string", description: "Start date YYYY-MM-DD (for timeline)" }, days: { type: "integer", description: "Number of days to show (for timeline)" }, }, required: ["page"], }, }, }, ], { search_estimates: { allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER], }, list_clients: { requiresPlanningRead: true, }, list_org_units: { requiresResourceOverview: true, }, }); export function createPlanningNavigationExecutors( deps: PlanningNavigationDeps, ): Record { return { async search_estimates( params: { projectCode?: string; query?: string; status?: string; limit?: number; }, ctx: ToolContext, ) { const caller = deps.createEstimateCaller(deps.createScopedCallerContext(ctx)); let projectId: string | undefined; if (params.projectCode) { const project = await deps.resolveProjectIdentifier(ctx, params.projectCode); if ("error" in project) { return project; } projectId = project.id; } return caller.list({ ...(params.query ? { query: params.query } : {}), ...(params.status ? { status: params.status as EstimateStatus } : {}), ...(projectId ? { projectId } : {}), }); }, async list_clients( params: { query?: string; limit?: number }, ctx: ToolContext, ) { const limit = Math.min(params.limit ?? 20, 50); const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx)); const clients = await caller.list({ isActive: true, ...(params.query ? { search: params.query } : {}), }); return clients.slice(0, limit).map((client) => ({ id: client.id, name: client.name, code: client.code, projectCount: client._count.projects, })); }, async list_org_units( params: { level?: number }, ctx: ToolContext, ) { const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx)); const units = await caller.list({ isActive: true, ...(params.level !== undefined ? { level: params.level } : {}), }); const details = await Promise.all(units.map((unit) => caller.getById({ id: unit.id }))); return details.map((unit) => ({ id: unit.id, name: unit.name, shortName: unit.shortName, level: unit.level, parent: unit.parent?.name ?? null, resourceCount: unit._count.resources, })); }, async get_my_timeline_entries_view( params: { startDate: string; endDate: string; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }, ctx: ToolContext, ) { const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx)); return caller.getMyEntriesView({ startDate: deps.parseIsoDate(params.startDate, "startDate"), endDate: deps.parseIsoDate(params.endDate, "endDate"), ...(params.resourceIds ? { resourceIds: params.resourceIds } : {}), ...(params.projectIds ? { projectIds: params.projectIds } : {}), ...(params.clientIds ? { clientIds: params.clientIds } : {}), ...(params.chapters ? { chapters: params.chapters } : {}), ...(params.eids ? { eids: params.eids } : {}), ...(params.countryCodes ? { countryCodes: params.countryCodes } : {}), }); }, async get_my_timeline_holiday_overlays( params: { startDate: string; endDate: string; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }, ctx: ToolContext, ) { const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx)); return caller.getMyHolidayOverlays({ startDate: deps.parseIsoDate(params.startDate, "startDate"), endDate: deps.parseIsoDate(params.endDate, "endDate"), ...(params.resourceIds ? { resourceIds: params.resourceIds } : {}), ...(params.projectIds ? { projectIds: params.projectIds } : {}), ...(params.clientIds ? { clientIds: params.clientIds } : {}), ...(params.chapters ? { chapters: params.chapters } : {}), ...(params.eids ? { eids: params.eids } : {}), ...(params.countryCodes ? { countryCodes: params.countryCodes } : {}), }); }, async navigate_to_page( params: { page: string; eids?: string; chapters?: string; projectIds?: string; clientIds?: string; countryCodes?: string; startDate?: string; days?: number; }, _ctx: ToolContext, ) { const pageMap: Record = { timeline: "/timeline", dashboard: "/dashboard", resources: "/resources", projects: "/projects", allocations: "/allocations", staffing: "/staffing", estimates: "/estimates", vacations: "/vacations", "my-vacations": "/vacations/my", roles: "/roles", "skills-analytics": "/analytics/skills", chargeability: "/reports/chargeability", "computation-graph": "/analytics/computation-graph", }; const path = pageMap[params.page]; if (!path) { return { error: `Unknown page: ${params.page}. Available: ${Object.keys(pageMap).join(", ")}` }; } const queryParts: string[] = []; if (params.eids) queryParts.push(`eids=${encodeURIComponent(params.eids)}`); if (params.chapters) queryParts.push(`chapters=${encodeURIComponent(params.chapters)}`); if (params.projectIds) queryParts.push(`projectIds=${encodeURIComponent(params.projectIds)}`); if (params.clientIds) queryParts.push(`clientIds=${encodeURIComponent(params.clientIds)}`); if (params.countryCodes) queryParts.push(`countryCodes=${encodeURIComponent(params.countryCodes)}`); if (params.startDate) queryParts.push(`startDate=${encodeURIComponent(params.startDate)}`); if (params.days) queryParts.push(`days=${params.days}`); const url = queryParts.length > 0 ? `${path}?${queryParts.join("&")}` : path; return { __action: "navigate" as const, url, description: `Navigiere zu ${path}`, }; }, }; }