feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -0,0 +1,770 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
export const advancedTimelineToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "find_best_project_resource",
|
||||
description: "Advanced assistant tool: find the best already-assigned resource on a project for a given period, ranked by remaining capacity or LCR. Holiday- and vacation-aware. Requires viewCosts and advanced assistant permissions.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
|
||||
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
|
||||
minHoursPerDay: { type: "number", description: "Minimum remaining availability per effective working day. Default: 3." },
|
||||
rankingMode: { type: "string", description: "Ranking mode: lowest_lcr, highest_remaining_hours_per_day, or highest_remaining_hours. Default: lowest_lcr." },
|
||||
chapter: { type: "string", description: "Optional chapter filter for candidate resources." },
|
||||
roleName: { type: "string", description: "Optional role filter for candidate resources." },
|
||||
},
|
||||
required: ["projectIdentifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_timeline_entries_view",
|
||||
description: "Advanced assistant tool: read-only timeline entries view with the same timeline/disposition readmodel used by the app. Returns allocations, demands, assignments, and matching holiday overlays for a period.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
|
||||
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
|
||||
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the view." },
|
||||
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the view." },
|
||||
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the view." },
|
||||
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
|
||||
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the view." },
|
||||
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_timeline_holiday_overlays",
|
||||
description: "Advanced assistant tool: read-only holiday overlays for the timeline, resolved with the same holiday logic as the app. Useful to explain regional holiday differences for assigned or filtered resources.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
|
||||
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
|
||||
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the overlays." },
|
||||
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the overlays via matching assignments." },
|
||||
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the overlays via matching projects." },
|
||||
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
|
||||
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the overlays." },
|
||||
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_project_timeline_context",
|
||||
description: "Advanced assistant tool: read-only project timeline/disposition context. Reuses the same project context readmodel as the app and adds holiday overlays plus cross-project overlap summaries for assigned resources.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
startDate: { type: "string", description: "Optional holiday/conflict window start date in YYYY-MM-DD. Defaults to the project start date when available." },
|
||||
endDate: { type: "string", description: "Optional holiday/conflict window end date in YYYY-MM-DD. Defaults to the project end date when available." },
|
||||
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted." },
|
||||
},
|
||||
required: ["projectIdentifier"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "preview_project_shift",
|
||||
description: "Advanced assistant tool: read-only preview of the timeline shift validation for a project. Uses the same preview logic as the timeline router and does not write changes.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
newStartDate: { type: "string", description: "New start date in YYYY-MM-DD." },
|
||||
newEndDate: { type: "string", description: "New end date in YYYY-MM-DD." },
|
||||
},
|
||||
required: ["projectIdentifier", "newStartDate", "newEndDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_timeline_allocation_inline",
|
||||
description: "Advanced assistant mutation: update a timeline allocation inline with the same manager/admin + manageAllocations validation as the timeline API. Supports hours/day, dates, includeSaturday, and role changes. Requires useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation, assignment, or demand row ID to update." },
|
||||
hoursPerDay: { type: "number", description: "Optional new booked hours per day." },
|
||||
startDate: { type: "string", description: "Optional new start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "Optional new end date in YYYY-MM-DD." },
|
||||
includeSaturday: { type: "boolean", description: "Optional Saturday-working flag stored in metadata." },
|
||||
role: { type: "string", description: "Optional new role label." },
|
||||
},
|
||||
required: ["allocationId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "apply_timeline_project_shift",
|
||||
description: "Advanced assistant mutation: apply the real timeline project shift mutation, including validation, date movement, cost recalculation, audit logging, and SSE. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
newStartDate: { type: "string", description: "New project start date in YYYY-MM-DD." },
|
||||
newEndDate: { type: "string", description: "New project end date in YYYY-MM-DD." },
|
||||
},
|
||||
required: ["projectIdentifier", "newStartDate", "newEndDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "quick_assign_timeline_resource",
|
||||
description: "Advanced assistant mutation: create a timeline quick-assignment with the same manager/admin + manageAllocations rules as the timeline UI. Resolves resource and project identifiers before calling the real mutation. Requires useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
|
||||
role: { type: "string", description: "Role label. Default: Team Member." },
|
||||
roleId: { type: "string", description: "Optional concrete role ID." },
|
||||
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
|
||||
},
|
||||
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "batch_quick_assign_timeline_resources",
|
||||
description: "Advanced assistant mutation: batch-create timeline quick-assignments using the same timeline router logic, permission checks, and audit/SSE side effects as the app. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
assignments: {
|
||||
type: "array",
|
||||
minItems: 1,
|
||||
maxItems: 50,
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
|
||||
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
|
||||
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
|
||||
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
|
||||
role: { type: "string", description: "Role label. Default: Team Member." },
|
||||
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
|
||||
},
|
||||
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
|
||||
},
|
||||
description: "Assignment rows to create in one batch.",
|
||||
},
|
||||
},
|
||||
required: ["assignments"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "batch_shift_timeline_allocations",
|
||||
description: "Advanced assistant mutation: shift multiple timeline allocations by a shared day delta using the real timeline batch move/resize mutation. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationIds: { type: "array", items: { type: "string" }, description: "Allocation IDs to shift." },
|
||||
daysDelta: { type: "integer", description: "Signed day delta to apply." },
|
||||
mode: { type: "string", enum: ["move", "resize-start", "resize-end"], description: "Shift mode. Default: move." },
|
||||
},
|
||||
required: ["allocationIds", "daysDelta"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
find_best_project_resource: {
|
||||
requiresPlanningRead: true,
|
||||
requiresCostView: true,
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_timeline_entries_view: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_timeline_holiday_overlays: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_project_timeline_context: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
preview_project_shift: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
update_timeline_allocation_inline: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
apply_timeline_project_shift: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
quick_assign_timeline_resource: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
batch_quick_assign_timeline_resources: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
batch_shift_timeline_allocations: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
});
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type ResolvedProject = {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
shortCode?: string | null;
|
||||
};
|
||||
|
||||
type ResolvedResource = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type TimelineMutationContext = "updateInline" | "applyShift" | "quickAssign" | "batchShift";
|
||||
|
||||
type BatchQuickAssignmentInput = {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
hoursPerDay?: number;
|
||||
role?: string;
|
||||
status?: AllocationStatus;
|
||||
};
|
||||
|
||||
type AdvancedTimelineDeps = {
|
||||
assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
|
||||
createStaffingCaller: (ctx: TRPCContext) => {
|
||||
getBestProjectResourceDetail: (params: {
|
||||
projectId: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
durationDays?: number;
|
||||
minHoursPerDay?: number;
|
||||
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
|
||||
chapter?: string;
|
||||
roleName?: string;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createTimelineCaller: (ctx: TRPCContext) => {
|
||||
getEntriesDetail: (params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
resourceIds?: string[];
|
||||
projectIds?: string[];
|
||||
clientIds?: string[];
|
||||
chapters?: string[];
|
||||
eids?: string[];
|
||||
countryCodes?: string[];
|
||||
}) => Promise<unknown>;
|
||||
getHolidayOverlayDetail: (params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
resourceIds?: string[];
|
||||
projectIds?: string[];
|
||||
clientIds?: string[];
|
||||
chapters?: string[];
|
||||
eids?: string[];
|
||||
countryCodes?: string[];
|
||||
}) => Promise<unknown>;
|
||||
getProjectContextDetail: (params: {
|
||||
projectId: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
}) => Promise<unknown>;
|
||||
getShiftPreviewDetail: (params: {
|
||||
projectId: string;
|
||||
newStartDate: Date;
|
||||
newEndDate: Date;
|
||||
}) => Promise<unknown>;
|
||||
updateAllocationInline: (params: {
|
||||
allocationId: string;
|
||||
hoursPerDay?: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
includeSaturday?: boolean;
|
||||
role?: string;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
projectId: string;
|
||||
resourceId?: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
hoursPerDay: number;
|
||||
role?: string | null;
|
||||
status: string;
|
||||
}>;
|
||||
applyShift: (params: {
|
||||
projectId: string;
|
||||
newStartDate: Date;
|
||||
newEndDate: Date;
|
||||
}) => Promise<{
|
||||
project: {
|
||||
id: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
};
|
||||
validation: unknown;
|
||||
}>;
|
||||
quickAssign: (params: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
hoursPerDay?: number;
|
||||
role?: string;
|
||||
roleId?: string;
|
||||
status?: AllocationStatus;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
projectId: string;
|
||||
resourceId?: string | null;
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
hoursPerDay: number;
|
||||
role?: string | null;
|
||||
status: string;
|
||||
}>;
|
||||
batchQuickAssign: (params: { assignments: BatchQuickAssignmentInput[] }) => Promise<{ count: number }>;
|
||||
batchShiftAllocations: (params: {
|
||||
allocationIds: string[];
|
||||
daysDelta: number;
|
||||
mode?: "move" | "resize-start" | "resize-end";
|
||||
}) => Promise<{ count: number }>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveProjectIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedProject | AssistantToolErrorResult>;
|
||||
resolveResourceIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
isAssistantToolErrorResult: (value: unknown) => value is AssistantToolErrorResult;
|
||||
toAssistantIndexedFieldError: (index: number, field: string, message: string) => unknown;
|
||||
toAssistantTimelineMutationError: (error: unknown, context: TimelineMutationContext) => unknown;
|
||||
};
|
||||
|
||||
function toDate(value: Date | string): Date {
|
||||
return value instanceof Date ? value : new Date(value);
|
||||
}
|
||||
|
||||
export function createAdvancedTimelineExecutors(
|
||||
deps: AdvancedTimelineDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async find_best_project_resource(params: {
|
||||
projectIdentifier: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
minHoursPerDay?: number;
|
||||
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
|
||||
chapter?: string;
|
||||
roleName?: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);
|
||||
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createStaffingCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getBestProjectResourceDetail({
|
||||
projectId: project.id,
|
||||
...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
|
||||
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
||||
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
|
||||
...(params.minHoursPerDay !== undefined ? { minHoursPerDay: params.minHoursPerDay } : {}),
|
||||
...(params.rankingMode ? { rankingMode: params.rankingMode } : {}),
|
||||
...(params.chapter ? { chapter: params.chapter } : {}),
|
||||
...(params.roleName ? { roleName: params.roleName } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async get_timeline_entries_view(params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
resourceIds?: string[];
|
||||
projectIds?: string[];
|
||||
clientIds?: string[];
|
||||
chapters?: string[];
|
||||
eids?: string[];
|
||||
countryCodes?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getEntriesDetail({ ...params });
|
||||
},
|
||||
|
||||
async get_timeline_holiday_overlays(params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
resourceIds?: string[];
|
||||
projectIds?: string[];
|
||||
clientIds?: string[];
|
||||
chapters?: string[];
|
||||
eids?: string[];
|
||||
countryCodes?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getHolidayOverlayDetail({ ...params });
|
||||
},
|
||||
|
||||
async get_project_timeline_context(params: {
|
||||
projectIdentifier: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getProjectContextDetail({
|
||||
projectId: project.id,
|
||||
...(params.startDate ? { startDate: params.startDate } : {}),
|
||||
...(params.endDate ? { endDate: params.endDate } : {}),
|
||||
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async preview_project_shift(params: {
|
||||
projectIdentifier: string;
|
||||
newStartDate: string;
|
||||
newEndDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getShiftPreviewDetail({
|
||||
projectId: project.id,
|
||||
newStartDate: deps.parseIsoDate(params.newStartDate, "newStartDate"),
|
||||
newEndDate: deps.parseIsoDate(params.newEndDate, "newEndDate"),
|
||||
});
|
||||
},
|
||||
|
||||
async update_timeline_allocation_inline(params: {
|
||||
allocationId: string;
|
||||
hoursPerDay?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
includeSaturday?: boolean;
|
||||
role?: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
let updated;
|
||||
try {
|
||||
updated = await caller.updateAllocationInline({
|
||||
allocationId: params.allocationId,
|
||||
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
|
||||
...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
|
||||
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
||||
...(params.includeSaturday !== undefined ? { includeSaturday: params.includeSaturday } : {}),
|
||||
...(params.role !== undefined ? { role: params.role } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantTimelineMutationError(error, "updateInline");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline", "project"],
|
||||
success: true,
|
||||
message: `Updated timeline allocation ${updated.id}.`,
|
||||
allocation: {
|
||||
id: updated.id,
|
||||
projectId: updated.projectId,
|
||||
resourceId: updated.resourceId ?? null,
|
||||
startDate: deps.fmtDate(updated.startDate),
|
||||
endDate: deps.fmtDate(updated.endDate),
|
||||
hoursPerDay: updated.hoursPerDay,
|
||||
role: updated.role ?? null,
|
||||
status: updated.status,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async apply_timeline_project_shift(params: {
|
||||
projectIdentifier: string;
|
||||
newStartDate: string;
|
||||
newEndDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const newStartDate = deps.parseIsoDate(params.newStartDate, "newStartDate");
|
||||
const newEndDate = deps.parseIsoDate(params.newEndDate, "newEndDate");
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
let result;
|
||||
try {
|
||||
result = await caller.applyShift({
|
||||
projectId: project.id,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantTimelineMutationError(error, "applyShift");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline", "project"],
|
||||
success: true,
|
||||
message: `Shifted project ${project.shortCode ?? project.name ?? project.id} to ${deps.fmtDate(newStartDate)} - ${deps.fmtDate(newEndDate)}.`,
|
||||
project: {
|
||||
id: result.project.id,
|
||||
startDate: deps.fmtDate(result.project.startDate),
|
||||
endDate: deps.fmtDate(result.project.endDate),
|
||||
},
|
||||
validation: result.validation,
|
||||
};
|
||||
},
|
||||
|
||||
async quick_assign_timeline_resource(params: {
|
||||
resourceIdentifier: string;
|
||||
projectIdentifier: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hoursPerDay?: number;
|
||||
role?: string;
|
||||
roleId?: string;
|
||||
status?: AllocationStatus;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, params.resourceIdentifier),
|
||||
deps.resolveProjectIdentifier(ctx, params.projectIdentifier),
|
||||
]);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
let allocation;
|
||||
try {
|
||||
allocation = await caller.quickAssign({
|
||||
resourceId: resource.id,
|
||||
projectId: project.id,
|
||||
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: deps.parseIsoDate(params.endDate, "endDate"),
|
||||
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
|
||||
...(params.role !== undefined ? { role: params.role } : {}),
|
||||
...(params.roleId !== undefined ? { roleId: params.roleId } : {}),
|
||||
...(params.status !== undefined ? { status: params.status } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline", "project"],
|
||||
success: true,
|
||||
message: `Quick-assigned ${resource.displayName} to ${project.name} (${project.shortCode ?? project.id}).`,
|
||||
allocation: {
|
||||
id: allocation.id,
|
||||
projectId: allocation.projectId,
|
||||
resourceId: allocation.resourceId ?? null,
|
||||
startDate: deps.fmtDate(toDate(allocation.startDate)),
|
||||
endDate: deps.fmtDate(toDate(allocation.endDate)),
|
||||
hoursPerDay: allocation.hoursPerDay,
|
||||
role: allocation.role ?? null,
|
||||
status: allocation.status,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
async batch_quick_assign_timeline_resources(params: {
|
||||
assignments: Array<{
|
||||
resourceIdentifier: string;
|
||||
projectIdentifier: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hoursPerDay?: number;
|
||||
role?: string;
|
||||
status?: AllocationStatus;
|
||||
}>;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const resolvedAssignments = await Promise.all(params.assignments.map(async (assignment, index) => {
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, assignment.resourceIdentifier),
|
||||
deps.resolveProjectIdentifier(ctx, assignment.projectIdentifier),
|
||||
]);
|
||||
if ("error" in resource) {
|
||||
return deps.toAssistantIndexedFieldError(index, "resourceIdentifier", resource.error);
|
||||
}
|
||||
if ("error" in project) {
|
||||
return deps.toAssistantIndexedFieldError(index, "projectIdentifier", project.error);
|
||||
}
|
||||
return {
|
||||
resourceId: resource.id,
|
||||
projectId: project.id,
|
||||
startDate: deps.parseIsoDate(assignment.startDate, `assignments[${index}].startDate`),
|
||||
endDate: deps.parseIsoDate(assignment.endDate, `assignments[${index}].endDate`),
|
||||
...(assignment.hoursPerDay !== undefined ? { hoursPerDay: assignment.hoursPerDay } : {}),
|
||||
...(assignment.role !== undefined ? { role: assignment.role } : {}),
|
||||
...(assignment.status !== undefined ? { status: assignment.status } : {}),
|
||||
};
|
||||
}));
|
||||
|
||||
const resolutionError = resolvedAssignments.find(deps.isAssistantToolErrorResult);
|
||||
if (resolutionError) {
|
||||
return resolutionError;
|
||||
}
|
||||
const validAssignments = resolvedAssignments.filter(
|
||||
(assignment): assignment is BatchQuickAssignmentInput => !deps.isAssistantToolErrorResult(assignment),
|
||||
);
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
let result;
|
||||
try {
|
||||
result = await caller.batchQuickAssign({
|
||||
assignments: validAssignments,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline", "project"],
|
||||
success: true,
|
||||
message: `Created ${result.count} timeline quick-assignment(s).`,
|
||||
count: result.count,
|
||||
};
|
||||
},
|
||||
|
||||
async batch_shift_timeline_allocations(params: {
|
||||
allocationIds: string[];
|
||||
daysDelta: number;
|
||||
mode?: "move" | "resize-start" | "resize-end";
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
let result;
|
||||
try {
|
||||
result = await caller.batchShiftAllocations({
|
||||
allocationIds: params.allocationIds,
|
||||
daysDelta: params.daysDelta,
|
||||
...(params.mode !== undefined ? { mode: params.mode } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantTimelineMutationError(error, "batchShift");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline", "project"],
|
||||
success: true,
|
||||
message: `Shifted ${result.count} allocation(s) by ${params.daysDelta} day(s).`,
|
||||
count: result.count,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { AllocationStatus, PermissionKey, UpdateAssignmentSchema } from "@capakraken/shared";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { fmtEur } from "../../lib/format-utils.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
type ResolvedProject = {
|
||||
id: string;
|
||||
name: string;
|
||||
shortCode: string;
|
||||
};
|
||||
|
||||
type ResolvedResource = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type AllocationPlanningDeps = {
|
||||
assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
|
||||
createAllocationCaller: (ctx: TRPCContext) => {
|
||||
listView: (params: {
|
||||
resourceId?: string;
|
||||
projectId?: string;
|
||||
status?: AllocationStatus;
|
||||
}) => Promise<{
|
||||
assignments: Array<{
|
||||
id: string;
|
||||
status: string;
|
||||
hoursPerDay: number;
|
||||
dailyCostCents?: number | null;
|
||||
startDate: Date | string;
|
||||
endDate: Date | string;
|
||||
role?: string | null;
|
||||
roleEntity?: { name?: string | null } | null;
|
||||
resource?: { displayName?: string | null; eid?: string | null } | null;
|
||||
project?: { name?: string | null; shortCode?: string | null } | null;
|
||||
}>;
|
||||
}>;
|
||||
ensureAssignment: (params: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
hoursPerDay: number;
|
||||
role?: string;
|
||||
}) => Promise<{
|
||||
action: "created" | "reactivated";
|
||||
assignment: {
|
||||
id: string;
|
||||
status: string;
|
||||
};
|
||||
}>;
|
||||
resolveAssignment: (params: {
|
||||
assignmentId?: string;
|
||||
resourceId?: string;
|
||||
projectId?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
selectionMode: "WINDOW" | "EXACT_START";
|
||||
excludeCancelled?: boolean;
|
||||
}) => Promise<{
|
||||
id: string;
|
||||
status: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
resource: { displayName: string };
|
||||
project: { name: string; shortCode: string };
|
||||
}>;
|
||||
updateAssignment: (params: {
|
||||
id: string;
|
||||
data: z.input<typeof UpdateAssignmentSchema>;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createTimelineCaller: (ctx: TRPCContext) => {
|
||||
getBudgetStatus: (params: { projectId: string }) => Promise<{
|
||||
projectName: string;
|
||||
projectCode: string;
|
||||
budgetCents: number;
|
||||
confirmedCents: number;
|
||||
proposedCents: number;
|
||||
allocatedCents: number;
|
||||
remainingCents: number;
|
||||
utilizationPercent: number;
|
||||
winProbabilityWeightedCents: number;
|
||||
totalAllocations: number;
|
||||
}>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
resolveProjectIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedProject | AssistantToolErrorResult>;
|
||||
resolveResourceIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<ResolvedResource | AssistantToolErrorResult>;
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
parseOptionalIsoDate: (value: string | undefined, fieldName: string) => Date | undefined;
|
||||
fmtDate: (value: Date | null | undefined) => string | null;
|
||||
toAssistantAllocationNotFoundError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export const allocationPlanningReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_allocations",
|
||||
description: "List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Filter by resource ID" },
|
||||
projectId: { type: "string", description: "Filter by project ID" },
|
||||
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
|
||||
projectCode: { type: "string", description: "Filter by project short code (partial match)" },
|
||||
status: { type: "string", description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
|
||||
limit: { type: "integer", description: "Max results. Default: 30" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_budget_status",
|
||||
description: "Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
list_allocations: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
get_budget_status: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const allocationPlanningMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_allocation",
|
||||
description: "Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
resourceId: { type: "string", description: "Resource ID" },
|
||||
projectId: { type: "string", description: "Project ID" },
|
||||
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "End date YYYY-MM-DD" },
|
||||
hoursPerDay: { type: "number", description: "Hours per day (e.g. 8)" },
|
||||
role: { type: "string", description: "Optional role name" },
|
||||
},
|
||||
required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "cancel_allocation",
|
||||
description: "Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID (if known)" },
|
||||
resourceName: { type: "string", description: "Resource name (partial match)" },
|
||||
projectCode: { type: "string", description: "Project short code (partial match)" },
|
||||
startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
|
||||
endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_allocation_status",
|
||||
description: "Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
allocationId: { type: "string", description: "Allocation ID" },
|
||||
resourceName: { type: "string", description: "Resource name (partial match, used if no allocationId)" },
|
||||
projectCode: { type: "string", description: "Project short code (partial match, used if no allocationId)" },
|
||||
startDate: { type: "string", description: "Start date filter YYYY-MM-DD (used if no allocationId)" },
|
||||
newStatus: { type: "string", description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
|
||||
},
|
||||
required: ["newStatus"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
create_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
cancel_allocation: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
update_allocation_status: {
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
});
|
||||
|
||||
export function createAllocationPlanningExecutors(
|
||||
deps: AllocationPlanningDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async list_allocations(params: {
|
||||
resourceId?: string;
|
||||
projectId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
const status = params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
|
||||
? params.status as AllocationStatus
|
||||
: undefined;
|
||||
const readModel = await caller.listView({
|
||||
...(params.resourceId ? { resourceId: params.resourceId } : {}),
|
||||
...(params.projectId ? { projectId: params.projectId } : {}),
|
||||
...(status ? { status } : {}),
|
||||
});
|
||||
|
||||
const resourceNameQuery = params.resourceName?.trim().toLowerCase();
|
||||
const projectCodeQuery = params.projectCode?.trim().toLowerCase();
|
||||
const limit = Math.min(params.limit ?? 30, 50);
|
||||
|
||||
return readModel.assignments
|
||||
.filter((assignment) => {
|
||||
if (
|
||||
resourceNameQuery
|
||||
&& !assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
projectCodeQuery
|
||||
&& !assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.slice(0, limit)
|
||||
.map((assignment) => ({
|
||||
id: assignment.id,
|
||||
resource: assignment.resource?.displayName ?? "Unknown",
|
||||
resourceEid: assignment.resource?.eid ?? null,
|
||||
project: assignment.project?.name ?? "Unknown",
|
||||
projectCode: assignment.project?.shortCode ?? null,
|
||||
role: assignment.role ?? assignment.roleEntity?.name ?? null,
|
||||
status: assignment.status,
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
dailyCost: assignment.dailyCostCents == null ? null : fmtEur(assignment.dailyCostCents),
|
||||
start: deps.fmtDate(new Date(assignment.startDate)),
|
||||
end: deps.fmtDate(new Date(assignment.endDate)),
|
||||
}));
|
||||
},
|
||||
|
||||
async get_budget_status(params: { projectId: string }, ctx: ToolContext) {
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectId);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
|
||||
const budgetStatus = await caller.getBudgetStatus({ projectId: project.id });
|
||||
|
||||
if (budgetStatus.budgetCents <= 0) {
|
||||
return {
|
||||
project: budgetStatus.projectName,
|
||||
code: budgetStatus.projectCode,
|
||||
budget: "Not set",
|
||||
note: "No budget defined for this project",
|
||||
totalAllocations: budgetStatus.totalAllocations,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
project: budgetStatus.projectName,
|
||||
code: budgetStatus.projectCode,
|
||||
budget: fmtEur(budgetStatus.budgetCents),
|
||||
confirmed: fmtEur(budgetStatus.confirmedCents),
|
||||
proposed: fmtEur(budgetStatus.proposedCents),
|
||||
allocated: fmtEur(budgetStatus.allocatedCents),
|
||||
remaining: fmtEur(budgetStatus.remainingCents),
|
||||
utilization: `${budgetStatus.utilizationPercent.toFixed(1)}%`,
|
||||
winWeighted: fmtEur(budgetStatus.winProbabilityWeightedCents),
|
||||
};
|
||||
},
|
||||
|
||||
async create_allocation(params: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hoursPerDay: number;
|
||||
role?: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, params.resourceId),
|
||||
deps.resolveProjectIdentifier(ctx, params.projectId),
|
||||
]);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
try {
|
||||
const result = await caller.ensureAssignment({
|
||||
resourceId: resource.id,
|
||||
projectId: project.id,
|
||||
startDate: deps.parseIsoDate(params.startDate, "startDate"),
|
||||
endDate: deps.parseIsoDate(params.endDate, "endDate"),
|
||||
hoursPerDay: params.hoursPerDay,
|
||||
...(params.role ? { role: params.role } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline"],
|
||||
success: true,
|
||||
message: `${result.action === "reactivated" ? "Reactivated" : "Created"} allocation: ${resource.displayName} → ${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
|
||||
allocationId: result.assignment.id,
|
||||
status: result.assignment.status,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError && error.code === "CONFLICT") {
|
||||
return { error: "Allocation already exists for this resource/project/dates. No new allocation created." };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async cancel_allocation(params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
let resourceId: string | undefined;
|
||||
let projectId: string | undefined;
|
||||
if (!params.allocationId && params.resourceName && params.projectCode) {
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, params.resourceName),
|
||||
deps.resolveProjectIdentifier(ctx, params.projectCode),
|
||||
]);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
resourceId = resource.id;
|
||||
projectId = project.id;
|
||||
}
|
||||
|
||||
const startDate = deps.parseOptionalIsoDate(params.startDate, "startDate");
|
||||
const endDate = deps.parseOptionalIsoDate(params.endDate, "endDate");
|
||||
let assignment;
|
||||
try {
|
||||
assignment = await caller.resolveAssignment({
|
||||
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
|
||||
...(resourceId ? { resourceId } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
...(startDate ? { startDate } : {}),
|
||||
...(endDate ? { endDate } : {}),
|
||||
selectionMode: "WINDOW",
|
||||
excludeCancelled: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantAllocationNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await caller.updateAssignment({
|
||||
id: assignment.id,
|
||||
data: UpdateAssignmentSchema.parse({ status: AllocationStatus.CANCELLED }),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantAllocationNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline"],
|
||||
success: true,
|
||||
message: `Cancelled allocation: ${assignment.resource.displayName} → ${assignment.project.name} (${assignment.project.shortCode}), ${deps.fmtDate(assignment.startDate)} to ${deps.fmtDate(assignment.endDate)}`,
|
||||
};
|
||||
},
|
||||
|
||||
async update_allocation_status(params: {
|
||||
allocationId?: string;
|
||||
resourceName?: string;
|
||||
projectCode?: string;
|
||||
startDate?: string;
|
||||
newStatus: string;
|
||||
}, ctx: ToolContext) {
|
||||
deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
|
||||
const validStatuses = ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"];
|
||||
if (!validStatuses.includes(params.newStatus)) {
|
||||
return { error: `Invalid status: ${params.newStatus}. Valid: ${validStatuses.join(", ")}` };
|
||||
}
|
||||
|
||||
const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
|
||||
let resourceId: string | undefined;
|
||||
let projectId: string | undefined;
|
||||
if (!params.allocationId && params.resourceName && params.projectCode) {
|
||||
const [resource, project] = await Promise.all([
|
||||
deps.resolveResourceIdentifier(ctx, params.resourceName),
|
||||
deps.resolveProjectIdentifier(ctx, params.projectCode),
|
||||
]);
|
||||
if ("error" in resource) {
|
||||
return resource;
|
||||
}
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
resourceId = resource.id;
|
||||
projectId = project.id;
|
||||
}
|
||||
|
||||
const startDate = deps.parseOptionalIsoDate(params.startDate, "startDate");
|
||||
let assignment;
|
||||
try {
|
||||
assignment = await caller.resolveAssignment({
|
||||
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
|
||||
...(resourceId ? { resourceId } : {}),
|
||||
...(projectId ? { projectId } : {}),
|
||||
...(startDate ? { startDate } : {}),
|
||||
selectionMode: "EXACT_START",
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantAllocationNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const oldStatus = assignment.status;
|
||||
try {
|
||||
await caller.updateAssignment({
|
||||
id: assignment.id,
|
||||
data: UpdateAssignmentSchema.parse({
|
||||
status: params.newStatus as AllocationStatus,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantAllocationNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["allocation", "timeline"],
|
||||
success: true,
|
||||
message: `Updated allocation status: ${assignment.resource.displayName} → ${assignment.project.name} (${assignment.project.shortCode}), ${deps.fmtDate(assignment.startDate)} to ${deps.fmtDate(assignment.endDate)}: ${oldStatus} → ${params.newStatus}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { PermissionKey } from "@capakraken/shared";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -67,7 +67,7 @@ export type ChargeabilityComputationDeps = {
|
||||
) => Promise<ResolvedProject | AssistantToolErrorResult>;
|
||||
};
|
||||
|
||||
export const chargeabilityComputationReadToolDefinitions: ToolDef[] = [
|
||||
export const chargeabilityComputationReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -122,7 +122,23 @@ export const chargeabilityComputationReadToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
get_chargeability_report: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_resource_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
get_project_computation_graph: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
requiresAdvancedAssistant: true,
|
||||
},
|
||||
});
|
||||
|
||||
export function createChargeabilityComputationExecutors(
|
||||
deps: ChargeabilityComputationDeps,
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { TRPCContext } from "../../trpc.js";
|
||||
import {
|
||||
CreateClientSchema,
|
||||
CreateOrgUnitSchema,
|
||||
SystemRole,
|
||||
UpdateClientSchema,
|
||||
UpdateOrgUnitSchema,
|
||||
} from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -49,7 +50,7 @@ type ClientsOrgUnitsDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const clientMutationToolDefinitions: ToolDef[] = [
|
||||
export const clientMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -102,9 +103,19 @@ export const clientMutationToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
update_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
delete_client: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
|
||||
export const orgUnitMutationToolDefinitions: ToolDef[] = [
|
||||
export const orgUnitMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -142,7 +153,14 @@ export const orgUnitMutationToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_org_unit: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
|
||||
export function createClientsOrgUnitsExecutors(
|
||||
deps: ClientsOrgUnitsDeps,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type ConfigReadmodelsDeps = {
|
||||
createManagementLevelCaller: (ctx: TRPCContext) => {
|
||||
@@ -77,7 +78,7 @@ type ConfigReadmodelsDeps = {
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
};
|
||||
|
||||
export const configReadmodelToolDefinitions: ToolDef[] = [
|
||||
export const configReadmodelToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -118,7 +119,23 @@ export const configReadmodelToolDefinitions: ToolDef[] = [
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
list_management_levels: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_utilization_categories: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
list_calculation_rules: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
list_effort_rules: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
list_experience_multipliers: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
|
||||
export function createConfigReadmodelExecutors(
|
||||
deps: ConfigReadmodelsDeps,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Prisma } from "@capakraken/db";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import {
|
||||
CreateCountrySchema,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
UpdateMetroCitySchema,
|
||||
} from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -52,7 +53,7 @@ type CountryMetroAdminDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const countryMetroAdminToolDefinitions: ToolDef[] = [
|
||||
export const countryMetroAdminToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -146,7 +147,23 @@ export const countryMetroAdminToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_country: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_metro_city: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
|
||||
export function createCountryMetroAdminExecutors(
|
||||
deps: CountryMetroAdminDeps,
|
||||
|
||||
@@ -2,11 +2,16 @@ import type {
|
||||
CreateEstimateInput,
|
||||
EstimateExportFormat,
|
||||
EstimateStatus,
|
||||
PermissionKey,
|
||||
UpdateEstimateDraftInput,
|
||||
} from "@capakraken/shared";
|
||||
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import {
|
||||
withToolAccess,
|
||||
type ToolContext,
|
||||
type ToolDef,
|
||||
type ToolExecutor,
|
||||
} from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -155,7 +160,7 @@ async function resolveEstimateProjectId(
|
||||
return project.id;
|
||||
}
|
||||
|
||||
export const estimateReadToolDefinitions: ToolDef[] = [
|
||||
export const estimateReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -228,9 +233,27 @@ export const estimateReadToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
get_estimate_detail: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
list_estimate_versions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_estimate_version_snapshot: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiresCostView: true,
|
||||
},
|
||||
get_estimate_weekly_phasing: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_estimate_commercial_terms: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
|
||||
export const estimateMutationToolDefinitions: ToolDef[] = [
|
||||
export const estimateMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -412,7 +435,48 @@ export const estimateMutationToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_estimate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
clone_estimate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
update_estimate_draft: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
submit_estimate_version: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
approve_estimate_version: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
create_estimate_revision: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
create_estimate_export: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
create_estimate_planning_handoff: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
|
||||
},
|
||||
generate_estimate_weekly_phasing: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
update_estimate_commercial_terms: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_PROJECTS],
|
||||
},
|
||||
});
|
||||
|
||||
export function createEstimateExecutors(
|
||||
deps: EstimateToolsDeps,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -143,7 +144,7 @@ type NotificationsTasksDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const notificationInboxToolDefinitions: ToolDef[] = [
|
||||
export const notificationInboxToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -207,9 +208,13 @@ export const notificationInboxToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_notification: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
});
|
||||
|
||||
export const notificationTaskToolDefinitions: ToolDef[] = [
|
||||
export const notificationTaskToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -444,7 +449,23 @@ export const notificationTaskToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_task_for_user: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
assign_task: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
send_broadcast: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
list_broadcasts: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_broadcast_detail: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
});
|
||||
|
||||
export function createNotificationsTasksExecutors(
|
||||
deps: NotificationsTasksDeps,
|
||||
|
||||
@@ -25,9 +25,6 @@ type ProjectRecord = ProjectSummaryRecord & {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
type ParsedCreateProjectInput = ReturnType<typeof CreateProjectSchema.parse>;
|
||||
type ParsedUpdateProjectInput = ReturnType<typeof UpdateProjectSchema.parse>;
|
||||
|
||||
type ResponsiblePersonResolution =
|
||||
| {
|
||||
status: "resolved";
|
||||
@@ -41,17 +38,10 @@ type ResponsiblePersonResolution =
|
||||
type ProjectToolsDeps = {
|
||||
assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
|
||||
createProjectCaller: (ctx: TRPCContext) => {
|
||||
searchSummariesDetail: (params: {
|
||||
search?: string | undefined;
|
||||
status?: ProjectStatus | undefined;
|
||||
limit: number;
|
||||
}) => Promise<unknown>;
|
||||
searchSummariesDetail: (params: any) => Promise<unknown>;
|
||||
getByIdentifierDetail: (params: { identifier: string }) => Promise<unknown>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: ParsedUpdateProjectInput;
|
||||
}) => Promise<ProjectSummaryRecord>;
|
||||
create: (params: ParsedCreateProjectInput) => Promise<ProjectRecord>;
|
||||
update: (params: any) => Promise<ProjectSummaryRecord>;
|
||||
create: (params: any) => Promise<ProjectRecord>;
|
||||
delete: (params: { id: string }) => Promise<unknown>;
|
||||
generateCover: (params: {
|
||||
projectId: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { CreateRoleSchema, UpdateRoleSchema } from "@capakraken/shared";
|
||||
import { CreateRoleSchema, PermissionKey, SystemRole, UpdateRoleSchema } from "@capakraken/shared";
|
||||
import { z } from "zod";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -63,7 +63,7 @@ type RolesAnalyticsDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const rolesAnalyticsReadToolDefinitions: ToolDef[] = [
|
||||
export const rolesAnalyticsReadToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -109,9 +109,22 @@ export const rolesAnalyticsReadToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
list_roles: {
|
||||
requiresPlanningRead: true,
|
||||
},
|
||||
search_by_skill: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_statistics: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_chargeability: {
|
||||
requiresCostView: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = [
|
||||
export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -160,7 +173,20 @@ export const rolesAnalyticsMutationToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
create_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
update_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
delete_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
requiredPermissions: [PermissionKey.MANAGE_ROLES],
|
||||
},
|
||||
});
|
||||
|
||||
export function createRolesAnalyticsExecutors(
|
||||
deps: RolesAnalyticsDeps,
|
||||
|
||||
@@ -142,6 +142,7 @@ export const scenarioRateAnalysisToolDefinitions: ToolDef[] = withToolAccess([
|
||||
], {
|
||||
lookup_rate: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
requiredPermissions: [PermissionKey.VIEW_COSTS],
|
||||
},
|
||||
simulate_scenario: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
|
||||
@@ -0,0 +1,809 @@
|
||||
import { isAiConfigured } from "../../ai-client.js";
|
||||
import { resolveSystemSettingsRuntime } from "../../lib/system-settings-runtime.js";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
export const settingsAdminToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_system_settings",
|
||||
description: "Get sanitized system settings through the real settings router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_system_settings",
|
||||
description: "Update non-secret system settings through the real settings router. Runtime secrets must be provisioned via deployment environment or secret manager. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
aiProvider: { type: "string", enum: ["openai", "azure"] },
|
||||
azureOpenAiEndpoint: { type: "string" },
|
||||
azureOpenAiDeployment: { type: "string" },
|
||||
azureApiVersion: { type: "string" },
|
||||
aiMaxCompletionTokens: { type: "integer" },
|
||||
aiTemperature: { type: "number" },
|
||||
aiSummaryPrompt: { type: "string" },
|
||||
scoreWeights: { type: "object" },
|
||||
scoreVisibleRoles: { type: "array", items: { type: "string" } },
|
||||
smtpHost: { type: "string" },
|
||||
smtpPort: { type: "integer" },
|
||||
smtpUser: { type: "string" },
|
||||
smtpFrom: { type: "string" },
|
||||
smtpTls: { type: "boolean" },
|
||||
anonymizationEnabled: { type: "boolean" },
|
||||
anonymizationDomain: { type: "string" },
|
||||
anonymizationMode: { type: "string", enum: ["global"] },
|
||||
azureDalleDeployment: { type: "string" },
|
||||
azureDalleEndpoint: { type: "string" },
|
||||
geminiModel: { type: "string" },
|
||||
imageProvider: { type: "string", enum: ["dalle", "gemini"] },
|
||||
vacationDefaultDays: { type: "integer" },
|
||||
timelineUndoMaxSteps: { type: "integer" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "clear_stored_runtime_secrets",
|
||||
description: "Clear legacy database-stored runtime secrets after they have been migrated to deployment secret management. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_ai_connection",
|
||||
description: "Run the real AI connection test from system settings. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_smtp_connection",
|
||||
description: "Run the real SMTP connection test from system settings. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_gemini_connection",
|
||||
description: "Run the real Gemini connection test from system settings. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_ai_configured",
|
||||
description: "Get whether AI is configured for the current system via the real settings router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_system_role_configs",
|
||||
description: "List system role configuration defaults via the real system-role-config router. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_system_role_config",
|
||||
description: "Update one system role configuration via the real system-role-config router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
role: { type: "string", description: "System role key." },
|
||||
label: { type: "string", description: "Optional role label." },
|
||||
description: { type: "string", description: "Optional role description." },
|
||||
color: { type: "string", description: "Optional role color." },
|
||||
defaultPermissions: { type: "array", items: { type: "string" }, description: "Optional default permission set." },
|
||||
},
|
||||
required: ["role"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_webhooks",
|
||||
description: "List webhooks via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_webhook",
|
||||
description: "Get one webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Webhook ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_webhook",
|
||||
description: "Create a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Webhook name." },
|
||||
url: { type: "string", description: "Webhook target URL." },
|
||||
secret: { type: "string", description: "Optional webhook signing secret." },
|
||||
events: { type: "array", items: { type: "string" }, description: "Subscribed webhook events." },
|
||||
isActive: { type: "boolean", description: "Whether the webhook is active. Default: true." },
|
||||
},
|
||||
required: ["name", "url", "events"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_webhook",
|
||||
description: "Update a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Webhook ID." },
|
||||
data: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
url: { type: "string" },
|
||||
secret: { type: "string" },
|
||||
events: { type: "array", items: { type: "string" } },
|
||||
isActive: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_webhook",
|
||||
description: "Delete a webhook via the real webhook router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Webhook ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "test_webhook",
|
||||
description: "Send a real test payload to a webhook via the real webhook router. Admin role required. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Webhook ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_audit_log_entries",
|
||||
description: "List audit log entries with full audit-router filters and cursor pagination. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
entityType: { type: "string", description: "Optional entity type filter." },
|
||||
entityId: { type: "string", description: "Optional entity ID filter." },
|
||||
userId: { type: "string", description: "Optional user ID filter." },
|
||||
action: { type: "string", description: "Optional action filter such as CREATE, UPDATE, DELETE, SHIFT, IMPORT." },
|
||||
source: { type: "string", description: "Optional source filter such as ui or assistant." },
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||
search: { type: "string", description: "Optional case-insensitive search across entity name, summary, and entity type." },
|
||||
limit: { type: "integer", description: "Max results. Default: 50, max: 100." },
|
||||
cursor: { type: "string", description: "Optional pagination cursor (last seen audit entry ID)." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_audit_log_entry",
|
||||
description: "Get one audit log entry including the full changes payload. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "Audit log entry ID." },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_audit_log_timeline",
|
||||
description: "Get audit log entries grouped by day for a time window. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||
limit: { type: "integer", description: "Max entries. Default: 200, max: 500." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_audit_activity_summary",
|
||||
description: "Get audit activity totals by entity type, action, and user for a date range. Controller/manager/admin roles only.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
|
||||
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_shoring_ratio",
|
||||
description: "Get the onshore/offshore staffing ratio for a project. Higher offshore is better (cost-efficient). The threshold is the MINIMUM offshore target. Shows country breakdown and whether the target is met.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
projectId: { type: "string", description: "Project ID or short code" },
|
||||
},
|
||||
required: ["projectId"],
|
||||
},
|
||||
},
|
||||
},
|
||||
], {
|
||||
get_system_settings: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_system_settings: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
clear_stored_runtime_secrets: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
test_ai_connection: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
test_smtp_connection: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
test_gemini_connection: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_ai_configured: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
list_system_role_configs: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_system_role_config: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
list_webhooks: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_webhook: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_webhook: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_webhook: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
delete_webhook: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
test_webhook: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
list_audit_log_entries: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_audit_log_entry: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_audit_log_timeline: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_audit_activity_summary: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
get_shoring_ratio: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
|
||||
},
|
||||
});
|
||||
|
||||
type SettingsAdminDeps = {
|
||||
createSettingsCaller: (ctx: TRPCContext) => {
|
||||
getSystemSettings: () => Promise<unknown>;
|
||||
updateSystemSettings: (params: {
|
||||
aiProvider?: "openai" | "azure";
|
||||
azureOpenAiEndpoint?: string;
|
||||
azureOpenAiDeployment?: string;
|
||||
azureApiVersion?: string;
|
||||
aiMaxCompletionTokens?: number;
|
||||
aiTemperature?: number;
|
||||
aiSummaryPrompt?: string;
|
||||
scoreWeights?: {
|
||||
skillDepth: number;
|
||||
skillBreadth: number;
|
||||
costEfficiency: number;
|
||||
chargeability: number;
|
||||
experience: number;
|
||||
};
|
||||
scoreVisibleRoles?: SystemRole[];
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
smtpUser?: string;
|
||||
smtpFrom?: string;
|
||||
smtpTls?: boolean;
|
||||
anonymizationEnabled?: boolean;
|
||||
anonymizationDomain?: string;
|
||||
anonymizationMode?: "global";
|
||||
azureDalleDeployment?: string;
|
||||
azureDalleEndpoint?: string;
|
||||
geminiModel?: string;
|
||||
imageProvider?: "dalle" | "gemini";
|
||||
vacationDefaultDays?: number;
|
||||
timelineUndoMaxSteps?: number;
|
||||
}) => Promise<unknown>;
|
||||
clearStoredRuntimeSecrets: () => Promise<unknown>;
|
||||
testAiConnection: () => Promise<unknown>;
|
||||
testSmtpConnection: () => Promise<unknown>;
|
||||
testGeminiConnection: () => Promise<unknown>;
|
||||
};
|
||||
createSystemRoleConfigCaller: (ctx: TRPCContext) => {
|
||||
list: () => Promise<unknown>;
|
||||
update: (params: {
|
||||
role: string;
|
||||
label?: string;
|
||||
description?: string | null;
|
||||
color?: string | null;
|
||||
defaultPermissions?: string[];
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createWebhookCaller: (ctx: TRPCContext) => {
|
||||
list: () => Promise<Array<{ secret?: string | null }>>;
|
||||
getById: (params: { id: string }) => Promise<{ secret?: string | null }>;
|
||||
create: (params: {
|
||||
name: string;
|
||||
url: string;
|
||||
secret?: string;
|
||||
events: [string, ...string[]];
|
||||
isActive?: boolean;
|
||||
}) => Promise<{ secret?: string | null }>;
|
||||
update: (params: {
|
||||
id: string;
|
||||
data: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
secret?: string | null;
|
||||
events?: [string, ...string[]];
|
||||
isActive?: boolean;
|
||||
};
|
||||
}) => Promise<{ secret?: string | null }>;
|
||||
delete: (params: { id: string }) => Promise<unknown>;
|
||||
test: (params: { id: string }) => Promise<unknown>;
|
||||
};
|
||||
createAuditLogCaller: (ctx: TRPCContext) => {
|
||||
listDetail: (params: {
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
userId?: string;
|
||||
action?: string;
|
||||
source?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}) => Promise<{ items: unknown[]; nextCursor?: string | null }>;
|
||||
getByIdDetail: (params: { id: string }) => Promise<unknown>;
|
||||
getTimelineDetail: (params: {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
limit?: number;
|
||||
}) => Promise<unknown>;
|
||||
getActivitySummary: (params: {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}) => Promise<unknown>;
|
||||
};
|
||||
createProjectCaller: (ctx: TRPCContext) => {
|
||||
getShoringRatio: (params: { projectId: string }) => Promise<{
|
||||
totalHours: number;
|
||||
byCountry: Record<string, { pct: number; resourceCount: number }>;
|
||||
offshoreRatio: number;
|
||||
threshold: number;
|
||||
onshoreRatio: number;
|
||||
onshoreCountryCode: string;
|
||||
unknownCount: number;
|
||||
}>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
parseIsoDate: (value: string, fieldName: string) => Date;
|
||||
resolveProjectIdentifier: (
|
||||
ctx: ToolContext,
|
||||
identifier: string,
|
||||
) => Promise<{ id: string; name: string; shortCode: string } | { error: string }>;
|
||||
sanitizeWebhook: <T extends { secret?: string | null }>(webhook: T) => Omit<T, "secret"> & { hasSecret: boolean };
|
||||
sanitizeWebhookList: <T extends { secret?: string | null }>(webhooks: T[]) => Array<Omit<T, "secret"> & { hasSecret: boolean }>;
|
||||
toAssistantWebhookNotFoundError: (error: unknown) => unknown;
|
||||
toAssistantWebhookMutationError: (error: unknown, action?: "create" | "update") => unknown;
|
||||
toAssistantAuditLogEntryNotFoundError: (error: unknown) => unknown;
|
||||
};
|
||||
|
||||
export function createSettingsAdminExecutors(deps: SettingsAdminDeps): Record<string, ToolExecutor> {
|
||||
return {
|
||||
async get_system_settings(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getSystemSettings();
|
||||
},
|
||||
|
||||
async update_system_settings(params: {
|
||||
aiProvider?: "openai" | "azure";
|
||||
azureOpenAiEndpoint?: string;
|
||||
azureOpenAiDeployment?: string;
|
||||
azureApiVersion?: string;
|
||||
aiMaxCompletionTokens?: number;
|
||||
aiTemperature?: number;
|
||||
aiSummaryPrompt?: string;
|
||||
scoreWeights?: {
|
||||
skillDepth: number;
|
||||
skillBreadth: number;
|
||||
costEfficiency: number;
|
||||
chargeability: number;
|
||||
experience: number;
|
||||
};
|
||||
scoreVisibleRoles?: SystemRole[];
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
smtpUser?: string;
|
||||
smtpFrom?: string;
|
||||
smtpTls?: boolean;
|
||||
anonymizationEnabled?: boolean;
|
||||
anonymizationDomain?: string;
|
||||
anonymizationMode?: "global";
|
||||
azureDalleDeployment?: string;
|
||||
azureDalleEndpoint?: string;
|
||||
geminiModel?: string;
|
||||
imageProvider?: "dalle" | "gemini";
|
||||
vacationDefaultDays?: number;
|
||||
timelineUndoMaxSteps?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.updateSystemSettings(params);
|
||||
},
|
||||
|
||||
async clear_stored_runtime_secrets(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.clearStoredRuntimeSecrets();
|
||||
},
|
||||
|
||||
async test_ai_connection(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.testAiConnection();
|
||||
},
|
||||
|
||||
async test_smtp_connection(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.testSmtpConnection();
|
||||
},
|
||||
|
||||
async test_gemini_connection(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSettingsCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.testGeminiConnection();
|
||||
},
|
||||
|
||||
async get_ai_configured(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const settings = resolveSystemSettingsRuntime(await ctx.db.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
aiProvider: true,
|
||||
azureOpenAiEndpoint: true,
|
||||
azureOpenAiDeployment: true,
|
||||
azureOpenAiApiKey: true,
|
||||
},
|
||||
}));
|
||||
return { configured: isAiConfigured(settings) };
|
||||
},
|
||||
|
||||
async list_system_role_configs(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createSystemRoleConfigCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.list();
|
||||
},
|
||||
|
||||
async update_system_role_config(params: {
|
||||
role: string;
|
||||
label?: string;
|
||||
description?: string | null;
|
||||
color?: string | null;
|
||||
defaultPermissions?: string[];
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createSystemRoleConfigCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.update(params);
|
||||
},
|
||||
|
||||
async list_webhooks(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
const webhooks = await caller.list();
|
||||
return deps.sanitizeWebhookList(webhooks);
|
||||
},
|
||||
|
||||
async get_webhook(params: {
|
||||
id: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
let webhook;
|
||||
try {
|
||||
webhook = await caller.getById({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantWebhookNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return deps.sanitizeWebhook(webhook);
|
||||
},
|
||||
|
||||
async create_webhook(params: {
|
||||
name: string;
|
||||
url: string;
|
||||
secret?: string;
|
||||
events: string[];
|
||||
isActive?: boolean;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
let webhook;
|
||||
try {
|
||||
webhook = await caller.create({
|
||||
name: params.name,
|
||||
url: params.url,
|
||||
events: params.events as [string, ...string[]],
|
||||
...(params.secret !== undefined ? { secret: params.secret } : {}),
|
||||
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantWebhookMutationError(error, "create");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return deps.sanitizeWebhook(webhook);
|
||||
},
|
||||
|
||||
async update_webhook(params: {
|
||||
id: string;
|
||||
data: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
secret?: string | null;
|
||||
events?: string[];
|
||||
isActive?: boolean;
|
||||
};
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
let webhook;
|
||||
try {
|
||||
webhook = await caller.update({
|
||||
id: params.id,
|
||||
data: {
|
||||
...(params.data.name !== undefined ? { name: params.data.name } : {}),
|
||||
...(params.data.url !== undefined ? { url: params.data.url } : {}),
|
||||
...(params.data.secret !== undefined ? { secret: params.data.secret } : {}),
|
||||
...(params.data.events !== undefined ? { events: params.data.events as [string, ...string[]] } : {}),
|
||||
...(params.data.isActive !== undefined ? { isActive: params.data.isActive } : {}),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantWebhookMutationError(error, "update");
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return deps.sanitizeWebhook(webhook);
|
||||
},
|
||||
|
||||
async delete_webhook(params: {
|
||||
id: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
try {
|
||||
await caller.delete({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantWebhookNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return { ok: true, id: params.id };
|
||||
},
|
||||
|
||||
async test_webhook(params: {
|
||||
id: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createWebhookCaller(deps.createScopedCallerContext(ctx));
|
||||
try {
|
||||
return await caller.test({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantWebhookNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async list_audit_log_entries(params: {
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
userId?: string;
|
||||
action?: string;
|
||||
source?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.listDetail({
|
||||
...(params.entityType ? { entityType: params.entityType } : {}),
|
||||
...(params.entityId ? { entityId: params.entityId } : {}),
|
||||
...(params.userId ? { userId: params.userId } : {}),
|
||||
...(params.action ? { action: params.action } : {}),
|
||||
...(params.source ? { source: params.source } : {}),
|
||||
...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
|
||||
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(params.cursor ? { cursor: params.cursor } : {}),
|
||||
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 100) } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
filters: {
|
||||
entityType: params.entityType ?? null,
|
||||
entityId: params.entityId ?? null,
|
||||
userId: params.userId ?? null,
|
||||
action: params.action ?? null,
|
||||
source: params.source ?? null,
|
||||
startDate: params.startDate ?? null,
|
||||
endDate: params.endDate ?? null,
|
||||
search: params.search ?? null,
|
||||
},
|
||||
itemCount: result.items.length,
|
||||
nextCursor: result.nextCursor ?? null,
|
||||
items: result.items,
|
||||
};
|
||||
},
|
||||
|
||||
async get_audit_log_entry(params: {
|
||||
id: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
|
||||
try {
|
||||
return await caller.getByIdDetail({ id: params.id });
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantAuditLogEntryNotFoundError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async get_audit_log_timeline(params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getTimelineDetail({
|
||||
...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
|
||||
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
||||
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 500) } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async get_audit_activity_summary(params: {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createAuditLogCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getActivitySummary({
|
||||
...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
|
||||
...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
|
||||
});
|
||||
},
|
||||
|
||||
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
|
||||
const project = await deps.resolveProjectIdentifier(ctx, params.projectId);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const caller = deps.createProjectCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.getShoringRatio({ projectId: project.id });
|
||||
|
||||
if (result.totalHours <= 0) {
|
||||
return `Project "${project.name}" (${project.shortCode}): No active assignments — shoring ratio not available.`;
|
||||
}
|
||||
|
||||
const countryParts = Object.entries(result.byCountry)
|
||||
.sort((a, b) => b[1].pct - a[1].pct)
|
||||
.map(([code, info]) => `${code} ${info.pct}% (${info.resourceCount} people)`)
|
||||
.join(", ");
|
||||
|
||||
const status = result.offshoreRatio >= result.threshold
|
||||
? `Target met (>=${result.threshold}% offshore)`
|
||||
: result.offshoreRatio >= result.threshold - 10
|
||||
? `Close to target (${result.threshold}% offshore needed)`
|
||||
: `Below target — only ${result.offshoreRatio}% offshore, need ${result.threshold}%`;
|
||||
|
||||
return `Project "${project.name}" (${project.shortCode}): ${result.onshoreRatio}% onshore (${result.onshoreCountryCode}), ${result.offshoreRatio}% offshore. ${status}. Breakdown: ${countryParts}.${result.unknownCount > 0 ? ` (${result.unknownCount} resource(s) without country)` : ""}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { prisma } from "@capakraken/db";
|
||||
import type { PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
|
||||
export type ToolContext = {
|
||||
db: typeof prisma;
|
||||
userId: string;
|
||||
userRole: string;
|
||||
permissions: Set<PermissionKey>;
|
||||
session?: TRPCContext["session"];
|
||||
dbUser?: TRPCContext["dbUser"];
|
||||
roleDefaults?: TRPCContext["roleDefaults"];
|
||||
};
|
||||
|
||||
export interface ToolAccessRequirements {
|
||||
requiredPermissions?: PermissionKey[];
|
||||
allowedSystemRoles?: SystemRole[];
|
||||
requiresPlanningRead?: boolean;
|
||||
requiresCostView?: boolean;
|
||||
requiresAdvancedAssistant?: boolean;
|
||||
requiresResourceOverview?: boolean;
|
||||
}
|
||||
|
||||
export interface ToolDef {
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
};
|
||||
access?: ToolAccessRequirements;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
|
||||
|
||||
export function withToolAccess(
|
||||
tools: ToolDef[],
|
||||
accessByName: Partial<Record<string, ToolAccessRequirements>>,
|
||||
): ToolDef[] {
|
||||
return tools.map((tool) => ({
|
||||
...tool,
|
||||
...(accessByName[tool.function.name]
|
||||
? { access: accessByName[tool.function.name] }
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -48,7 +48,7 @@ type UserAdminDeps = {
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const userAdminToolDefinitions: ToolDef[] = [
|
||||
export const userAdminToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -212,7 +212,41 @@ export const userAdminToolDefinitions: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
list_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
create_user: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_password: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_role: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
update_user_name: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
link_user_resource: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
auto_link_users_by_email: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
set_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
reset_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
get_effective_user_permissions: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
disable_user_totp: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
|
||||
export function createUserAdminExecutors(
|
||||
deps: UserAdminDeps,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import type { TRPCContext } from "../../trpc.js";
|
||||
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
|
||||
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
|
||||
|
||||
type AssistantToolErrorResult = { error: string };
|
||||
|
||||
@@ -35,12 +36,15 @@ type UserSelfServiceDeps = {
|
||||
activeCount: () => Promise<unknown>;
|
||||
};
|
||||
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
|
||||
toAssistantCurrentUserError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
toAssistantTotpEnableError: (
|
||||
error: unknown,
|
||||
) => AssistantToolErrorResult | null;
|
||||
};
|
||||
|
||||
export const userSelfServiceToolDefinitions: ToolDef[] = [
|
||||
export const userSelfServiceToolDefinitions: ToolDef[] = withToolAccess([
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
@@ -187,11 +191,30 @@ export const userSelfServiceToolDefinitions: ToolDef[] = [
|
||||
parameters: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
], {
|
||||
list_assignable_users: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
|
||||
},
|
||||
get_active_user_count: {
|
||||
allowedSystemRoles: [SystemRole.ADMIN],
|
||||
},
|
||||
});
|
||||
|
||||
export function createUserSelfServiceExecutors(
|
||||
deps: UserSelfServiceDeps,
|
||||
): Record<string, ToolExecutor> {
|
||||
async function withCurrentUserErrorMapping<T>(run: () => Promise<T>) {
|
||||
try {
|
||||
return await run();
|
||||
} catch (error) {
|
||||
const mapped = deps.toAssistantCurrentUserError(error);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async list_assignable_users(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
@@ -200,17 +223,22 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async get_current_user(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.me();
|
||||
return withCurrentUserErrorMapping(() => caller.me());
|
||||
},
|
||||
|
||||
async get_dashboard_layout(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getDashboardLayout();
|
||||
return withCurrentUserErrorMapping(() => caller.getDashboardLayout());
|
||||
},
|
||||
|
||||
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.saveDashboardLayout({ layout: params.layout });
|
||||
const result = await withCurrentUserErrorMapping(
|
||||
() => caller.saveDashboardLayout({ layout: params.layout }),
|
||||
);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["dashboard"],
|
||||
@@ -222,12 +250,17 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async get_favorite_project_ids(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getFavoriteProjectIds();
|
||||
return withCurrentUserErrorMapping(() => caller.getFavoriteProjectIds());
|
||||
},
|
||||
|
||||
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.toggleFavoriteProject({ projectId: params.projectId });
|
||||
const result = await withCurrentUserErrorMapping(
|
||||
() => caller.toggleFavoriteProject({ projectId: params.projectId }),
|
||||
);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["project"],
|
||||
@@ -239,7 +272,7 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async get_column_preferences(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getColumnPreferences();
|
||||
return withCurrentUserErrorMapping(() => caller.getColumnPreferences());
|
||||
},
|
||||
|
||||
async set_column_preferences(params: {
|
||||
@@ -249,12 +282,15 @@ export function createUserSelfServiceExecutors(
|
||||
rowOrder?: string[] | null;
|
||||
}, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.setColumnPreferences({
|
||||
const result = await withCurrentUserErrorMapping(() => caller.setColumnPreferences({
|
||||
view: params.view,
|
||||
...(params.visible !== undefined ? { visible: params.visible } : {}),
|
||||
...(params.sort !== undefined ? { sort: params.sort } : {}),
|
||||
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
|
||||
});
|
||||
}));
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["user"],
|
||||
@@ -266,7 +302,10 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async generate_totp_secret(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
const result = await caller.generateTotpSecret();
|
||||
const result = await withCurrentUserErrorMapping(() => caller.generateTotpSecret());
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
__action: "invalidate" as const,
|
||||
scope: ["user"],
|
||||
@@ -299,7 +338,7 @@ export function createUserSelfServiceExecutors(
|
||||
|
||||
async get_mfa_status(_params: Record<string, never>, ctx: ToolContext) {
|
||||
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
|
||||
return caller.getMfaStatus();
|
||||
return withCurrentUserErrorMapping(() => caller.getMfaStatus());
|
||||
},
|
||||
|
||||
async get_active_user_count(_params: Record<string, never>, ctx: ToolContext) {
|
||||
|
||||
Reference in New Issue
Block a user