diff --git a/docs/architecture-hardening-backlog.md b/docs/architecture-hardening-backlog.md index a31817a..cfac09e 100644 --- a/docs/architecture-hardening-backlog.md +++ b/docs/architecture-hardening-backlog.md @@ -44,12 +44,13 @@ - the comment listing and comment mutation assistant helpers now live in their own domain module, keeping collaboration-side comment flows out of the monolithic assistant router without changing the assistant contract - the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract - the import/export and staged Dispo assistant helpers now live in their own domain module, keeping file-bound export/import and batch-staging orchestration out of the monolithic assistant router without changing the assistant contract +- the remaining estimate search, planning lookup, self-service timeline read, and navigation assistant helpers now live in their own domain module, keeping another mixed read-only cluster out of the monolithic assistant router without changing the assistant contract ## Next Up Pin the next structural cleanup on the API side: continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract. -The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining navigation/search helpers or other small read-only assistant clusters still living in the monolithic router. +The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining country read helpers or other small read-only assistant clusters still living in the monolithic router. ## Remaining Major Themes diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index 99af4f6..f2ee33e 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -7,7 +7,6 @@ import { Prisma, VacationType } from "@capakraken/db"; import { CreateAssignmentSchema, AllocationStatus, - EstimateStatus, PermissionKey, SystemRole, } from "@capakraken/shared"; @@ -144,6 +143,10 @@ import { auditHistoryToolDefinitions, createAuditHistoryExecutors, } from "./assistant-tools/audit-history.js"; +import { + createPlanningNavigationExecutors, + planningNavigationToolDefinitions, +} from "./assistant-tools/planning-navigation.js"; import { withToolAccess, type ToolAccessRequirements, @@ -421,8 +424,6 @@ const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const; const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial> = { search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, - list_clients: { requiresPlanningRead: true }, - list_org_units: { requiresResourceOverview: true }, update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, @@ -2005,117 +2006,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([ ...vacationHolidayMutationToolDefinitions, ...rolesAnalyticsReadToolDefinitions, ...chargeabilityComputationReadToolDefinitions, - { - 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)" }, - }, - }, - }, - }, - - // ── NAVIGATION TOOLS ── - { - 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"], - }, - }, - }, + ...planningNavigationToolDefinitions, // ── WRITE TOOLS ── ...allocationPlanningMutationToolDefinitions, @@ -2466,6 +2357,15 @@ const executors = { createReportCaller, createScopedCallerContext, }), + ...createPlanningNavigationExecutors({ + createEstimateCaller, + createClientCaller, + createOrgUnitCaller, + createTimelineCaller, + createScopedCallerContext, + resolveProjectIdentifier, + parseIsoDate, + }), ...createScenarioRateAnalysisExecutors({ assertPermission, createRateCardCaller, @@ -2485,149 +2385,6 @@ const executors = { createScopedCallerContext, }), - async search_estimates(params: { - projectCode?: string; query?: string; status?: string; limit?: number; - }, ctx: ToolContext) { - const caller = createEstimateCaller(createScopedCallerContext(ctx)); - let projectId: string | undefined; - if (params.projectCode) { - const project = await 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 = createClientCaller(createScopedCallerContext(ctx)); - const clients = await caller.list({ - isActive: true, - ...(params.query ? { search: params.query } : {}), - }); - return clients.slice(0, limit).map((c) => ({ - id: c.id, - name: c.name, - code: c.code, - projectCount: c._count.projects, - })); - }, - - async list_org_units(params: { level?: number }, ctx: ToolContext) { - const caller = createOrgUnitCaller(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((u) => ({ - id: u.id, - name: u.name, - shortName: u.shortName, - level: u.level, - parent: u.parent?.name ?? null, - resourceCount: u._count.resources, - })); - }, - - // ── NAVIGATION TOOLS ── - - 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 = createTimelineCaller(createScopedCallerContext(ctx)); - return caller.getMyEntriesView({ - startDate: parseIsoDate(params.startDate, "startDate"), - endDate: 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 = createTimelineCaller(createScopedCallerContext(ctx)); - return caller.getMyHolidayOverlays({ - startDate: parseIsoDate(params.startDate, "startDate"), - endDate: 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(", ")}` }; - - // Build query params for pages that support them - 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", - url, - description: `Navigiere zu ${path}`, - }; - }, - // ── VACATION MANAGEMENT ── async create_vacation(params: { diff --git a/packages/api/src/router/assistant-tools/planning-navigation.ts b/packages/api/src/router/assistant-tools/planning-navigation.ts new file mode 100644 index 0000000..c60ff20 --- /dev/null +++ b/packages/api/src/router/assistant-tools/planning-navigation.ts @@ -0,0 +1,369 @@ +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}`, + }; + }, + }; +}