refactor(api): extract assistant planning navigation slice
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user