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
+14 -257
View File
@@ -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<Record<string, ToolAccessRequirements>> = {
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<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 ──
async create_vacation(params: {