refactor(api): extract assistant tool admin slices
This commit is contained in:
@@ -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}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user