refactor(api): extract assistant vacation entitlement slice
This commit is contained in:
@@ -151,6 +151,10 @@ import {
|
||||
createPlanningNavigationExecutors,
|
||||
planningNavigationToolDefinitions,
|
||||
} from "./assistant-tools/planning-navigation.js";
|
||||
import {
|
||||
createVacationEntitlementExecutors,
|
||||
vacationEntitlementToolDefinitions,
|
||||
} from "./assistant-tools/vacation-entitlements.js";
|
||||
import {
|
||||
withToolAccess,
|
||||
type ToolAccessRequirements,
|
||||
@@ -418,23 +422,11 @@ const CONTROLLER_ASSISTANT_ROLES = [
|
||||
SystemRole.CONTROLLER,
|
||||
] as const;
|
||||
|
||||
const MANAGER_ASSISTANT_ROLES = [
|
||||
SystemRole.ADMIN,
|
||||
SystemRole.MANAGER,
|
||||
] as const;
|
||||
|
||||
const ADMIN_ASSISTANT_ROLES = [SystemRole.ADMIN] as const;
|
||||
|
||||
const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequirements>> = {
|
||||
search_projects: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||
get_project: { allowedSystemRoles: [...CONTROLLER_ASSISTANT_ROLES] },
|
||||
update_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
create_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
approve_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
reject_vacation: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
|
||||
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
|
||||
@@ -2015,136 +2007,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
|
||||
...allocationPlanningMutationToolDefinitions,
|
||||
...resourceMutationToolDefinitions,
|
||||
...projectMutationToolDefinitions,
|
||||
|
||||
// ── VACATION MANAGEMENT ──
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_vacation",
|
||||
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["ANNUAL", "SICK", "OTHER"],
|
||||
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
|
||||
},
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
|
||||
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
|
||||
note: { type: "string", description: "Optional note" },
|
||||
},
|
||||
required: ["resourceId", "type", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "approve_vacation",
|
||||
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "reject_vacation",
|
||||
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
reason: { type: "string", description: "Rejection reason" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_vacation",
|
||||
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
vacationId: { type: "string", description: "Vacation ID" },
|
||||
},
|
||||
required: ["vacationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_pending_vacation_approvals",
|
||||
description: "List vacation requests awaiting approval.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "integer", description: "Max results. Default: 20" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_team_vacation_overlap",
|
||||
description: "Check if team members have overlapping vacations in a date range.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID to check overlap for" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
},
|
||||
required: ["resourceId", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ── ENTITLEMENT ──
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_entitlement_summary",
|
||||
description: "Get vacation entitlement year summary for all resources or a specific resource.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
year: { type: "integer", description: "Year. Default: current year" },
|
||||
resourceName: { type: "string", description: "Filter by resource name (optional)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_entitlement",
|
||||
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
|
||||
year: { type: "integer", description: "Year" },
|
||||
entitledDays: { type: "number", description: "Number of entitled vacation days" },
|
||||
},
|
||||
required: ["resourceId", "year", "entitledDays"],
|
||||
},
|
||||
},
|
||||
},
|
||||
...vacationEntitlementToolDefinitions,
|
||||
|
||||
// ── DEMAND / STAFFING ──
|
||||
...staffingDemandReadToolDefinitions,
|
||||
@@ -2360,230 +2223,18 @@ const executors = {
|
||||
createAuditLogCaller,
|
||||
createScopedCallerContext,
|
||||
}),
|
||||
|
||||
// ── VACATION MANAGEMENT ──
|
||||
|
||||
async create_vacation(params: {
|
||||
resourceId: string; type: string;
|
||||
startDate: string; endDate: string;
|
||||
isHalfDay?: boolean; halfDayPart?: string; note?: string;
|
||||
}, ctx: ToolContext) {
|
||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) return resource;
|
||||
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
const type = parseAssistantVacationRequestType(params.type);
|
||||
let vacation;
|
||||
try {
|
||||
vacation = await caller.create({
|
||||
resourceId: resource.id,
|
||||
type,
|
||||
startDate: parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: parseIsoDate(params.endDate, "endDate"),
|
||||
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
|
||||
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
|
||||
...(params.note !== undefined ? { note: params.note } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationCreationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const effectiveDays = "effectiveDays" in vacation && typeof vacation.effectiveDays === "number"
|
||||
? vacation.effectiveDays
|
||||
: null;
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["vacation"],
|
||||
success: true,
|
||||
message: `Created ${type} for ${resource.displayName}: ${params.startDate} to ${params.endDate} (status: ${vacation.status}${effectiveDays !== null ? `, deducted ${effectiveDays} day(s)` : ""})`,
|
||||
vacationId: vacation.id,
|
||||
vacation,
|
||||
};
|
||||
},
|
||||
|
||||
async approve_vacation(params: { vacationId: string }, ctx: ToolContext) {
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "approve");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let approved;
|
||||
try {
|
||||
approved = await caller.approve({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "approve");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["vacation"],
|
||||
success: true,
|
||||
warnings: approved.warnings,
|
||||
vacation: approved,
|
||||
message: `Approved vacation for ${existing.resource?.displayName ?? params.vacationId}`,
|
||||
};
|
||||
},
|
||||
|
||||
async reject_vacation(params: { vacationId: string; reason?: string }, ctx: ToolContext) {
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "reject");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let rejected;
|
||||
try {
|
||||
rejected = await caller.reject({
|
||||
id: params.vacationId,
|
||||
...(params.reason !== undefined ? { rejectionReason: params.reason } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "reject");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["vacation"],
|
||||
success: true,
|
||||
vacation: rejected,
|
||||
message: `Rejected vacation for ${existing.resource?.displayName ?? params.vacationId}${params.reason ? `: ${params.reason}` : ""}`,
|
||||
};
|
||||
},
|
||||
|
||||
async cancel_vacation(params: { vacationId: string }, ctx: ToolContext) {
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "cancel");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let cancelled;
|
||||
try {
|
||||
cancelled = await caller.cancel({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = toAssistantVacationMutationError(error, "cancel");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["vacation"],
|
||||
success: true,
|
||||
vacation: cancelled,
|
||||
message: `Cancelled vacation for ${existing.resource?.displayName ?? params.vacationId}`,
|
||||
};
|
||||
},
|
||||
|
||||
async get_pending_vacation_approvals(params: { limit?: number }, ctx: ToolContext) {
|
||||
const limit = Math.min(params.limit ?? 20, 50);
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
const vacations = await caller.getPendingApprovals();
|
||||
return vacations.map((v) => ({
|
||||
id: v.id,
|
||||
resource: v.resource.displayName,
|
||||
eid: v.resource.eid,
|
||||
chapter: v.resource.chapter,
|
||||
type: v.type,
|
||||
start: fmtDate(v.startDate),
|
||||
end: fmtDate(v.endDate),
|
||||
isHalfDay: v.isHalfDay,
|
||||
})).slice(0, limit);
|
||||
},
|
||||
|
||||
async get_team_vacation_overlap(params: {
|
||||
resourceId: string; startDate: string; endDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = createVacationCaller(createScopedCallerContext(ctx));
|
||||
return caller.getTeamOverlapDetail({
|
||||
resourceId: resource.id,
|
||||
startDate: parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: parseIsoDate(params.endDate, "endDate"),
|
||||
});
|
||||
},
|
||||
|
||||
// ── ENTITLEMENT ──
|
||||
|
||||
async get_entitlement_summary(params: { year?: number; resourceName?: string }, ctx: ToolContext) {
|
||||
const year = params.year ?? new Date().getFullYear();
|
||||
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
|
||||
return caller.getYearSummaryDetail({
|
||||
year,
|
||||
...(params.resourceName ? { resourceName: params.resourceName } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async set_entitlement(params: {
|
||||
resourceId: string; year: number; entitledDays: number; carryoverDays?: number;
|
||||
}, ctx: ToolContext) {
|
||||
if (params.carryoverDays !== undefined) {
|
||||
return {
|
||||
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
|
||||
};
|
||||
}
|
||||
|
||||
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) return resource;
|
||||
|
||||
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
|
||||
let entitlement;
|
||||
try {
|
||||
entitlement = await caller.set({
|
||||
resourceId: resource.id,
|
||||
year: params.year,
|
||||
entitledDays: params.entitledDays,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = toAssistantEntitlementMutationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["vacation"],
|
||||
success: true,
|
||||
entitlement,
|
||||
message: `Set entitlement for ${resource.displayName} (${params.year}): ${params.entitledDays} days`,
|
||||
};
|
||||
},
|
||||
...createVacationEntitlementExecutors({
|
||||
createVacationCaller,
|
||||
createEntitlementCaller,
|
||||
createScopedCallerContext,
|
||||
resolveResourceIdentifier,
|
||||
parseIsoDate,
|
||||
fmtDate,
|
||||
parseAssistantVacationRequestType,
|
||||
toAssistantVacationCreationError,
|
||||
toAssistantVacationMutationError,
|
||||
toAssistantEntitlementMutationError,
|
||||
}),
|
||||
|
||||
// ── ESTIMATES ──
|
||||
...createEstimateExecutors({
|
||||
|
||||
Reference in New Issue
Block a user