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; }; createTimelineCaller: (ctx: TRPCContext) => { getEntriesDetail: (params: { startDate?: string; endDate?: string; durationDays?: number; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }) => Promise; getHolidayOverlayDetail: (params: { startDate?: string; endDate?: string; durationDays?: number; resourceIds?: string[]; projectIds?: string[]; clientIds?: string[]; chapters?: string[]; eids?: string[]; countryCodes?: string[]; }) => Promise; getProjectContextDetail: (params: { projectId: string; startDate?: string; endDate?: string; durationDays?: number; }) => Promise; getShiftPreviewDetail: (params: { projectId: string; newStartDate: Date; newEndDate: Date; }) => Promise; 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; resolveResourceIdentifier: ( ctx: ToolContext, identifier: string, ) => Promise; 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 { 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, }; }, }; }