refactor(api): extract assistant vacation entitlement slice

This commit is contained in:
2026-03-30 23:09:32 +02:00
parent 45c25b17c1
commit bcfb18393e
3 changed files with 515 additions and 367 deletions
+17 -366
View File
@@ -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({