Files
CapaKraken/packages/api/src/router/assistant-tools/vacation-holidays.ts
T

718 lines
24 KiB
TypeScript

import type { TRPCContext } from "../../trpc.js";
import {
CreateHolidayCalendarEntrySchema,
CreateHolidayCalendarSchema,
PreviewResolvedHolidaysSchema,
UpdateHolidayCalendarEntrySchema,
UpdateHolidayCalendarSchema,
} from "@capakraken/shared";
import { z } from "zod";
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type ResolvedResource = {
id: string;
};
type HolidayCalendarEntryRecord = {
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
};
type HolidayCalendarRecord = {
id: string;
name: string;
scopeType: string;
stateCode?: string | null;
isActive?: boolean | null;
priority?: number | null;
country?: { id: string; code: string; name: string } | null;
metroCity?: { id: string; name: string } | null;
_count?: { entries?: number | null } | null;
entries?: HolidayCalendarEntryRecord[] | null;
};
type VacationHolidayDeps = {
createEntitlementCaller: (ctx: TRPCContext) => {
getBalanceDetail: (params: { resourceId: string; year: number }) => Promise<unknown>;
};
createVacationCaller: (ctx: TRPCContext) => {
list: (params: {
status: "APPROVED";
startDate: Date;
endDate: Date;
limit: number;
}) => Promise<Array<{
type: string;
startDate: Date;
endDate: Date;
isHalfDay?: boolean | null;
halfDayPart?: string | null;
resource: {
displayName: string;
eid: string;
chapter?: string | null;
};
}>>;
};
createHolidayCalendarCaller: (ctx: TRPCContext) => {
resolveHolidaysDetail: (params: {
periodStart: Date;
periodEnd: Date;
countryCode: string;
stateCode?: string;
metroCityName?: string;
}) => Promise<{
locationContext: unknown;
periodStart: string;
periodEnd: string;
count: number;
summary: unknown;
holidays: unknown[];
}>;
resolveResourceHolidaysDetail: (params: {
resourceId: string;
periodStart: Date;
periodEnd: Date;
}) => Promise<{
resource: unknown;
periodStart: string;
periodEnd: string;
count: number;
summary: unknown;
holidays: unknown[];
}>;
listCalendarsDetail: (params: {
includeInactive?: boolean;
countryCode?: string;
scopeType?: "COUNTRY" | "STATE" | "CITY";
stateCode?: string;
metroCity?: string;
}) => Promise<unknown>;
getCalendarByIdentifierDetail: (params: { identifier: string }) => Promise<unknown>;
previewResolvedHolidaysDetail: (
params: z.input<typeof PreviewResolvedHolidaysSchema>,
) => Promise<unknown>;
createCalendar: (
params: z.input<typeof CreateHolidayCalendarSchema>,
) => Promise<HolidayCalendarRecord>;
updateCalendar: (params: {
id: string;
data: z.input<typeof UpdateHolidayCalendarSchema>;
}) => Promise<HolidayCalendarRecord>;
deleteCalendar: (params: { id: string }) => Promise<{ name: string }>;
createEntry: (
params: z.input<typeof CreateHolidayCalendarEntrySchema>,
) => Promise<HolidayCalendarEntryRecord>;
updateEntry: (params: {
id: string;
data: z.input<typeof UpdateHolidayCalendarEntrySchema>;
}) => Promise<HolidayCalendarEntryRecord>;
deleteEntry: (params: { id: string }) => Promise<{ name: string }>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
resolveResourceIdentifier: (
ctx: ToolContext,
identifier: string,
) => Promise<ResolvedResource | AssistantToolErrorResult>;
resolveHolidayPeriod: (input: {
year?: number;
periodStart?: string;
periodEnd?: string;
}) => { year: number | null; periodStart: Date; periodEnd: Date };
resolveEntityOrAssistantError: <T>(
resolve: () => Promise<T>,
notFoundMessage: string,
) => Promise<T | AssistantToolErrorResult>;
assertAdminRole: (ctx: ToolContext) => void;
fmtDate: (value: Date | null | undefined) => string | null;
formatHolidayCalendar: (calendar: HolidayCalendarRecord) => unknown;
formatHolidayCalendarEntry: (entry: HolidayCalendarEntryRecord) => unknown;
toAssistantHolidayCalendarMutationError: (
error: unknown,
) => AssistantToolErrorResult | null;
toAssistantHolidayCalendarNotFoundError: (
error: unknown,
) => AssistantToolErrorResult | null;
toAssistantHolidayEntryMutationError: (
error: unknown,
) => AssistantToolErrorResult | null;
toAssistantHolidayEntryNotFoundError: (
error: unknown,
) => AssistantToolErrorResult | null;
};
export const vacationHolidayReadToolDefinitions: ToolDef[] = [
{
type: "function",
function: {
name: "get_vacation_balance",
description: "Get the holiday-aware vacation balance for a resource via the real entitlement workflow. Authenticated users can read their own balance; manager/admin/controller can read broader balances.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year. Default: current year" },
},
required: ["resourceId"],
},
},
},
{
type: "function",
function: {
name: "list_vacations_upcoming",
description: "List upcoming vacations across all resources, or for a specific resource/team. Shows who is off when.",
parameters: {
type: "object",
properties: {
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
chapter: { type: "string", description: "Filter by chapter/team" },
daysAhead: { type: "integer", description: "How many days ahead to look. Default: 30" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "list_holidays_by_region",
description: "List resolved public holidays for a country, federal state, and optionally a city in a given year or date range. Use this to compare regions such as Bayern vs Hamburg.",
parameters: {
type: "object",
properties: {
countryCode: { type: "string", description: "Country code such as DE, ES, US, IN." },
federalState: { type: "string", description: "Federal state / region code, e.g. BY, HH, NRW." },
metroCity: { type: "string", description: "Optional city name for local city-specific holidays, e.g. Augsburg." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["countryCode"],
},
},
},
{
type: "function",
function: {
name: "get_resource_holidays",
description: "List resolved public holidays for a specific resource based on that person's country, federal state, and city context.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, EID, or display name." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "list_holiday_calendars",
description: "List holiday calendars including scope, assignment, active flag, priority, and entry count. Useful to inspect the calendar-editor configuration context.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive calendars. Default: false." },
countryCode: { type: "string", description: "Optional country code filter such as DE or ES." },
scopeType: { type: "string", description: "Optional scope filter: COUNTRY, STATE, CITY." },
stateCode: { type: "string", description: "Optional state/region code filter such as BY or NRW." },
metroCity: { type: "string", description: "Optional city-name filter." },
},
},
},
},
{
type: "function",
function: {
name: "get_holiday_calendar",
description: "Get a single holiday calendar including all entries. Accepts either the calendar ID or its name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Holiday calendar ID or name." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "preview_resolved_holiday_calendar",
description: "Preview the resolved holiday result for a country/state/city scope and year, including which calendar each holiday comes from.",
parameters: {
type: "object",
properties: {
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Optional state/region code." },
metroCityId: { type: "string", description: "Optional metro city ID for city-specific preview." },
year: { type: "integer", description: "Full year, e.g. 2026." },
},
required: ["countryId", "year"],
},
},
},
];
export const vacationHolidayMutationToolDefinitions: ToolDef[] = [
{
type: "function",
function: {
name: "create_holiday_calendar",
description: "Create a holiday calendar for a country, state, or city scope. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Calendar name." },
scopeType: { type: "string", description: "COUNTRY, STATE, or CITY." },
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Required for STATE calendars." },
metroCityId: { type: "string", description: "Required for CITY calendars." },
isActive: { type: "boolean", description: "Whether the calendar is active. Default: true." },
priority: { type: "integer", description: "Priority used during calendar resolution. Default: 0." },
},
required: ["name", "scopeType", "countryId"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar",
description: "Update an existing holiday calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
stateCode: { type: "string" },
metroCityId: { type: "string" },
isActive: { type: "boolean" },
priority: { type: "integer" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar",
description: "Delete a holiday calendar and all of its entries. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_holiday_calendar_entry",
description: "Create a holiday entry in an existing calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
holidayCalendarId: { type: "string", description: "Holiday calendar ID." },
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string", description: "Holiday name." },
isRecurringAnnual: { type: "boolean", description: "Whether the holiday repeats every year." },
source: { type: "string", description: "Optional source or legal basis." },
},
required: ["holidayCalendarId", "date", "name"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar_entry",
description: "Update an existing holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
data: {
type: "object",
properties: {
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string" },
isRecurringAnnual: { type: "boolean" },
source: { type: "string" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar_entry",
description: "Delete a holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
},
required: ["id"],
},
},
},
];
export function createVacationHolidayExecutors(
deps: VacationHolidayDeps,
): Record<string, ToolExecutor> {
return {
async get_vacation_balance(params: { resourceId: string; year?: number }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = deps.createEntitlementCaller(deps.createScopedCallerContext(ctx));
return caller.getBalanceDetail({ resourceId: resource.id, year });
},
async list_vacations_upcoming(params: {
resourceName?: string;
chapter?: string;
daysAhead?: number;
limit?: number;
}, ctx: ToolContext) {
const daysAhead = params.daysAhead ?? 30;
const limit = Math.min(params.limit ?? 30, 50);
const caller = deps.createVacationCaller(deps.createScopedCallerContext(ctx));
const now = new Date();
const until = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
const vacations = await caller.list({
status: "APPROVED",
startDate: now,
endDate: until,
limit,
});
return vacations
.filter((vacation) => {
if (params.resourceName) {
const resourceName = vacation.resource.displayName.toLowerCase();
if (!resourceName.includes(params.resourceName.toLowerCase())) {
return false;
}
}
if (params.chapter) {
const chapter = vacation.resource.chapter?.toLowerCase() ?? "";
if (!chapter.includes(params.chapter.toLowerCase())) {
return false;
}
}
return true;
})
.slice(0, limit)
.map((vacation) => ({
resource: vacation.resource.displayName,
eid: vacation.resource.eid,
chapter: vacation.resource.chapter ?? null,
type: vacation.type,
start: deps.fmtDate(vacation.startDate),
end: deps.fmtDate(vacation.endDate),
isHalfDay: vacation.isHalfDay,
halfDayPart: vacation.halfDayPart,
}));
},
async list_holidays_by_region(params: {
countryCode: string;
federalState?: string;
metroCity?: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const { year, periodStart, periodEnd } = deps.resolveHolidayPeriod(params);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
const resolved = await caller.resolveHolidaysDetail({
periodStart,
periodEnd,
countryCode: params.countryCode.trim().toUpperCase(),
...(params.federalState ? { stateCode: params.federalState } : {}),
...(params.metroCity ? { metroCityName: params.metroCity } : {}),
});
return {
locationContext: resolved.locationContext,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async get_resource_holidays(params: {
identifier: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const resource = await deps.resolveResourceIdentifier(ctx, params.identifier);
if ("error" in resource) {
return resource;
}
const { year, periodStart, periodEnd } = deps.resolveHolidayPeriod(params);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
const resolved = await caller.resolveResourceHolidaysDetail({
resourceId: resource.id,
periodStart,
periodEnd,
});
return {
resource: resolved.resource,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async list_holiday_calendars(params: {
includeInactive?: boolean;
countryCode?: string;
scopeType?: "COUNTRY" | "STATE" | "CITY";
stateCode?: string;
metroCity?: string;
}, ctx: ToolContext) {
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
return caller.listCalendarsDetail(params);
},
async get_holiday_calendar(params: { identifier: string }, ctx: ToolContext) {
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
const identifier = params.identifier.trim();
return deps.resolveEntityOrAssistantError(
() => caller.getCalendarByIdentifierDetail({ identifier }),
`Holiday calendar not found: ${identifier}`,
);
},
async preview_resolved_holiday_calendar(params: {
countryId: string;
stateCode?: string;
metroCityId?: string;
year: number;
}, ctx: ToolContext) {
const input = PreviewResolvedHolidaysSchema.parse(params);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
return caller.previewResolvedHolidaysDetail(input);
},
async create_holiday_calendar(params: {
name: string;
scopeType: "COUNTRY" | "STATE" | "CITY";
countryId: string;
stateCode?: string;
metroCityId?: string;
isActive?: boolean;
priority?: number;
}, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
let created;
try {
created = await caller.createCalendar(CreateHolidayCalendarSchema.parse(params));
} catch (error) {
const mapped = deps.toAssistantHolidayCalendarMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: deps.formatHolidayCalendar(created),
message: `Created holiday calendar: ${created.name}`,
};
},
async update_holiday_calendar(params: {
id: string;
data: {
name?: string;
stateCode?: string | null;
metroCityId?: string | null;
isActive?: boolean;
priority?: number;
};
}, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarSchema.parse(params.data),
};
let updated;
try {
updated = await caller.updateCalendar(input);
} catch (error) {
const mapped = deps.toAssistantHolidayCalendarMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: deps.formatHolidayCalendar(updated),
message: `Updated holiday calendar: ${updated.name}`,
};
},
async delete_holiday_calendar(params: { id: string }, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
let deleted;
try {
deleted = await caller.deleteCalendar({ id: params.id });
} catch (error) {
const mapped = deps.toAssistantHolidayCalendarNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday calendar: ${deleted.name}`,
};
},
async create_holiday_calendar_entry(params: {
holidayCalendarId: string;
date: string;
name: string;
isRecurringAnnual?: boolean;
source?: string;
}, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
let created;
try {
created = await caller.createEntry(CreateHolidayCalendarEntrySchema.parse(params));
} catch (error) {
const mapped = deps.toAssistantHolidayEntryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
entry: deps.formatHolidayCalendarEntry(created),
message: `Created holiday entry: ${created.name}`,
};
},
async update_holiday_calendar_entry(params: {
id: string;
data: {
date?: string;
name?: string;
isRecurringAnnual?: boolean;
source?: string | null;
};
}, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarEntrySchema.parse(params.data),
};
let updated;
try {
updated = await caller.updateEntry(input);
} catch (error) {
const mapped = deps.toAssistantHolidayEntryMutationError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
entry: deps.formatHolidayCalendarEntry(updated),
message: `Updated holiday entry: ${updated.name}`,
};
},
async delete_holiday_calendar_entry(params: { id: string }, ctx: ToolContext) {
deps.assertAdminRole(ctx);
const caller = deps.createHolidayCalendarCaller(deps.createScopedCallerContext(ctx));
let deleted;
try {
deleted = await caller.deleteEntry({ id: params.id });
} catch (error) {
const mapped = deps.toAssistantHolidayEntryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday entry: ${deleted.name}`,
};
},
};
}