refactor(api): extract assistant vacation entitlement slice
This commit is contained in:
@@ -46,12 +46,13 @@
|
||||
- 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
|
||||
- the remaining estimate search, planning lookup, self-service timeline read, and navigation assistant helpers now live in their own domain module, keeping another mixed read-only cluster out of the monolithic assistant router without changing the assistant contract
|
||||
- the country listing and country detail assistant helpers now live in their own domain module, keeping the remaining geo/readmodel lookups out of the monolithic assistant router without changing the assistant contract
|
||||
- the remaining vacation workflow and entitlement assistant helpers now live in their own domain module, leaving `packages/api/src/router/assistant-tools.ts` as an aggregator/composition layer instead of the last mixed monolithic execution block
|
||||
|
||||
## 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 workflow helpers or other small assistant clusters still living in the monolithic router.
|
||||
That extraction work is now effectively complete for the current assistant-tool scope; the next structural cleanup should target oversized source routers or shared helper boundaries rather than another inline assistant-tool cluster.
|
||||
|
||||
## Remaining Major Themes
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
import { VacationType } from "@capakraken/db";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type ResolvedResource = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type VacationRecord = {
|
||||
id: string;
|
||||
status: string;
|
||||
effectiveDays?: number | null;
|
||||
warnings?: unknown;
|
||||
resource?: {
|
||||
displayName?: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PendingVacationApproval = {
|
||||
id: string;
|
||||
resource: {
|
||||
displayName: string;
|
||||
eid: string | null;
|
||||
chapter: string | null;
|
||||
};
|
||||
type: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
isHalfDay: boolean;
|
||||
};
|
||||
|
||||
type VacationEntitlementDeps = {
|
||||
createVacationCaller: (ctx: TRPCContext) => {
|
||||
create: (params: {
|
||||
resourceId: string;
|
||||
type: VacationType;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
isHalfDay?: boolean;
|
||||
halfDayPart?: "MORNING" | "AFTERNOON";
|
||||
note?: string;
|
||||
}) => Promise<VacationRecord>;
|
||||
getById: (params: { id: string }) => Promise<VacationRecord>;
|
||||
approve: (params: { id: string }) => Promise<VacationRecord>;
|
||||
reject: (params: { id: string; rejectionReason?: string }) => Promise<VacationRecord>;
|
||||
cancel: (params: { id: string }) => Promise<VacationRecord>;
|
||||
getPendingApprovals: () => Promise<PendingVacationApproval[]>;
|
||||
getTeamOverlapDetail: (params: {
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createEntitlementCaller: (ctx: TRPCContext) => {
|
||||
getYearSummaryDetail: (params: {
|
||||
year: number;
|
||||
resourceName?: string;
|
||||
}) => Promise<unknown>;
|
||||
set: (params: {
|
||||
resourceId: string;
|
||||
year: number;
|
||||
entitledDays: number;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveResourceIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
parseAssistantVacationRequestType: (input: string) => VacationType;
|
||||
toAssistantVacationCreationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantVacationMutationError: (
|
||||
error: unknown,
|
||||
action: "approve" | "reject" | "cancel",
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantEntitlementMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const vacationEntitlementToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
approve_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
reject_vacation: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_pending_vacation_approvals: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_entitlement_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
set_entitlement: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
});
|
||||
|
||||
export function createVacationEntitlementExecutors(
|
||||
deps: VacationEntitlementDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async create_vacation(
|
||||
params: {
|
||||
resourceId: string;
|
||||
type: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
isHalfDay?: boolean;
|
||||
halfDayPart?: string;
|
||||
note?: string;
|
||||
},
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
const type = deps.parseAssistantVacationRequestType(params.type);
|
||||
let vacation;
|
||||
try {
|
||||
vacation = await caller.create({
|
||||
resourceId: resource.id,
|
||||
type,
|
||||
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: deps.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 = deps.toAssistantVacationCreationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const effectiveDays = 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 = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantVacationMutationError(error, "approve");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let approved;
|
||||
try {
|
||||
approved = await caller.approve({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = deps.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 = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = deps.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 = deps.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 = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
let existing;
|
||||
try {
|
||||
existing = await caller.getById({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantVacationMutationError(error, "cancel");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let cancelled;
|
||||
try {
|
||||
cancelled = await caller.cancel({ id: params.vacationId });
|
||||
} catch (error) {
|
||||
const mapped = deps.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 = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
const vacations = await caller.getPendingApprovals();
|
||||
|
||||
return vacations.map((vacation) => ({
|
||||
id: vacation.id,
|
||||
resource: vacation.resource.displayName,
|
||||
eid: vacation.resource.eid,
|
||||
chapter: vacation.resource.chapter,
|
||||
type: vacation.type,
|
||||
start: deps.fmtDate(vacation.startDate),
|
||||
end: deps.fmtDate(vacation.endDate),
|
||||
isHalfDay: vacation.isHalfDay,
|
||||
})).slice(0, limit);
|
||||
},
|
||||
|
||||
async get_team_vacation_overlap(
|
||||
params: { resourceId: string; startDate: string; endDate: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getTeamOverlapDetail({
|
||||
resourceId: resource.id,
|
||||
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: deps.parseIsoDate(params.endDate, "endDate"),
|
||||
});
|
||||
},
|
||||
|
||||
async get_entitlement_summary(
|
||||
params: { year?: number; resourceName?: string },
|
||||
ctx: ToolContext,
|
||||
) {
|
||||
const year = params.year ?? new Date().getFullYear();
|
||||
const caller = deps.createEntitlementCaller(deps.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 deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = deps.createEntitlementCaller(deps.createScopedCallerContext(ctx));
|
||||
let entitlement;
|
||||
try {
|
||||
entitlement = await caller.set({
|
||||
resourceId: resource.id,
|
||||
year: params.year,
|
||||
entitledDays: params.entitledDays,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.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`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user