diff --git a/docs/architecture-hardening-backlog.md b/docs/architecture-hardening-backlog.md index 1a6fc43..a31817a 100644 --- a/docs/architecture-hardening-backlog.md +++ b/docs/architecture-hardening-backlog.md @@ -43,12 +43,13 @@ - the scenario simulation, project narrative, and rate lookup assistant helpers now live in their own domain module, keeping the remaining controller-side scenario/AI analytics wiring 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 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 ## Next Up 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. -The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining import/export or staged-dispo helpers 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 navigation/search helpers or other small read-only assistant clusters still living in the monolithic router. ## Remaining Major Themes diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index 992e10f..99af4f6 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -3,7 +3,7 @@ * Each tool has a JSON schema (for the AI) and an execute function (for the server). */ -import { Prisma, ImportBatchStatus, StagedRecordStatus, DispoStagedRecordType, VacationType } from "@capakraken/db"; +import { Prisma, VacationType } from "@capakraken/db"; import { CreateAssignmentSchema, AllocationStatus, @@ -131,6 +131,10 @@ import { createScenarioRateAnalysisExecutors, scenarioRateAnalysisToolDefinitions, } from "./assistant-tools/scenario-rate-analysis.js"; +import { + createImportExportDispoExecutors, + importExportDispoToolDefinitions, +} from "./assistant-tools/import-export-dispo.js"; import { commentMutationToolDefinitions, commentReadToolDefinitions, @@ -430,24 +434,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial, ctx: ToolContext) { - const caller = createImportExportCaller(createScopedCallerContext(ctx)); - const csv = await caller.exportResourcesCSV(); - return { - format: "csv", - lineCount: csv.length === 0 ? 0 : csv.split("\n").length, - csv, - }; - }, - - async export_projects_csv(_params: Record, ctx: ToolContext) { - const caller = createImportExportCaller(createScopedCallerContext(ctx)); - const csv = await caller.exportProjectsCSV(); - return { - format: "csv", - lineCount: csv.length === 0 ? 0 : csv.split("\n").length, - csv, - }; - }, - - async import_csv_data(params: { - entityType: "resources" | "projects" | "allocations"; - rows: Array>; - dryRun?: boolean; - }, ctx: ToolContext) { - assertPermission(ctx, PermissionKey.IMPORT_DATA); - const caller = createImportExportCaller(createScopedCallerContext(ctx)); - return caller.importCSV({ - entityType: params.entityType, - rows: params.rows, - dryRun: params.dryRun ?? true, - }); - }, - - async list_dispo_import_batches(params: { - status?: ImportBatchStatus; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listImportBatches({ - ...(params.status ? { status: params.status } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async get_dispo_import_batch(params: { - id: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - try { - return await caller.getImportBatch({ id: params.id }); - } catch (error) { - const mapped = toAssistantDispoImportBatchNotFoundError(error); - if (mapped) { - return mapped; - } - throw error; - } - }, - - async stage_dispo_import_batch(params: { - chargeabilityWorkbookPath: string; - costWorkbookPath?: string; - notes?: string | null; - planningWorkbookPath: string; - referenceWorkbookPath: string; - rosterWorkbookPath?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.stageImportBatch({ - chargeabilityWorkbookPath: params.chargeabilityWorkbookPath, - planningWorkbookPath: params.planningWorkbookPath, - referenceWorkbookPath: params.referenceWorkbookPath, - ...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}), - ...(params.notes !== undefined ? { notes: params.notes } : {}), - ...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}), - }); - }, - - async validate_dispo_import_batch(params: { - chargeabilityWorkbookPath: string; - costWorkbookPath?: string; - importBatchId?: string; - notes?: string | null; - planningWorkbookPath: string; - referenceWorkbookPath: string; - rosterWorkbookPath?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.validateImportBatch({ - chargeabilityWorkbookPath: params.chargeabilityWorkbookPath, - planningWorkbookPath: params.planningWorkbookPath, - referenceWorkbookPath: params.referenceWorkbookPath, - ...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}), - ...(params.importBatchId !== undefined ? { importBatchId: params.importBatchId } : {}), - ...(params.notes !== undefined ? { notes: params.notes } : {}), - ...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}), - }); - }, - - async cancel_dispo_import_batch(params: { - id: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.cancelImportBatch({ id: params.id }); - }, - - async list_dispo_staged_resources(params: { - importBatchId: string; - status?: StagedRecordStatus; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listStagedResources({ - importBatchId: params.importBatchId, - ...(params.status !== undefined ? { status: params.status } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async list_dispo_staged_projects(params: { - importBatchId: string; - status?: StagedRecordStatus; - isTbd?: boolean; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listStagedProjects({ - importBatchId: params.importBatchId, - ...(params.status !== undefined ? { status: params.status } : {}), - ...(params.isTbd !== undefined ? { isTbd: params.isTbd } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async list_dispo_staged_assignments(params: { - importBatchId: string; - status?: StagedRecordStatus; - resourceExternalId?: string; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listStagedAssignments({ - importBatchId: params.importBatchId, - ...(params.status !== undefined ? { status: params.status } : {}), - ...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async list_dispo_staged_vacations(params: { - importBatchId: string; - resourceExternalId?: string; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listStagedVacations({ - importBatchId: params.importBatchId, - ...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async list_dispo_staged_unresolved_records(params: { - importBatchId: string; - recordType?: DispoStagedRecordType; - limit?: number; - cursor?: string; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.listStagedUnresolvedRecords({ - importBatchId: params.importBatchId, - ...(params.recordType !== undefined ? { recordType: params.recordType } : {}), - ...(params.cursor ? { cursor: params.cursor } : {}), - ...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}), - }); - }, - - async resolve_dispo_staged_record(params: { - action: "APPROVE" | "REJECT" | "SKIP"; - id: string; - recordType: DispoStagedRecordType; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.resolveStagedRecord({ - action: params.action, - id: params.id, - recordType: params.recordType, - }); - }, - - async commit_dispo_import_batch(params: { - allowTbdUnresolved?: boolean; - importBatchId: string; - importTbdProjects?: boolean; - }, ctx: ToolContext) { - const caller = createDispoCaller(createScopedCallerContext(ctx)); - return caller.commitImportBatch({ - importBatchId: params.importBatchId, - ...(params.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: params.allowTbdUnresolved } : {}), - ...(params.importTbdProjects !== undefined ? { importTbdProjects: params.importTbdProjects } : {}), - }); - }, + ...createImportExportDispoExecutors({ + assertPermission, + createImportExportCaller, + createDispoCaller, + createScopedCallerContext, + toAssistantDispoImportBatchNotFoundError, + }), ...createSettingsAdminExecutors({ createSettingsCaller, diff --git a/packages/api/src/router/assistant-tools/import-export-dispo.ts b/packages/api/src/router/assistant-tools/import-export-dispo.ts new file mode 100644 index 0000000..1d1edbe --- /dev/null +++ b/packages/api/src/router/assistant-tools/import-export-dispo.ts @@ -0,0 +1,613 @@ +import { DispoStagedRecordType, ImportBatchStatus, StagedRecordStatus } from "@capakraken/db"; +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import type { TRPCContext } from "../../trpc.js"; +import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js"; + +type ImportExportDispoDeps = { + assertPermission: (ctx: ToolContext, perm: PermissionKey) => void; + createImportExportCaller: (ctx: TRPCContext) => { + exportResourcesCSV: () => Promise; + exportProjectsCSV: () => Promise; + importCSV: (params: { + entityType: "resources" | "projects" | "allocations"; + rows: Array>; + dryRun: boolean; + }) => Promise; + }; + createDispoCaller: (ctx: TRPCContext) => { + listImportBatches: (params: { + status?: ImportBatchStatus; + limit?: number; + cursor?: string; + }) => Promise; + getImportBatch: (params: { id: string }) => Promise; + stageImportBatch: (params: { + chargeabilityWorkbookPath: string; + costWorkbookPath?: string; + notes?: string | null; + planningWorkbookPath: string; + referenceWorkbookPath: string; + rosterWorkbookPath?: string; + }) => Promise; + validateImportBatch: (params: { + chargeabilityWorkbookPath: string; + costWorkbookPath?: string; + importBatchId?: string; + notes?: string | null; + planningWorkbookPath: string; + referenceWorkbookPath: string; + rosterWorkbookPath?: string; + }) => Promise; + cancelImportBatch: (params: { id: string }) => Promise; + listStagedResources: (params: { + importBatchId: string; + status?: StagedRecordStatus; + limit?: number; + cursor?: string; + }) => Promise; + listStagedProjects: (params: { + importBatchId: string; + status?: StagedRecordStatus; + isTbd?: boolean; + limit?: number; + cursor?: string; + }) => Promise; + listStagedAssignments: (params: { + importBatchId: string; + status?: StagedRecordStatus; + resourceExternalId?: string; + limit?: number; + cursor?: string; + }) => Promise; + listStagedVacations: (params: { + importBatchId: string; + resourceExternalId?: string; + limit?: number; + cursor?: string; + }) => Promise; + listStagedUnresolvedRecords: (params: { + importBatchId: string; + recordType?: DispoStagedRecordType; + limit?: number; + cursor?: string; + }) => Promise; + resolveStagedRecord: (params: { + action: "APPROVE" | "REJECT" | "SKIP"; + id: string; + recordType: DispoStagedRecordType; + }) => Promise; + commitImportBatch: (params: { + importBatchId: string; + allowTbdUnresolved?: boolean; + importTbdProjects?: boolean; + }) => Promise; + }; + createScopedCallerContext: (ctx: ToolContext) => TRPCContext; + toAssistantDispoImportBatchNotFoundError: (error: unknown) => { error: string } | null; +}; + +const CONTROLLER_ASSISTANT_ROLES = [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER] as const; +const MANAGER_ASSISTANT_ROLES = [SystemRole.ADMIN, SystemRole.MANAGER] as const; +const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const; + +export const importExportDispoToolDefinitions: ToolDef[] = withToolAccess([ + { + type: "function", + function: { + name: "export_resources_csv", + description: "Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.", + parameters: { + type: "object", + properties: {}, + }, + }, + }, + { + type: "function", + function: { + name: "export_projects_csv", + description: "Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.", + parameters: { + type: "object", + properties: {}, + }, + }, + }, + { + type: "function", + function: { + name: "import_csv_data", + description: "Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and defaults to dry-run.", + parameters: { + type: "object", + properties: { + entityType: { type: "string", enum: ["resources", "projects", "allocations"], description: "Import target entity type." }, + rows: { + type: "array", + description: "CSV rows already parsed to key/value objects.", + items: { + type: "object", + additionalProperties: { type: "string" }, + }, + }, + dryRun: { type: "boolean", description: "Validate only without persisting changes. Default: true." }, + }, + required: ["entityType", "rows"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_import_batches", + description: "List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + status: { type: "string", description: "Optional batch status filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + }, + }, + }, + { + type: "function", + function: { + name: "get_dispo_import_batch", + description: "Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + id: { type: "string", description: "Import batch ID." }, + }, + required: ["id"], + }, + }, + }, + { + type: "function", + function: { + name: "stage_dispo_import_batch", + description: "Stage a Dispo import batch via the real dispo router. Admin role required. Always confirm first.", + parameters: { + type: "object", + properties: { + planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." }, + referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." }, + chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." }, + costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." }, + rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." }, + notes: { type: "string", description: "Optional import notes." }, + }, + required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"], + }, + }, + }, + { + type: "function", + function: { + name: "validate_dispo_import_batch", + description: "Validate a Dispo import batch readiness check via the real dispo router without committing anything. Admin role required.", + parameters: { + type: "object", + properties: { + planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." }, + referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." }, + chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." }, + costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." }, + rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." }, + importBatchId: { type: "string", description: "Optional existing staged import batch ID." }, + notes: { type: "string", description: "Optional import notes." }, + }, + required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"], + }, + }, + }, + { + type: "function", + function: { + name: "cancel_dispo_import_batch", + description: "Cancel a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.", + parameters: { + type: "object", + properties: { + id: { type: "string", description: "Import batch ID." }, + }, + required: ["id"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_staged_resources", + description: "List staged Dispo resources for one import batch via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + status: { type: "string", description: "Optional staged record status filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + required: ["importBatchId"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_staged_projects", + description: "List staged Dispo projects for one import batch via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + status: { type: "string", description: "Optional staged record status filter." }, + isTbd: { type: "boolean", description: "Optional TBD-project filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + required: ["importBatchId"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_staged_assignments", + description: "List staged Dispo assignments for one import batch via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + status: { type: "string", description: "Optional staged record status filter." }, + resourceExternalId: { type: "string", description: "Optional resource external ID filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + required: ["importBatchId"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_staged_vacations", + description: "List staged Dispo vacations for one import batch via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + resourceExternalId: { type: "string", description: "Optional resource external ID filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + required: ["importBatchId"], + }, + }, + }, + { + type: "function", + function: { + name: "list_dispo_staged_unresolved_records", + description: "List staged unresolved Dispo records for one import batch via the real dispo router. Admin role required.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + recordType: { type: "string", description: "Optional unresolved record type filter." }, + limit: { type: "integer", description: "Max results. Default: 50, max: 200." }, + cursor: { type: "string", description: "Optional pagination cursor." }, + }, + required: ["importBatchId"], + }, + }, + }, + { + type: "function", + function: { + name: "resolve_dispo_staged_record", + description: "Resolve one staged Dispo record via the real dispo router. Admin role required. Always confirm first.", + parameters: { + type: "object", + properties: { + id: { type: "string", description: "Staged record ID." }, + recordType: { type: "string", description: "Staged record type." }, + action: { type: "string", enum: ["APPROVE", "REJECT", "SKIP"], description: "Resolution action." }, + }, + required: ["id", "recordType", "action"], + }, + }, + }, + { + type: "function", + function: { + name: "commit_dispo_import_batch", + description: "Commit a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.", + parameters: { + type: "object", + properties: { + importBatchId: { type: "string", description: "Import batch ID." }, + allowTbdUnresolved: { type: "boolean", description: "Allow unresolved TBD projects during commit." }, + importTbdProjects: { type: "boolean", description: "Whether TBD projects should be imported." }, + }, + required: ["importBatchId"], + }, + }, + }, +], { + export_resources_csv: { + allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES], + }, + export_projects_csv: { + allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES], + }, + import_csv_data: { + requiredPermissions: [PermissionKey.IMPORT_DATA], + allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES], + }, + list_dispo_import_batches: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + get_dispo_import_batch: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + stage_dispo_import_batch: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + validate_dispo_import_batch: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + cancel_dispo_import_batch: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + list_dispo_staged_resources: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + list_dispo_staged_projects: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + list_dispo_staged_assignments: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + list_dispo_staged_vacations: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + list_dispo_staged_unresolved_records: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + resolve_dispo_staged_record: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, + commit_dispo_import_batch: { + allowedSystemRoles: [...ADMIN_ASSISTANT_ROLES], + }, +}); + +function clampLimit(limit: number | undefined): number | undefined { + return limit !== undefined ? Math.min(Math.max(limit, 1), 200) : undefined; +} + +export function createImportExportDispoExecutors( + deps: ImportExportDispoDeps, +): Record { + return { + async export_resources_csv(_params: Record, ctx: ToolContext) { + const caller = deps.createImportExportCaller(deps.createScopedCallerContext(ctx)); + const csv = await caller.exportResourcesCSV(); + return { + format: "csv", + lineCount: csv.length === 0 ? 0 : csv.split("\n").length, + csv, + }; + }, + + async export_projects_csv(_params: Record, ctx: ToolContext) { + const caller = deps.createImportExportCaller(deps.createScopedCallerContext(ctx)); + const csv = await caller.exportProjectsCSV(); + return { + format: "csv", + lineCount: csv.length === 0 ? 0 : csv.split("\n").length, + csv, + }; + }, + + async import_csv_data( + params: { + entityType: "resources" | "projects" | "allocations"; + rows: Array>; + dryRun?: boolean; + }, + ctx: ToolContext, + ) { + deps.assertPermission(ctx, PermissionKey.IMPORT_DATA); + const caller = deps.createImportExportCaller(deps.createScopedCallerContext(ctx)); + return caller.importCSV({ + entityType: params.entityType, + rows: params.rows, + dryRun: params.dryRun ?? true, + }); + }, + + async list_dispo_import_batches( + params: { status?: ImportBatchStatus; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listImportBatches({ + ...(params.status ? { status: params.status } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async get_dispo_import_batch( + params: { id: string }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + try { + return await caller.getImportBatch({ id: params.id }); + } catch (error) { + const mapped = deps.toAssistantDispoImportBatchNotFoundError(error); + if (mapped) { + return mapped; + } + throw error; + } + }, + + async stage_dispo_import_batch( + params: { + chargeabilityWorkbookPath: string; + costWorkbookPath?: string; + notes?: string | null; + planningWorkbookPath: string; + referenceWorkbookPath: string; + rosterWorkbookPath?: string; + }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.stageImportBatch({ + chargeabilityWorkbookPath: params.chargeabilityWorkbookPath, + planningWorkbookPath: params.planningWorkbookPath, + referenceWorkbookPath: params.referenceWorkbookPath, + ...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}), + ...(params.notes !== undefined ? { notes: params.notes } : {}), + ...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}), + }); + }, + + async validate_dispo_import_batch( + params: { + chargeabilityWorkbookPath: string; + costWorkbookPath?: string; + importBatchId?: string; + notes?: string | null; + planningWorkbookPath: string; + referenceWorkbookPath: string; + rosterWorkbookPath?: string; + }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.validateImportBatch({ + chargeabilityWorkbookPath: params.chargeabilityWorkbookPath, + planningWorkbookPath: params.planningWorkbookPath, + referenceWorkbookPath: params.referenceWorkbookPath, + ...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}), + ...(params.importBatchId !== undefined ? { importBatchId: params.importBatchId } : {}), + ...(params.notes !== undefined ? { notes: params.notes } : {}), + ...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}), + }); + }, + + async cancel_dispo_import_batch( + params: { id: string }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.cancelImportBatch({ id: params.id }); + }, + + async list_dispo_staged_resources( + params: { importBatchId: string; status?: StagedRecordStatus; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listStagedResources({ + importBatchId: params.importBatchId, + ...(params.status !== undefined ? { status: params.status } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async list_dispo_staged_projects( + params: { importBatchId: string; status?: StagedRecordStatus; isTbd?: boolean; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listStagedProjects({ + importBatchId: params.importBatchId, + ...(params.status !== undefined ? { status: params.status } : {}), + ...(params.isTbd !== undefined ? { isTbd: params.isTbd } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async list_dispo_staged_assignments( + params: { importBatchId: string; status?: StagedRecordStatus; resourceExternalId?: string; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listStagedAssignments({ + importBatchId: params.importBatchId, + ...(params.status !== undefined ? { status: params.status } : {}), + ...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async list_dispo_staged_vacations( + params: { importBatchId: string; resourceExternalId?: string; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listStagedVacations({ + importBatchId: params.importBatchId, + ...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async list_dispo_staged_unresolved_records( + params: { importBatchId: string; recordType?: DispoStagedRecordType; limit?: number; cursor?: string }, + ctx: ToolContext, + ) { + const limit = clampLimit(params.limit); + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.listStagedUnresolvedRecords({ + importBatchId: params.importBatchId, + ...(params.recordType !== undefined ? { recordType: params.recordType } : {}), + ...(params.cursor ? { cursor: params.cursor } : {}), + ...(limit !== undefined ? { limit } : {}), + }); + }, + + async resolve_dispo_staged_record( + params: { action: "APPROVE" | "REJECT" | "SKIP"; id: string; recordType: DispoStagedRecordType }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.resolveStagedRecord({ + action: params.action, + id: params.id, + recordType: params.recordType, + }); + }, + + async commit_dispo_import_batch( + params: { allowTbdUnresolved?: boolean; importBatchId: string; importTbdProjects?: boolean }, + ctx: ToolContext, + ) { + const caller = deps.createDispoCaller(deps.createScopedCallerContext(ctx)); + return caller.commitImportBatch({ + importBatchId: params.importBatchId, + ...(params.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: params.allowTbdUnresolved } : {}), + ...(params.importTbdProjects !== undefined ? { importTbdProjects: params.importTbdProjects } : {}), + }); + }, + }; +}