b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
971 lines
34 KiB
TypeScript
971 lines
34 KiB
TypeScript
import type { TRPCContext } from "../../trpc.js";
|
|
import { AllocationStatus, PermissionKey, SystemRole } from "@nexus/shared";
|
|
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
|
|
|
export const advancedTimelineToolDefinitions: ToolDef[] = withToolAccess(
|
|
[
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "find_best_project_resource",
|
|
description:
|
|
"Advanced assistant tool: find the best already-assigned resource on a project for a given period, ranked by remaining capacity or LCR. Holiday- and vacation-aware. Requires viewCosts and advanced assistant permissions.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
startDate: {
|
|
type: "string",
|
|
description: "Optional start date in YYYY-MM-DD. Default: today.",
|
|
},
|
|
endDate: {
|
|
type: "string",
|
|
description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used.",
|
|
},
|
|
durationDays: {
|
|
type: "integer",
|
|
description:
|
|
"Optional duration in calendar days when endDate is omitted. Default: 21.",
|
|
},
|
|
minHoursPerDay: {
|
|
type: "number",
|
|
description: "Minimum remaining availability per effective working day. Default: 3.",
|
|
},
|
|
rankingMode: {
|
|
type: "string",
|
|
description:
|
|
"Ranking mode: lowest_lcr, highest_remaining_hours_per_day, or highest_remaining_hours. Default: lowest_lcr.",
|
|
},
|
|
chapter: {
|
|
type: "string",
|
|
description: "Optional chapter filter for candidate resources.",
|
|
},
|
|
roleName: {
|
|
type: "string",
|
|
description: "Optional role filter for candidate resources.",
|
|
},
|
|
},
|
|
required: ["projectIdentifier"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "get_timeline_entries_view",
|
|
description:
|
|
"Advanced assistant tool: read-only timeline entries view with the same timeline/disposition readmodel used by the app. Returns allocations, demands, assignments, and matching holiday overlays for a period.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
startDate: {
|
|
type: "string",
|
|
description: "Optional start date in YYYY-MM-DD. Default: today.",
|
|
},
|
|
endDate: {
|
|
type: "string",
|
|
description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used.",
|
|
},
|
|
durationDays: {
|
|
type: "integer",
|
|
description:
|
|
"Optional duration in calendar days when endDate is omitted. Default: 21.",
|
|
},
|
|
resourceIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional resource IDs to scope the view.",
|
|
},
|
|
projectIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional project IDs to scope the view.",
|
|
},
|
|
clientIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional client IDs to scope the view.",
|
|
},
|
|
chapters: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional chapter filters.",
|
|
},
|
|
eids: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional employee IDs to scope the view.",
|
|
},
|
|
countryCodes: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional country codes such as DE or ES.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "get_timeline_holiday_overlays",
|
|
description:
|
|
"Advanced assistant tool: read-only holiday overlays for the timeline, resolved with the same holiday logic as the app. Useful to explain regional holiday differences for assigned or filtered resources.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
startDate: {
|
|
type: "string",
|
|
description: "Optional start date in YYYY-MM-DD. Default: today.",
|
|
},
|
|
endDate: {
|
|
type: "string",
|
|
description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used.",
|
|
},
|
|
durationDays: {
|
|
type: "integer",
|
|
description:
|
|
"Optional duration in calendar days when endDate is omitted. Default: 21.",
|
|
},
|
|
resourceIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional resource IDs to scope the overlays.",
|
|
},
|
|
projectIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional project IDs to scope the overlays via matching assignments.",
|
|
},
|
|
clientIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional client IDs to scope the overlays via matching projects.",
|
|
},
|
|
chapters: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional chapter filters.",
|
|
},
|
|
eids: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional employee IDs to scope the overlays.",
|
|
},
|
|
countryCodes: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Optional country codes such as DE or ES.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "get_project_timeline_context",
|
|
description:
|
|
"Advanced assistant tool: read-only project timeline/disposition context. Reuses the same project context readmodel as the app and adds holiday overlays plus cross-project overlap summaries for assigned resources.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
startDate: {
|
|
type: "string",
|
|
description:
|
|
"Optional holiday/conflict window start date in YYYY-MM-DD. Defaults to the project start date when available.",
|
|
},
|
|
endDate: {
|
|
type: "string",
|
|
description:
|
|
"Optional holiday/conflict window end date in YYYY-MM-DD. Defaults to the project end date when available.",
|
|
},
|
|
durationDays: {
|
|
type: "integer",
|
|
description: "Optional duration in calendar days when endDate is omitted.",
|
|
},
|
|
},
|
|
required: ["projectIdentifier"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "preview_project_shift",
|
|
description:
|
|
"Advanced assistant tool: read-only preview of the timeline shift validation for a project. Uses the same preview logic as the timeline router and does not write changes.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
newStartDate: { type: "string", description: "New start date in YYYY-MM-DD." },
|
|
newEndDate: { type: "string", description: "New end date in YYYY-MM-DD." },
|
|
},
|
|
required: ["projectIdentifier", "newStartDate", "newEndDate"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "update_timeline_allocation_inline",
|
|
description:
|
|
"Advanced assistant mutation: update a timeline allocation inline with the same manager/admin + manageAllocations validation as the timeline API. Supports hours/day, dates, includeSaturday, and role changes. Requires useAssistantAdvancedTools.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
allocationId: {
|
|
type: "string",
|
|
description: "Allocation, assignment, or demand row ID to update.",
|
|
},
|
|
hoursPerDay: { type: "number", description: "Optional new booked hours per day." },
|
|
startDate: { type: "string", description: "Optional new start date in YYYY-MM-DD." },
|
|
endDate: { type: "string", description: "Optional new end date in YYYY-MM-DD." },
|
|
includeSaturday: {
|
|
type: "boolean",
|
|
description: "Optional Saturday-working flag stored in metadata.",
|
|
},
|
|
role: { type: "string", description: "Optional new role label." },
|
|
},
|
|
required: ["allocationId"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "apply_timeline_project_shift",
|
|
description:
|
|
"Advanced assistant mutation: apply the real timeline project shift mutation, including validation, date movement, cost recalculation, audit logging, and SSE. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
newStartDate: { type: "string", description: "New project start date in YYYY-MM-DD." },
|
|
newEndDate: { type: "string", description: "New project end date in YYYY-MM-DD." },
|
|
},
|
|
required: ["projectIdentifier", "newStartDate", "newEndDate"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "quick_assign_timeline_resource",
|
|
description:
|
|
"Advanced assistant mutation: create a timeline quick-assignment with the same manager/admin + manageAllocations rules as the timeline UI. Resolves resource and project identifiers before calling the real mutation. Requires useAssistantAdvancedTools.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
resourceIdentifier: {
|
|
type: "string",
|
|
description: "Resource ID, eid, or display name.",
|
|
},
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
|
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
|
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
|
|
role: { type: "string", description: "Role label. Default: Team Member." },
|
|
roleId: { type: "string", description: "Optional concrete role ID." },
|
|
status: {
|
|
type: "string",
|
|
enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"],
|
|
description: "Assignment status. Default: PROPOSED.",
|
|
},
|
|
},
|
|
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "batch_quick_assign_timeline_resources",
|
|
description:
|
|
"Advanced assistant mutation: batch-create timeline quick-assignments using the same timeline router logic, permission checks, and audit/SSE side effects as the app. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
assignments: {
|
|
type: "array",
|
|
minItems: 1,
|
|
maxItems: 50,
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
resourceIdentifier: {
|
|
type: "string",
|
|
description: "Resource ID, eid, or display name.",
|
|
},
|
|
projectIdentifier: {
|
|
type: "string",
|
|
description: "Project ID, short code, or project name.",
|
|
},
|
|
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
|
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
|
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
|
|
role: { type: "string", description: "Role label. Default: Team Member." },
|
|
status: {
|
|
type: "string",
|
|
enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"],
|
|
description: "Assignment status. Default: PROPOSED.",
|
|
},
|
|
},
|
|
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
|
|
},
|
|
description: "Assignment rows to create in one batch.",
|
|
},
|
|
},
|
|
required: ["assignments"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "function",
|
|
function: {
|
|
name: "batch_shift_timeline_allocations",
|
|
description:
|
|
"Advanced assistant mutation: shift multiple timeline allocations by a shared day delta using the real timeline batch move/resize mutation. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
allocationIds: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Allocation IDs to shift.",
|
|
},
|
|
daysDelta: { type: "integer", description: "Signed day delta to apply." },
|
|
mode: {
|
|
type: "string",
|
|
enum: ["move", "resize-start", "resize-end"],
|
|
description: "Shift mode. Default: move.",
|
|
},
|
|
},
|
|
required: ["allocationIds", "daysDelta"],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
{
|
|
find_best_project_resource: {
|
|
requiresPlanningRead: true,
|
|
requiresCostView: true,
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
get_timeline_entries_view: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
get_timeline_holiday_overlays: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
get_project_timeline_context: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
preview_project_shift: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
update_timeline_allocation_inline: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
|
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
apply_timeline_project_shift: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
|
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
quick_assign_timeline_resource: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
|
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
batch_quick_assign_timeline_resources: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
|
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
batch_shift_timeline_allocations: {
|
|
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
|
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
|
requiresAdvancedAssistant: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
type AssistantToolErrorResult = { error: string };
|
|
|
|
type ResolvedProject = {
|
|
id: string;
|
|
name?: string | null;
|
|
shortCode?: string | null;
|
|
};
|
|
|
|
type ResolvedResource = {
|
|
id: string;
|
|
displayName: string;
|
|
};
|
|
|
|
type TimelineMutationContext = "updateInline" | "applyShift" | "quickAssign" | "batchShift";
|
|
|
|
type BatchQuickAssignmentInput = {
|
|
resourceId: string;
|
|
projectId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay?: number;
|
|
role?: string;
|
|
status?: AllocationStatus;
|
|
};
|
|
|
|
type AdvancedTimelineDeps = {
|
|
assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
|
|
createStaffingCaller: (ctx: TRPCContext) => {
|
|
getBestProjectResourceDetail: (params: {
|
|
projectId: string;
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
durationDays?: number;
|
|
minHoursPerDay?: number;
|
|
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
|
|
chapter?: string;
|
|
roleName?: string;
|
|
}) => Promise<unknown>;
|
|
};
|
|
createTimelineCaller: (ctx: TRPCContext) => {
|
|
getEntriesDetail: (params: {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
resourceIds?: string[];
|
|
projectIds?: string[];
|
|
clientIds?: string[];
|
|
chapters?: string[];
|
|
eids?: string[];
|
|
countryCodes?: string[];
|
|
}) => Promise<unknown>;
|
|
getHolidayOverlayDetail: (params: {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
resourceIds?: string[];
|
|
projectIds?: string[];
|
|
clientIds?: string[];
|
|
chapters?: string[];
|
|
eids?: string[];
|
|
countryCodes?: string[];
|
|
}) => Promise<unknown>;
|
|
getProjectContextDetail: (params: {
|
|
projectId: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
}) => Promise<unknown>;
|
|
getShiftPreviewDetail: (params: {
|
|
projectId: string;
|
|
newStartDate: Date;
|
|
newEndDate: Date;
|
|
}) => Promise<unknown>;
|
|
updateAllocationInline: (params: {
|
|
allocationId: string;
|
|
hoursPerDay?: number;
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
includeSaturday?: boolean;
|
|
role?: string;
|
|
}) => Promise<{
|
|
id: string;
|
|
projectId: string;
|
|
resourceId?: string | null;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay: number;
|
|
role?: string | null;
|
|
status: string;
|
|
}>;
|
|
applyShift: (params: { projectId: string; newStartDate: Date; newEndDate: Date }) => Promise<{
|
|
project: {
|
|
id: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
};
|
|
validation: unknown;
|
|
}>;
|
|
quickAssign: (params: {
|
|
resourceId: string;
|
|
projectId: string;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay?: number;
|
|
role?: string;
|
|
roleId?: string;
|
|
status?: AllocationStatus;
|
|
}) => Promise<{
|
|
id: string;
|
|
projectId: string;
|
|
resourceId?: string | null;
|
|
startDate: Date | string;
|
|
endDate: Date | string;
|
|
hoursPerDay: number;
|
|
role?: string | null;
|
|
status: string;
|
|
}>;
|
|
batchQuickAssign: (params: {
|
|
assignments: BatchQuickAssignmentInput[];
|
|
}) => Promise<{ count: number }>;
|
|
batchShiftAllocations: (params: {
|
|
allocationIds: string[];
|
|
daysDelta: number;
|
|
mode?: "move" | "resize-start" | "resize-end";
|
|
}) => Promise<{ count: number }>;
|
|
};
|
|
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
|
resolveProjectIdentifier: (
|
|
ctx: ToolContext,
|
|
identifier: string,
|
|
) => Promise<ResolvedProject | AssistantToolErrorResult>;
|
|
resolveResourceIdentifier: (
|
|
ctx: ToolContext,
|
|
identifier: string,
|
|
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
|
parseIsoDate: (value: string, fieldName: string) => Date;
|
|
fmtDate: (value: Date | null | undefined) => string | null;
|
|
isAssistantToolErrorResult: (value: unknown) => value is AssistantToolErrorResult;
|
|
toAssistantIndexedFieldError: (index: number, field: string, message: string) => unknown;
|
|
toAssistantTimelineMutationError: (error: unknown, context: TimelineMutationContext) => unknown;
|
|
};
|
|
|
|
function toDate(value: Date | string): Date {
|
|
return value instanceof Date ? value : new Date(value);
|
|
}
|
|
|
|
export function createAdvancedTimelineExecutors(
|
|
deps: AdvancedTimelineDeps,
|
|
): Record<string, ToolExecutor> {
|
|
return {
|
|
async find_best_project_resource(
|
|
params: {
|
|
projectIdentifier: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
minHoursPerDay?: number;
|
|
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
|
|
chapter?: string;
|
|
roleName?: string;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
|
|
|
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
|
if ("error" in project) {
|
|
return project;
|
|
}
|
|
|
|
const caller = deps.createStaffingCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.getBestProjectResourceDetail({
|
|
projectId: project.id,
|
|
...(params.startDate
|
|
? { startDate: deps.parseIsoDate(params.startDate, "startDate") }
|
|
: {}),
|
|
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
|
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
|
|
...(params.minHoursPerDay !== undefined ? { minHoursPerDay: params.minHoursPerDay } : {}),
|
|
...(params.rankingMode ? { rankingMode: params.rankingMode } : {}),
|
|
...(params.chapter ? { chapter: params.chapter } : {}),
|
|
...(params.roleName ? { roleName: params.roleName } : {}),
|
|
});
|
|
},
|
|
|
|
async get_timeline_entries_view(
|
|
params: {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
resourceIds?: string[];
|
|
projectIds?: string[];
|
|
clientIds?: string[];
|
|
chapters?: string[];
|
|
eids?: string[];
|
|
countryCodes?: string[];
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.getEntriesDetail({ ...params });
|
|
},
|
|
|
|
async get_timeline_holiday_overlays(
|
|
params: {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
resourceIds?: string[];
|
|
projectIds?: string[];
|
|
clientIds?: string[];
|
|
chapters?: string[];
|
|
eids?: string[];
|
|
countryCodes?: string[];
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.getHolidayOverlayDetail({ ...params });
|
|
},
|
|
|
|
async get_project_timeline_context(
|
|
params: {
|
|
projectIdentifier: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
durationDays?: number;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
|
if ("error" in project) {
|
|
return project;
|
|
}
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.getProjectContextDetail({
|
|
projectId: project.id,
|
|
...(params.startDate ? { startDate: params.startDate } : {}),
|
|
...(params.endDate ? { endDate: params.endDate } : {}),
|
|
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
|
|
});
|
|
},
|
|
|
|
async preview_project_shift(
|
|
params: {
|
|
projectIdentifier: string;
|
|
newStartDate: string;
|
|
newEndDate: string;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
|
if ("error" in project) {
|
|
return project;
|
|
}
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
return caller.getShiftPreviewDetail({
|
|
projectId: project.id,
|
|
newStartDate: deps.parseIsoDate(params.newStartDate, "newStartDate"),
|
|
newEndDate: deps.parseIsoDate(params.newEndDate, "newEndDate"),
|
|
});
|
|
},
|
|
|
|
async update_timeline_allocation_inline(
|
|
params: {
|
|
allocationId: string;
|
|
hoursPerDay?: number;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
includeSaturday?: boolean;
|
|
role?: string;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
let updated;
|
|
try {
|
|
updated = await caller.updateAllocationInline({
|
|
allocationId: params.allocationId,
|
|
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
|
|
...(params.startDate
|
|
? { startDate: deps.parseIsoDate(params.startDate, "startDate") }
|
|
: {}),
|
|
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
|
...(params.includeSaturday !== undefined
|
|
? { includeSaturday: params.includeSaturday }
|
|
: {}),
|
|
...(params.role !== undefined ? { role: params.role } : {}),
|
|
});
|
|
} catch (error) {
|
|
const mapped = deps.toAssistantTimelineMutationError(error, "updateInline");
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
__action: "invalidate",
|
|
scope: ["allocation", "timeline", "project"],
|
|
success: true,
|
|
message: `Updated timeline allocation ${updated.id}.`,
|
|
allocation: {
|
|
id: updated.id,
|
|
projectId: updated.projectId,
|
|
resourceId: updated.resourceId ?? null,
|
|
startDate: deps.fmtDate(updated.startDate),
|
|
endDate: deps.fmtDate(updated.endDate),
|
|
hoursPerDay: updated.hoursPerDay,
|
|
role: updated.role ?? null,
|
|
status: updated.status,
|
|
},
|
|
};
|
|
},
|
|
|
|
async apply_timeline_project_shift(
|
|
params: {
|
|
projectIdentifier: string;
|
|
newStartDate: string;
|
|
newEndDate: string;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
|
if ("error" in project) {
|
|
return project;
|
|
}
|
|
|
|
const newStartDate = deps.parseIsoDate(params.newStartDate, "newStartDate");
|
|
const newEndDate = deps.parseIsoDate(params.newEndDate, "newEndDate");
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
let result;
|
|
try {
|
|
result = await caller.applyShift({
|
|
projectId: project.id,
|
|
newStartDate,
|
|
newEndDate,
|
|
});
|
|
} catch (error) {
|
|
const mapped = deps.toAssistantTimelineMutationError(error, "applyShift");
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
__action: "invalidate",
|
|
scope: ["allocation", "timeline", "project"],
|
|
success: true,
|
|
message: `Shifted project ${project.shortCode ?? project.name ?? project.id} to ${deps.fmtDate(newStartDate)} - ${deps.fmtDate(newEndDate)}.`,
|
|
project: {
|
|
id: result.project.id,
|
|
startDate: deps.fmtDate(result.project.startDate),
|
|
endDate: deps.fmtDate(result.project.endDate),
|
|
},
|
|
validation: result.validation,
|
|
};
|
|
},
|
|
|
|
async quick_assign_timeline_resource(
|
|
params: {
|
|
resourceIdentifier: string;
|
|
projectIdentifier: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
hoursPerDay?: number;
|
|
role?: string;
|
|
roleId?: string;
|
|
status?: AllocationStatus;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const [resource, project] = await Promise.all([
|
|
deps.resolveResourceIdentifier(ctx, params.resourceIdentifier),
|
|
deps.resolveProjectIdentifier(ctx, params.projectIdentifier),
|
|
]);
|
|
if ("error" in resource) {
|
|
return resource;
|
|
}
|
|
if ("error" in project) {
|
|
return project;
|
|
}
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
let allocation;
|
|
try {
|
|
allocation = await caller.quickAssign({
|
|
resourceId: resource.id,
|
|
projectId: project.id,
|
|
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
|
endDate: deps.parseIsoDate(params.endDate, "endDate"),
|
|
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
|
|
...(params.role !== undefined ? { role: params.role } : {}),
|
|
...(params.roleId !== undefined ? { roleId: params.roleId } : {}),
|
|
...(params.status !== undefined ? { status: params.status } : {}),
|
|
});
|
|
} catch (error) {
|
|
const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
__action: "invalidate",
|
|
scope: ["allocation", "timeline", "project"],
|
|
success: true,
|
|
message: `Quick-assigned ${resource.displayName} to ${project.name} (${project.shortCode ?? project.id}).`,
|
|
allocation: {
|
|
id: allocation.id,
|
|
projectId: allocation.projectId,
|
|
resourceId: allocation.resourceId ?? null,
|
|
startDate: deps.fmtDate(toDate(allocation.startDate)),
|
|
endDate: deps.fmtDate(toDate(allocation.endDate)),
|
|
hoursPerDay: allocation.hoursPerDay,
|
|
role: allocation.role ?? null,
|
|
status: allocation.status,
|
|
},
|
|
};
|
|
},
|
|
|
|
async batch_quick_assign_timeline_resources(
|
|
params: {
|
|
assignments: Array<{
|
|
resourceIdentifier: string;
|
|
projectIdentifier: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
hoursPerDay?: number;
|
|
role?: string;
|
|
status?: AllocationStatus;
|
|
}>;
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const resolvedAssignments = await Promise.all(
|
|
params.assignments.map(async (assignment, index) => {
|
|
const [resource, project] = await Promise.all([
|
|
deps.resolveResourceIdentifier(ctx, assignment.resourceIdentifier),
|
|
deps.resolveProjectIdentifier(ctx, assignment.projectIdentifier),
|
|
]);
|
|
if ("error" in resource) {
|
|
return deps.toAssistantIndexedFieldError(index, "resourceIdentifier", resource.error);
|
|
}
|
|
if ("error" in project) {
|
|
return deps.toAssistantIndexedFieldError(index, "projectIdentifier", project.error);
|
|
}
|
|
return {
|
|
resourceId: resource.id,
|
|
projectId: project.id,
|
|
startDate: deps.parseIsoDate(assignment.startDate, `assignments[${index}].startDate`),
|
|
endDate: deps.parseIsoDate(assignment.endDate, `assignments[${index}].endDate`),
|
|
...(assignment.hoursPerDay !== undefined
|
|
? { hoursPerDay: assignment.hoursPerDay }
|
|
: {}),
|
|
...(assignment.role !== undefined ? { role: assignment.role } : {}),
|
|
...(assignment.status !== undefined ? { status: assignment.status } : {}),
|
|
};
|
|
}),
|
|
);
|
|
|
|
const resolutionError = resolvedAssignments.find(deps.isAssistantToolErrorResult);
|
|
if (resolutionError) {
|
|
return resolutionError;
|
|
}
|
|
const validAssignments = resolvedAssignments.filter(
|
|
(assignment): assignment is BatchQuickAssignmentInput =>
|
|
!deps.isAssistantToolErrorResult(assignment),
|
|
);
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
let result;
|
|
try {
|
|
result = await caller.batchQuickAssign({
|
|
assignments: validAssignments,
|
|
});
|
|
} catch (error) {
|
|
const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
__action: "invalidate",
|
|
scope: ["allocation", "timeline", "project"],
|
|
success: true,
|
|
message: `Created ${result.count} timeline quick-assignment(s).`,
|
|
count: result.count,
|
|
};
|
|
},
|
|
|
|
async batch_shift_timeline_allocations(
|
|
params: {
|
|
allocationIds: string[];
|
|
daysDelta: number;
|
|
mode?: "move" | "resize-start" | "resize-end";
|
|
},
|
|
ctx: ToolContext,
|
|
) {
|
|
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
|
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
|
|
|
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
|
let result;
|
|
try {
|
|
result = await caller.batchShiftAllocations({
|
|
allocationIds: params.allocationIds,
|
|
daysDelta: params.daysDelta,
|
|
...(params.mode !== undefined ? { mode: params.mode } : {}),
|
|
});
|
|
} catch (error) {
|
|
const mapped = deps.toAssistantTimelineMutationError(error, "batchShift");
|
|
if (mapped) {
|
|
return mapped;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return {
|
|
__action: "invalidate",
|
|
scope: ["allocation", "timeline", "project"],
|
|
success: true,
|
|
message: `Shifted ${result.count} allocation(s) by ${params.daysDelta} day(s).`,
|
|
count: result.count,
|
|
};
|
|
},
|
|
};
|
|
}
|