refactor(api): extract assistant planning navigation slice

This commit is contained in:
2026-03-30 22:51:39 +02:00
parent aed99cb894
commit 0cc7b9805a
3 changed files with 385 additions and 258 deletions
+2 -1
View File
@@ -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 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 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 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 ## Next Up
Pin the next structural cleanup on the API side: 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. 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 ## Remaining Major Themes
+14 -257
View File
@@ -7,7 +7,6 @@ import { Prisma, VacationType } from "@capakraken/db";
import { import {
CreateAssignmentSchema, CreateAssignmentSchema,
AllocationStatus, AllocationStatus,
EstimateStatus,
PermissionKey, PermissionKey,
SystemRole, SystemRole,
} from "@capakraken/shared"; } from "@capakraken/shared";
@@ -144,6 +143,10 @@ import {
auditHistoryToolDefinitions, auditHistoryToolDefinitions,
createAuditHistoryExecutors, createAuditHistoryExecutors,
} from "./assistant-tools/audit-history.js"; } from "./assistant-tools/audit-history.js";
import {
createPlanningNavigationExecutors,
planningNavigationToolDefinitions,
} from "./assistant-tools/planning-navigation.js";
import { import {
withToolAccess, withToolAccess,
type ToolAccessRequirements, type ToolAccessRequirements,
@@ -421,8 +424,6 @@ const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const;
const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = { const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = {
search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] }, search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
get_project: { 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] }, update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
@@ -2005,117 +2006,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...vacationHolidayMutationToolDefinitions, ...vacationHolidayMutationToolDefinitions,
...rolesAnalyticsReadToolDefinitions, ...rolesAnalyticsReadToolDefinitions,
...chargeabilityComputationReadToolDefinitions, ...chargeabilityComputationReadToolDefinitions,
{ ...planningNavigationToolDefinitions,
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"],
},
},
},
// ── WRITE TOOLS ── // ── WRITE TOOLS ──
...allocationPlanningMutationToolDefinitions, ...allocationPlanningMutationToolDefinitions,
@@ -2466,6 +2357,15 @@ const executors = {
createReportCaller, createReportCaller,
createScopedCallerContext, createScopedCallerContext,
}), }),
...createPlanningNavigationExecutors({
createEstimateCaller,
createClientCaller,
createOrgUnitCaller,
createTimelineCaller,
createScopedCallerContext,
resolveProjectIdentifier,
parseIsoDate,
}),
...createScenarioRateAnalysisExecutors({ ...createScenarioRateAnalysisExecutors({
assertPermission, assertPermission,
createRateCardCaller, createRateCardCaller,
@@ -2485,149 +2385,6 @@ const executors = {
createScopedCallerContext, 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<string, string> = {
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 ── // ── VACATION MANAGEMENT ──
async create_vacation(params: { async create_vacation(params: {
@@ -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<unknown>;
};
createClientCaller: (ctx: TRPCContext) => {
list: (params: {
isActive: boolean;
search?: string;
}) => Promise<Array<{
id: string;
name: string;
code: string | null;
_count: { projects: number };
}>>;
};
createOrgUnitCaller: (ctx: TRPCContext) => {
list: (params: {
isActive: boolean;
level?: number;
}) => Promise<Array<{ id: string }>>;
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<unknown>;
getMyHolidayOverlays: (params: {
startDate: Date;
endDate: Date;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}) => Promise<unknown>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
resolveProjectIdentifier: (
ctx: ToolContext,
identifier: string,
) => Promise<ResolvedProject | AssistantToolErrorResult>;
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<string, ToolExecutor> {
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<string, string> = {
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}`,
};
},
};
}