refactor(api): extract assistant staffing demand slice
This commit is contained in:
@@ -113,6 +113,11 @@ import {
|
||||
projectMutationToolDefinitions,
|
||||
projectReadToolDefinitions,
|
||||
} from "./assistant-tools/projects.js";
|
||||
import {
|
||||
createStaffingDemandExecutors,
|
||||
staffingDemandMutationToolDefinitions,
|
||||
staffingDemandReadToolDefinitions,
|
||||
} from "./assistant-tools/staffing-demand.js";
|
||||
import {
|
||||
withToolAccess,
|
||||
type ToolAccessRequirements,
|
||||
@@ -404,15 +409,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
|
||||
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
list_demands: { requiresPlanningRead: true },
|
||||
create_demand: { requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS] },
|
||||
fill_demand: { requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS] },
|
||||
check_resource_availability: { requiresPlanningRead: true },
|
||||
get_staffing_suggestions: {
|
||||
requiresPlanningRead: true,
|
||||
requiresCostView: true,
|
||||
},
|
||||
find_capacity: { requiresPlanningRead: true },
|
||||
list_blueprints: { requiresPlanningRead: true },
|
||||
get_blueprint: { requiresPlanningRead: true },
|
||||
list_rate_cards: {
|
||||
@@ -2367,108 +2363,8 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
||||
},
|
||||
|
||||
// ── DEMAND / STAFFING ──
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_demands",
|
||||
description: "List staffing demand requirements for projects. Shows open positions that need to be filled.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Filter by project ID or short code" },
|
||||
status: { type: "string", description: "Filter by status: OPEN, PARTIALLY_FILLED, FILLED, CANCELLED" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_demand",
|
||||
description: "Create a staffing demand requirement on a project. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role name for the demand" },
|
||||
headcount: { type: "integer", description: "Number of people needed. Default: 1" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day required" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["projectId", "roleName", "hoursPerDay", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "fill_demand",
|
||||
description: "Fill/assign a resource to an open demand requirement. Requires manageAllocations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
demandId: { type: "string", description: "Demand requirement ID" },
|
||||
resourceId: { type: "string", description: "Resource ID or name to assign" },
|
||||
},
|
||||
required: ["demandId", "resourceId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "check_resource_availability",
|
||||
description: "Check if a resource is available in a given date range (no conflicting allocations or vacations).",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_staffing_suggestions",
|
||||
description: "Get AI-powered staffing suggestions for a project based on required skills, availability, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
roleName: { type: "string", description: "Role to find candidates for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
limit: { type: "integer", description: "Max suggestions. Default: 5" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "find_capacity",
|
||||
description: "Find resources with available capacity in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
minHoursPerDay: { type: "number", description: "Minimum available hours/day. Default: 4" },
|
||||
roleName: { type: "string", description: "Filter by role name" },
|
||||
chapter: { type: "string", description: "Filter by chapter" },
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
...staffingDemandReadToolDefinitions,
|
||||
...staffingDemandMutationToolDefinitions,
|
||||
|
||||
// ── BLUEPRINT ──
|
||||
{
|
||||
@@ -3144,6 +3040,21 @@ const executors = {
|
||||
toAssistantProjectCreationError,
|
||||
toAssistantProjectNotFoundError,
|
||||
}),
|
||||
...createStaffingDemandExecutors({
|
||||
assertPermission,
|
||||
createAllocationCaller,
|
||||
createStaffingCaller,
|
||||
createRoleCaller,
|
||||
createScopedCallerContext,
|
||||
resolveProjectIdentifier,
|
||||
resolveResourceIdentifier,
|
||||
resolveEntityOrAssistantError,
|
||||
parseIsoDate,
|
||||
parseOptionalIsoDate,
|
||||
fmtDate,
|
||||
toAssistantDemandCreationError,
|
||||
toAssistantDemandFillError,
|
||||
}),
|
||||
|
||||
...createAdvancedTimelineExecutors({
|
||||
assertPermission,
|
||||
@@ -3736,169 +3647,6 @@ const executors = {
|
||||
};
|
||||
},
|
||||
|
||||
// ── DEMAND / STAFFING ──
|
||||
|
||||
async list_demands(params: { projectId?: string; status?: string; limit?: number }, ctx: ToolContext) {
|
||||
const limit = Math.min(params.limit ?? 30, 50);
|
||||
const caller = createAllocationCaller(createScopedCallerContext(ctx));
|
||||
const resolvedProject = params.projectId
|
||||
? await resolveProjectIdentifier(ctx, params.projectId)
|
||||
: null;
|
||||
if (resolvedProject && "error" in resolvedProject) {
|
||||
return resolvedProject;
|
||||
}
|
||||
const demands = await caller.listDemands({
|
||||
...(resolvedProject ? { projectId: resolvedProject.id } : {}),
|
||||
...(params.status ? { status: params.status as AllocationStatus } : {}),
|
||||
});
|
||||
|
||||
return demands.map((d) => ({
|
||||
id: d.id,
|
||||
project: d.project.name,
|
||||
projectCode: d.project.shortCode,
|
||||
role: d.roleEntity?.name ?? d.role ?? "Unspecified",
|
||||
status: d.status,
|
||||
headcount: d.headcount,
|
||||
filled: d.assignments.length,
|
||||
remaining: d.headcount - d.assignments.length,
|
||||
hoursPerDay: d.hoursPerDay,
|
||||
start: fmtDate(d.startDate),
|
||||
end: fmtDate(d.endDate),
|
||||
})).slice(0, limit);
|
||||
},
|
||||
|
||||
async create_demand(params: {
|
||||
projectId: string; roleName: string; headcount?: number;
|
||||
hoursPerDay: number; startDate: string; endDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
assertPermission(ctx, "manageAllocations" as PermissionKey);
|
||||
const roleCaller = createRoleCaller(createScopedCallerContext(ctx));
|
||||
const [project, role] = await Promise.all([
|
||||
resolveProjectIdentifier(ctx, params.projectId),
|
||||
resolveEntityOrAssistantError(
|
||||
() => roleCaller.resolveByIdentifier({ identifier: params.roleName }),
|
||||
`Role not found: ${params.roleName}`,
|
||||
),
|
||||
]);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
if ("error" in role) {
|
||||
return role;
|
||||
}
|
||||
|
||||
const caller = createAllocationCaller(createScopedCallerContext(ctx));
|
||||
let demand;
|
||||
try {
|
||||
demand = await caller.createDemand({
|
||||
projectId: project.id,
|
||||
roleId: role.id,
|
||||
role: role.name,
|
||||
headcount: params.headcount ?? 1,
|
||||
hoursPerDay: params.hoursPerDay,
|
||||
startDate: parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: parseIsoDate(params.endDate, "endDate"),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantDemandCreationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation"],
|
||||
success: true,
|
||||
message: `Created demand: ${role.name} × ${params.headcount ?? 1} for ${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
|
||||
demandId: demand.id,
|
||||
};
|
||||
},
|
||||
|
||||
async fill_demand(params: { demandId: string; resourceId: string }, ctx: ToolContext) {
|
||||
assertPermission(ctx, "manageAllocations" as PermissionKey);
|
||||
const allocationCaller = createAllocationCaller(createScopedCallerContext(ctx));
|
||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) return resource;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await allocationCaller.assignResourceToDemand({
|
||||
demandRequirementId: params.demandId,
|
||||
resourceId: resource.id,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantDemandFillError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const roleName = result.demandRequirement.roleEntity?.name ?? result.demandRequirement.role ?? null;
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline"],
|
||||
success: true,
|
||||
message: `Assigned ${resource.displayName} to ${roleName ?? "demand"} on ${result.demandRequirement.project.name} (${result.demandRequirement.project.shortCode})`,
|
||||
assignmentId: result.assignment.id,
|
||||
};
|
||||
},
|
||||
|
||||
async check_resource_availability(params: {
|
||||
resourceId: string; startDate: string; endDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = createAllocationCaller(createScopedCallerContext(ctx));
|
||||
return caller.getResourceAvailabilitySummary({
|
||||
resourceId: resource.id,
|
||||
startDate: parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: parseIsoDate(params.endDate, "endDate"),
|
||||
});
|
||||
},
|
||||
|
||||
async get_staffing_suggestions(params: {
|
||||
projectId: string; roleName?: string;
|
||||
startDate?: string; endDate?: string; limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const project = await resolveProjectIdentifier(ctx, params.projectId);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = createStaffingCaller(createScopedCallerContext(ctx));
|
||||
const startDate = parseOptionalIsoDate(params.startDate, "startDate");
|
||||
const endDate = parseOptionalIsoDate(params.endDate, "endDate");
|
||||
return caller.getProjectStaffingSuggestions({
|
||||
projectId: project.id,
|
||||
...(params.roleName ? { roleName: params.roleName } : {}),
|
||||
...(startDate ? { startDate } : {}),
|
||||
...(endDate ? { endDate } : {}),
|
||||
...(params.limit ? { limit: params.limit } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async find_capacity(params: {
|
||||
startDate: string; endDate: string;
|
||||
minHoursPerDay?: number; roleName?: string;
|
||||
chapter?: string; limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = createStaffingCaller(createScopedCallerContext(ctx));
|
||||
return caller.searchCapacity({
|
||||
startDate: parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: parseIsoDate(params.endDate, "endDate"),
|
||||
minHoursPerDay: params.minHoursPerDay ?? 4,
|
||||
...(params.roleName ? { roleName: params.roleName } : {}),
|
||||
...(params.chapter ? { chapter: params.chapter } : {}),
|
||||
...(params.limit ? { limit: params.limit } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
// ── BLUEPRINT ──
|
||||
|
||||
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
|
||||
|
||||
Reference in New Issue
Block a user