feat(assistant): add approval inbox and e2e hardening
This commit is contained in:
@@ -20,6 +20,13 @@ import {
|
||||
getAvailabilityHoursForDate,
|
||||
loadResourceDailyAvailabilityContexts,
|
||||
} from "../lib/resource-capacity.js";
|
||||
import {
|
||||
loadTimelineEntriesReadModel,
|
||||
loadTimelineHolidayOverlays,
|
||||
loadTimelineProjectContext,
|
||||
previewTimelineProjectShift,
|
||||
type TimelineEntriesFilters,
|
||||
} from "./timeline.js";
|
||||
import { resolveRecipients } from "../lib/notification-targeting.js";
|
||||
import {
|
||||
emitNotificationCreated,
|
||||
@@ -29,10 +36,11 @@ import {
|
||||
emitBroadcastSent,
|
||||
} from "../sse/event-bus.js";
|
||||
import { logger } from "../lib/logger.js";
|
||||
import type { TRPCContext } from "../trpc.js";
|
||||
|
||||
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
|
||||
|
||||
const MUTATION_TOOLS = new Set([
|
||||
export const MUTATION_TOOLS = new Set([
|
||||
"create_allocation", "cancel_allocation", "update_allocation_status",
|
||||
"update_resource", "deactivate_resource", "create_resource",
|
||||
"update_project", "create_project", "delete_project",
|
||||
@@ -49,6 +57,10 @@ const MUTATION_TOOLS = new Set([
|
||||
|
||||
export const ADVANCED_ASSISTANT_TOOLS = new Set([
|
||||
"find_best_project_resource",
|
||||
"get_timeline_entries_view",
|
||||
"get_timeline_holiday_overlays",
|
||||
"get_project_timeline_context",
|
||||
"preview_project_shift",
|
||||
]);
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
@@ -58,6 +70,9 @@ export type ToolContext = {
|
||||
userId: string;
|
||||
userRole: string;
|
||||
permissions: Set<PermissionKey>;
|
||||
session?: TRPCContext["session"];
|
||||
dbUser?: TRPCContext["dbUser"];
|
||||
roleDefaults?: TRPCContext["roleDefaults"];
|
||||
};
|
||||
|
||||
export interface ToolDef {
|
||||
@@ -206,6 +221,127 @@ function createDateRange(input: {
|
||||
return { startDate, endDate };
|
||||
}
|
||||
|
||||
function normalizeStringList(values?: string[] | undefined): string[] | undefined {
|
||||
const normalized = values
|
||||
?.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
|
||||
return normalized && normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function createTimelineFilters(input: {
|
||||
resourceIds?: string[] | undefined;
|
||||
projectIds?: string[] | undefined;
|
||||
clientIds?: string[] | undefined;
|
||||
chapters?: string[] | undefined;
|
||||
eids?: string[] | undefined;
|
||||
countryCodes?: string[] | undefined;
|
||||
}): Omit<TimelineEntriesFilters, "startDate" | "endDate"> {
|
||||
return {
|
||||
resourceIds: normalizeStringList(input.resourceIds),
|
||||
projectIds: normalizeStringList(input.projectIds),
|
||||
clientIds: normalizeStringList(input.clientIds),
|
||||
chapters: normalizeStringList(input.chapters),
|
||||
eids: normalizeStringList(input.eids),
|
||||
countryCodes: normalizeStringList(input.countryCodes),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeTimelineEntries(readModel: {
|
||||
allocations: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||
demands: Array<{ projectId: string | null }>;
|
||||
assignments: Array<{ projectId: string | null; resourceId: string | null }>;
|
||||
}) {
|
||||
const projectIds = new Set<string>();
|
||||
const resourceIds = new Set<string>();
|
||||
|
||||
for (const entry of [...readModel.allocations, ...readModel.demands, ...readModel.assignments]) {
|
||||
if (entry.projectId) {
|
||||
projectIds.add(entry.projectId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const assignment of [...readModel.allocations, ...readModel.assignments]) {
|
||||
if (assignment.resourceId) {
|
||||
resourceIds.add(assignment.resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allocationCount: readModel.allocations.length,
|
||||
demandCount: readModel.demands.length,
|
||||
assignmentCount: readModel.assignments.length,
|
||||
projectCount: projectIds.size,
|
||||
resourceCount: resourceIds.size,
|
||||
};
|
||||
}
|
||||
|
||||
function formatHolidayOverlays(
|
||||
overlays: Array<{
|
||||
id: string;
|
||||
resourceId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
note?: string | null;
|
||||
scope?: string | null;
|
||||
calendarName?: string | null;
|
||||
sourceType?: string | null;
|
||||
}>,
|
||||
) {
|
||||
return overlays.map((overlay) => ({
|
||||
id: overlay.id,
|
||||
resourceId: overlay.resourceId,
|
||||
startDate: fmtDate(overlay.startDate),
|
||||
endDate: fmtDate(overlay.endDate),
|
||||
note: overlay.note ?? null,
|
||||
scope: overlay.scope ?? null,
|
||||
calendarName: overlay.calendarName ?? null,
|
||||
sourceType: overlay.sourceType ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function summarizeHolidayOverlays(
|
||||
overlays: ReturnType<typeof formatHolidayOverlays>,
|
||||
) {
|
||||
const resourceIds = new Set<string>();
|
||||
const byScope = new Map<string, number>();
|
||||
|
||||
for (const overlay of overlays) {
|
||||
resourceIds.add(overlay.resourceId);
|
||||
const scope = overlay.scope ?? "UNKNOWN";
|
||||
byScope.set(scope, (byScope.get(scope) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
overlayCount: overlays.length,
|
||||
holidayResourceCount: resourceIds.size,
|
||||
byScope: [...byScope.entries()]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([scope, count]) => ({ scope, count })),
|
||||
};
|
||||
}
|
||||
|
||||
function rangesOverlap(
|
||||
leftStart: Date,
|
||||
leftEnd: Date,
|
||||
rightStart: Date,
|
||||
rightEnd: Date,
|
||||
): boolean {
|
||||
return leftStart <= rightEnd && rightStart <= leftEnd;
|
||||
}
|
||||
|
||||
function parseIsoDate(value: string, fieldName: string): Date {
|
||||
const parsed = new Date(`${value}T00:00:00.000Z`);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
throw new Error(`Invalid ${fieldName}: ${value}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function toDate(value: Date | string): Date {
|
||||
return value instanceof Date ? value : new Date(value);
|
||||
}
|
||||
|
||||
async function resolveProjectIdentifier(
|
||||
identifier: string,
|
||||
db: ToolContext["db"],
|
||||
@@ -334,6 +470,81 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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: {
|
||||
@@ -822,7 +1033,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "set_entitlement",
|
||||
description: "Set vacation entitlement for a resource for a year. Requires admin permission. Always confirm first.",
|
||||
description: "Set vacation entitlement for a resource for a year. Requires manageVacations permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1014,7 +1225,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_estimate",
|
||||
description: "Create a new estimate for a project. Requires manageEstimates permission. Always confirm first.",
|
||||
description: "Create a new estimate for a project. Requires manageProjects permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1159,7 +1370,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_users",
|
||||
description: "List system users with their roles and linked resources. Requires admin permission.",
|
||||
description: "List system users with their roles and linked resources. Requires manageUsers permission.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1236,7 +1447,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_org_unit",
|
||||
description: "Create a new organizational unit. Requires admin permission. Always confirm first.",
|
||||
description: "Create a new organizational unit. Requires manageResources permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1253,7 +1464,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_org_unit",
|
||||
description: "Update an organizational unit. Requires admin permission. Always confirm first.",
|
||||
description: "Update an organizational unit. Requires manageResources permission. Always confirm first.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1378,7 +1589,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_task_for_user",
|
||||
description: "Create a task for a specific user. Requires manageProjects or manageResources permission. The task appears in their task list.",
|
||||
description: "Create a task for a specific user. Requires manageProjects permission. The task appears in their task list.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1399,7 +1610,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
type: "function",
|
||||
function: {
|
||||
name: "send_broadcast",
|
||||
description: "Send a notification to a group of users (by role, project members, org unit, or all). Requires manager permission.",
|
||||
description: "Send a notification to a group of users (by role, project members, org unit, or all). Requires manageProjects permission.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -2251,6 +2462,222 @@ const executors = {
|
||||
};
|
||||
},
|
||||
|
||||
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) {
|
||||
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const { startDate, endDate } = createDateRange(params);
|
||||
const filters = createTimelineFilters(params);
|
||||
const input = { ...filters, startDate, endDate };
|
||||
|
||||
const [readModel, holidayOverlays] = await Promise.all([
|
||||
loadTimelineEntriesReadModel(ctx.db, input),
|
||||
loadTimelineHolidayOverlays(ctx.db, input),
|
||||
]);
|
||||
const formattedHolidayOverlays = formatHolidayOverlays(holidayOverlays);
|
||||
|
||||
return {
|
||||
period: {
|
||||
startDate: fmtDate(startDate),
|
||||
endDate: fmtDate(endDate),
|
||||
},
|
||||
filters,
|
||||
summary: {
|
||||
...summarizeTimelineEntries(readModel),
|
||||
...summarizeHolidayOverlays(formattedHolidayOverlays),
|
||||
},
|
||||
allocations: readModel.allocations,
|
||||
demands: readModel.demands,
|
||||
assignments: readModel.assignments,
|
||||
holidayOverlays: formattedHolidayOverlays,
|
||||
};
|
||||
},
|
||||
|
||||
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) {
|
||||
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const { startDate, endDate } = createDateRange(params);
|
||||
const filters = createTimelineFilters(params);
|
||||
const holidayOverlays = await loadTimelineHolidayOverlays(ctx.db, {
|
||||
...filters,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
const formattedOverlays = formatHolidayOverlays(holidayOverlays);
|
||||
|
||||
return {
|
||||
period: {
|
||||
startDate: fmtDate(startDate),
|
||||
endDate: fmtDate(endDate),
|
||||
},
|
||||
filters,
|
||||
summary: summarizeHolidayOverlays(formattedOverlays),
|
||||
overlays: formattedOverlays,
|
||||
};
|
||||
},
|
||||
|
||||
async get_project_timeline_context(params: {
|
||||
projectIdentifier: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
durationDays?: number;
|
||||
}, ctx: ToolContext) {
|
||||
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const project = await resolveProjectIdentifier(params.projectIdentifier, ctx.db);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const projectContext = await loadTimelineProjectContext(ctx.db, project.id);
|
||||
|
||||
const derivedStartDate = params.startDate
|
||||
? parseIsoDate(params.startDate, "startDate")
|
||||
: projectContext.project.startDate
|
||||
?? projectContext.assignments[0]?.startDate
|
||||
?? projectContext.demands[0]?.startDate
|
||||
?? createDateRange({ durationDays: 1 }).startDate;
|
||||
const derivedEndDate = params.endDate
|
||||
? parseIsoDate(params.endDate, "endDate")
|
||||
: projectContext.project.endDate
|
||||
?? createDateRange({
|
||||
startDate: fmtDate(derivedStartDate) ?? undefined,
|
||||
durationDays: params.durationDays ?? 21,
|
||||
}).endDate;
|
||||
|
||||
if (derivedEndDate < derivedStartDate) {
|
||||
throw new Error("endDate must be on or after startDate.");
|
||||
}
|
||||
|
||||
const holidayOverlays = projectContext.resourceIds.length > 0
|
||||
? await loadTimelineHolidayOverlays(ctx.db, {
|
||||
startDate: derivedStartDate,
|
||||
endDate: derivedEndDate,
|
||||
resourceIds: projectContext.resourceIds,
|
||||
projectIds: [project.id],
|
||||
})
|
||||
: [];
|
||||
|
||||
const formattedHolidayOverlays = formatHolidayOverlays(holidayOverlays);
|
||||
const assignmentConflicts = projectContext.assignments
|
||||
.filter((assignment) => assignment.resourceId && assignment.resource)
|
||||
.map((assignment) => {
|
||||
const overlaps = projectContext.allResourceAllocations
|
||||
.filter((booking) => (
|
||||
booking.resourceId === assignment.resourceId
|
||||
&& booking.id !== assignment.id
|
||||
&& rangesOverlap(
|
||||
toDate(booking.startDate),
|
||||
toDate(booking.endDate),
|
||||
toDate(assignment.startDate),
|
||||
toDate(assignment.endDate),
|
||||
)
|
||||
))
|
||||
.map((booking) => ({
|
||||
id: booking.id,
|
||||
projectId: booking.projectId,
|
||||
projectName: booking.project?.name ?? null,
|
||||
projectShortCode: booking.project?.shortCode ?? null,
|
||||
startDate: fmtDate(toDate(booking.startDate)),
|
||||
endDate: fmtDate(toDate(booking.endDate)),
|
||||
hoursPerDay: booking.hoursPerDay,
|
||||
status: booking.status,
|
||||
sameProject: booking.projectId === project.id,
|
||||
}));
|
||||
|
||||
return {
|
||||
assignmentId: assignment.id,
|
||||
resourceId: assignment.resourceId!,
|
||||
resourceName: assignment.resource?.displayName ?? null,
|
||||
startDate: fmtDate(toDate(assignment.startDate)),
|
||||
endDate: fmtDate(toDate(assignment.endDate)),
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
overlapCount: overlaps.length,
|
||||
crossProjectOverlapCount: overlaps.filter((booking) => !booking.sameProject).length,
|
||||
overlaps,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
project: projectContext.project,
|
||||
period: {
|
||||
startDate: fmtDate(derivedStartDate),
|
||||
endDate: fmtDate(derivedEndDate),
|
||||
},
|
||||
summary: {
|
||||
...summarizeTimelineEntries({
|
||||
allocations: projectContext.allocations,
|
||||
demands: projectContext.demands,
|
||||
assignments: projectContext.assignments,
|
||||
}),
|
||||
resourceIds: projectContext.resourceIds.length,
|
||||
allResourceAllocationCount: projectContext.allResourceAllocations.length,
|
||||
conflictedAssignmentCount: assignmentConflicts.filter((item) => item.crossProjectOverlapCount > 0).length,
|
||||
...summarizeHolidayOverlays(formattedHolidayOverlays),
|
||||
},
|
||||
allocations: projectContext.allocations,
|
||||
demands: projectContext.demands,
|
||||
assignments: projectContext.assignments,
|
||||
allResourceAllocations: projectContext.allResourceAllocations,
|
||||
assignmentConflicts,
|
||||
holidayOverlays: formattedHolidayOverlays,
|
||||
resourceIds: projectContext.resourceIds,
|
||||
};
|
||||
},
|
||||
|
||||
async preview_project_shift(params: {
|
||||
projectIdentifier: string;
|
||||
newStartDate: string;
|
||||
newEndDate: string;
|
||||
}, ctx: ToolContext) {
|
||||
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
|
||||
|
||||
const project = await resolveProjectIdentifier(params.projectIdentifier, ctx.db);
|
||||
if ("error" in project) {
|
||||
return project;
|
||||
}
|
||||
|
||||
const newStartDate = parseIsoDate(params.newStartDate, "newStartDate");
|
||||
const newEndDate = parseIsoDate(params.newEndDate, "newEndDate");
|
||||
if (newEndDate < newStartDate) {
|
||||
throw new Error("newEndDate must be on or after newStartDate.");
|
||||
}
|
||||
|
||||
const preview = await previewTimelineProjectShift(ctx.db, {
|
||||
projectId: project.id,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
});
|
||||
|
||||
return {
|
||||
project,
|
||||
requestedShift: {
|
||||
newStartDate: fmtDate(newStartDate),
|
||||
newEndDate: fmtDate(newEndDate),
|
||||
},
|
||||
preview,
|
||||
};
|
||||
},
|
||||
|
||||
async list_allocations(params: {
|
||||
resourceId?: string; projectId?: string;
|
||||
resourceName?: string; projectCode?: string;
|
||||
|
||||
Reference in New Issue
Block a user