refactor(api): extract assistant import export dispo slice

This commit is contained in:
2026-03-30 22:45:00 +02:00
parent 4d8c91d705
commit aed99cb894
3 changed files with 628 additions and 479 deletions
+13 -478
View File
@@ -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<Record<string, ToolAccessRequiremen
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
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] },
};
const ASSISTANT_VACATION_REQUEST_TYPES = [
@@ -2328,252 +2314,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...scenarioRateAnalysisToolDefinitions,
...commentMutationToolDefinitions,
...auditHistoryToolDefinitions,
{
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 confirmation.",
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"],
},
},
},
...importExportDispoToolDefinitions,
...settingsAdminToolDefinitions,
], LEGACY_MONOLITHIC_TOOL_ACCESS);
@@ -3206,219 +2947,13 @@ const executors = {
toAssistantNotificationCreationError,
}),
async export_resources_csv(_params: Record<string, never>, 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<string, never>, 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<Record<string, string>>;
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,
@@ -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<string>;
exportProjectsCSV: () => Promise<string>;
importCSV: (params: {
entityType: "resources" | "projects" | "allocations";
rows: Array<Record<string, string>>;
dryRun: boolean;
}) => Promise<unknown>;
};
createDispoCaller: (ctx: TRPCContext) => {
listImportBatches: (params: {
status?: ImportBatchStatus;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
getImportBatch: (params: { id: string }) => Promise<unknown>;
stageImportBatch: (params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}) => Promise<unknown>;
validateImportBatch: (params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
importBatchId?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}) => Promise<unknown>;
cancelImportBatch: (params: { id: string }) => Promise<unknown>;
listStagedResources: (params: {
importBatchId: string;
status?: StagedRecordStatus;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
listStagedProjects: (params: {
importBatchId: string;
status?: StagedRecordStatus;
isTbd?: boolean;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
listStagedAssignments: (params: {
importBatchId: string;
status?: StagedRecordStatus;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
listStagedVacations: (params: {
importBatchId: string;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
listStagedUnresolvedRecords: (params: {
importBatchId: string;
recordType?: DispoStagedRecordType;
limit?: number;
cursor?: string;
}) => Promise<unknown>;
resolveStagedRecord: (params: {
action: "APPROVE" | "REJECT" | "SKIP";
id: string;
recordType: DispoStagedRecordType;
}) => Promise<unknown>;
commitImportBatch: (params: {
importBatchId: string;
allowTbdUnresolved?: boolean;
importTbdProjects?: boolean;
}) => Promise<unknown>;
};
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<string, ToolExecutor> {
return {
async export_resources_csv(_params: Record<string, never>, 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<string, never>, 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<Record<string, string>>;
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 } : {}),
});
},
};
}