refactor(api): extract assistant tool admin slices
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import {
|
||||
CreateClientSchema,
|
||||
CreateOrgUnitSchema,
|
||||
UpdateClientSchema,
|
||||
UpdateOrgUnitSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type ClientsOrgUnitsDeps = {
|
||||
createClientCaller: (ctx: TRPCContext) => {
|
||||
create: (params: z.input<typeof CreateClientSchema>) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateClientSchema>;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
getById: (params: { id: string }) => Promise<{ name: string }>;
|
||||
delete: (params: { id: string }) => Promise<unknown>;
|
||||
};
|
||||
createOrgUnitCaller: (ctx: TRPCContext) => {
|
||||
create: (params: z.input<typeof CreateOrgUnitSchema>) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateOrgUnitSchema>;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
toAssistantClientMutationError: (
|
||||
error: unknown,
|
||||
action?: "create" | "update" | "delete",
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantOrgUnitMutationError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const clientMutationToolDefinitions: ToolDef[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_client",
|
||||
description: "Create a new client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Client name" },
|
||||
code: { type: "string", description: "Client code" },
|
||||
parentId: { type: "string", description: "Optional parent client ID" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
tags: { type: "array", items: { type: "string" }, description: "Optional client tags" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_client",
|
||||
description: "Update a client. Requires manager or admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
code: { type: "string", description: "New code" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent client ID; use null to clear" },
|
||||
tags: { type: "array", items: { type: "string" }, description: "Replacement client tags" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_client",
|
||||
description: "Delete a client. Requires admin role. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Client ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const orgUnitMutationToolDefinitions: ToolDef[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_org_unit",
|
||||
description: "Create a new organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Org unit name" },
|
||||
shortName: { type: "string", description: "Short name/code" },
|
||||
level: { type: "integer", description: "Level (5, 6, or 7)" },
|
||||
parentId: { type: "string", description: "Parent org unit ID (optional)" },
|
||||
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
|
||||
},
|
||||
required: ["name", "level"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_org_unit",
|
||||
description: "Update an organizational unit. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Org unit ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
shortName: { type: "string", description: "New short name" },
|
||||
sortOrder: { type: "integer", description: "New sort order" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
parentId: { type: "string", description: "Parent org unit ID; use null to clear" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function createClientsOrgUnitsExecutors(
|
||||
deps: ClientsOrgUnitsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async create_client(params: {
|
||||
name: string;
|
||||
code?: string;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
tags?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await caller.create(CreateClientSchema.parse(params));
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantClientMutationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["client"],
|
||||
success: true,
|
||||
message: `Created client: ${client.name}`,
|
||||
clientId: client.id,
|
||||
client,
|
||||
};
|
||||
},
|
||||
|
||||
async update_client(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
code?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
tags?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateClientSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
...(params.code !== undefined ? { code: params.code } : {}),
|
||||
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
|
||||
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
|
||||
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
|
||||
...(params.tags !== undefined ? { tags: params.tags } : {}),
|
||||
});
|
||||
if (Object.keys(data).length === 0) {
|
||||
return { error: "No fields to update" };
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await caller.update({ id: params.id, data });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantClientMutationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["client"],
|
||||
success: true,
|
||||
message: `Updated client: ${client.name}`,
|
||||
clientId: client.id,
|
||||
client,
|
||||
};
|
||||
},
|
||||
|
||||
async delete_client(params: { id: string }, ctx: ToolContext) {
|
||||
const caller = deps.createClientCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await caller.getById({ id: params.id });
|
||||
await caller.delete({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantClientMutationError(error, "delete");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["client"],
|
||||
success: true,
|
||||
message: `Deleted client: ${client.name}`,
|
||||
};
|
||||
},
|
||||
|
||||
async create_org_unit(params: {
|
||||
name: string;
|
||||
shortName?: string;
|
||||
level: number;
|
||||
parentId?: string;
|
||||
sortOrder?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let orgUnit;
|
||||
try {
|
||||
orgUnit = await caller.create(CreateOrgUnitSchema.parse(params));
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantOrgUnitMutationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["orgUnit"],
|
||||
success: true,
|
||||
message: `Created org unit: ${orgUnit.name}`,
|
||||
orgUnitId: orgUnit.id,
|
||||
orgUnit,
|
||||
};
|
||||
},
|
||||
|
||||
async update_org_unit(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
shortName?: string | null;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
parentId?: string | null;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createOrgUnitCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateOrgUnitSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
...(params.shortName !== undefined ? { shortName: params.shortName } : {}),
|
||||
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
|
||||
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
|
||||
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
|
||||
});
|
||||
if (Object.keys(data).length === 0) {
|
||||
return { error: "No fields to update" };
|
||||
}
|
||||
|
||||
let orgUnit;
|
||||
try {
|
||||
orgUnit = await caller.update({ id: params.id, data });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantOrgUnitMutationError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["orgUnit"],
|
||||
success: true,
|
||||
message: `Updated org unit: ${orgUnit.name}`,
|
||||
orgUnitId: orgUnit.id,
|
||||
orgUnit,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { CreateRoleSchema, UpdateRoleSchema } from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type ResolvedResource = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type RolesAnalyticsDeps = {
|
||||
createRoleCaller: (ctx: TRPCContext) => {
|
||||
list: (params: Record<string, never>) => Promise<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}>>;
|
||||
create: (params: z.input<typeof CreateRoleSchema>) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateRoleSchema>;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
getById: (params: { id: string }) => Promise<{ name: string }>;
|
||||
delete: (params: { id: string }) => Promise<unknown>;
|
||||
};
|
||||
createResourceCaller: (ctx: TRPCContext) => {
|
||||
searchBySkills: (params: {
|
||||
rules: Array<{ skill: string; minProficiency: number }>;
|
||||
operator: "OR";
|
||||
}) => Promise<Array<{
|
||||
id: string;
|
||||
eid: string;
|
||||
displayName: string;
|
||||
chapter?: string | null;
|
||||
matchedSkills: Array<{
|
||||
skill: string;
|
||||
proficiency: number;
|
||||
}>;
|
||||
}>>;
|
||||
getChargeabilitySummary: (params: {
|
||||
resourceId: string;
|
||||
month: string;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createDashboardCaller: (ctx: TRPCContext) => {
|
||||
getStatisticsDetail: () => Promise<unknown>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveResourceIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||
toAssistantRoleMutationError: (
|
||||
error: unknown,
|
||||
action: "create" | "update" | "delete",
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const rolesAnalyticsReadToolDefinitions: ToolDef[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_roles",
|
||||
description: "List all available roles with their colors.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "search_by_skill",
|
||||
description: "Find resources that have a specific skill. Controller/manager/admin access required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skill: { type: "string", description: "Skill name to search for" },
|
||||
},
|
||||
required: ["skill"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_statistics",
|
||||
description: "Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.",
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_chargeability",
|
||||
description: "Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID, eid, or name" },
|
||||
month: { type: "string", description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month" },
|
||||
},
|
||||
required: ["resourceId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = [
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_role",
|
||||
description: "Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Role name" },
|
||||
description: { type: "string", description: "Optional role description" },
|
||||
color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_role",
|
||||
description: "Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
name: { type: "string", description: "New name" },
|
||||
description: { type: "string", description: "New description" },
|
||||
color: { type: "string", description: "New hex color" },
|
||||
isActive: { type: "boolean", description: "Set active state" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_role",
|
||||
description: "Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Role ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function createRolesAnalyticsExecutors(
|
||||
deps: RolesAnalyticsDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_roles(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
const roles = await caller.list({});
|
||||
return roles.map((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color ?? null,
|
||||
}));
|
||||
},
|
||||
|
||||
async search_by_skill(params: { skill: string }, ctx: ToolContext) {
|
||||
const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx));
|
||||
const matched = await caller.searchBySkills({
|
||||
rules: [{ skill: params.skill, minProficiency: 1 }],
|
||||
operator: "OR",
|
||||
});
|
||||
|
||||
return matched.slice(0, 20).map((resource) => ({
|
||||
id: resource.id,
|
||||
eid: resource.eid,
|
||||
name: resource.displayName,
|
||||
matchedSkill: resource.matchedSkills[0]?.skill ?? null,
|
||||
level: resource.matchedSkills[0]?.proficiency ?? null,
|
||||
chapter: resource.chapter ?? null,
|
||||
}));
|
||||
},
|
||||
|
||||
async get_statistics(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createDashboardCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getStatisticsDetail();
|
||||
},
|
||||
|
||||
async get_chargeability(params: { resourceId: string; month?: string }, ctx: ToolContext) {
|
||||
const now = new Date();
|
||||
const month = params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
const resource = await deps.resolveResourceIdentifier(ctx, params.resourceId);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
const caller = deps.createResourceCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getChargeabilitySummary({
|
||||
resourceId: resource.id,
|
||||
month,
|
||||
});
|
||||
},
|
||||
|
||||
async create_role(params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let role;
|
||||
try {
|
||||
role = await caller.create(CreateRoleSchema.parse(params));
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantRoleMutationError(error, "create");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["role"],
|
||||
success: true,
|
||||
message: `Created role: ${role.name}`,
|
||||
roleId: role.id,
|
||||
role,
|
||||
};
|
||||
},
|
||||
|
||||
async update_role(params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
isActive?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
const data = UpdateRoleSchema.parse({
|
||||
...(params.name !== undefined ? { name: params.name } : {}),
|
||||
...(params.description !== undefined ? { description: params.description } : {}),
|
||||
...(params.color !== undefined ? { color: params.color } : {}),
|
||||
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
|
||||
});
|
||||
if (Object.keys(data).length === 0) {
|
||||
return { error: "No fields to update" };
|
||||
}
|
||||
|
||||
let role;
|
||||
try {
|
||||
role = await caller.update({ id: params.id, data });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantRoleMutationError(error, "update");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["role"],
|
||||
success: true,
|
||||
message: `Updated role: ${role.name}`,
|
||||
roleId: role.id,
|
||||
role,
|
||||
};
|
||||
},
|
||||
|
||||
async delete_role(params: { id: string }, ctx: ToolContext) {
|
||||
const caller = deps.createRoleCaller(deps.createScopedCallerContext(ctx));
|
||||
|
||||
let role;
|
||||
try {
|
||||
role = await caller.getById({ id: params.id });
|
||||
await caller.delete({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantRoleMutationError(error, "delete");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["role"],
|
||||
success: true,
|
||||
message: `Deleted role: ${role.name}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
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}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user