feat: add create_project tool to AI assistant
Adds the missing create_project tool definition and executor to assistant-tools.ts, enabling the AI chatbot to create new projects when provided with the required information (shortCode, name, orderType, budgetCents, startDate, endDate). Includes validation for enums, short code format/uniqueness, date range, and optional blueprint/client lookup by name. Follows the same permission guard pattern as existing write tools. Closes #14 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -379,6 +379,32 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
function: {
|
||||
name: "create_project",
|
||||
description: "Create a new project. Requires manageProjects permission. Always confirm with the user before calling this. The project is created in DRAFT status by default.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
shortCode: { type: "string", description: "Unique project code, uppercase alphanumeric with hyphens/underscores (e.g. 'PROJ-001')" },
|
||||
name: { type: "string", description: "Project name" },
|
||||
orderType: { type: "string", description: "Order type: BD, CHARGEABLE, INTERNAL, OVERHEAD" },
|
||||
allocationType: { type: "string", description: "Allocation type: INT or EXT. Default: INT" },
|
||||
budgetCents: { type: "integer", description: "Budget in cents (e.g. 10000000 = 100,000 EUR)" },
|
||||
startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
|
||||
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
|
||||
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
|
||||
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
|
||||
responsiblePerson: { type: "string", description: "Name of the responsible person" },
|
||||
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
|
||||
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
|
||||
clientName: { type: "string", description: "Client name to look up and attach (partial match)" },
|
||||
},
|
||||
required: ["shortCode", "name", "orderType", "budgetCents", "startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Tool Executors ─────────────────────────────────────────────────────────
|
||||
@@ -1380,6 +1406,114 @@ const executors = {
|
||||
});
|
||||
return { __action: "invalidate", scope: ["project"], success: true, message: `Updated project ${project.name} (${project.shortCode})`, updatedFields: Object.keys(data) };
|
||||
},
|
||||
|
||||
async create_project(params: {
|
||||
shortCode: string; name: string; orderType: string;
|
||||
allocationType?: string; budgetCents: number;
|
||||
startDate: string; endDate: string;
|
||||
winProbability?: number; status?: string;
|
||||
responsiblePerson?: string; color?: string;
|
||||
blueprintName?: string; clientName?: string;
|
||||
}, ctx: ToolContext) {
|
||||
assertPermission(ctx, "manageProjects" as PermissionKey);
|
||||
|
||||
// Validate enums
|
||||
const validOrderTypes = ["BD", "CHARGEABLE", "INTERNAL", "OVERHEAD"];
|
||||
if (!validOrderTypes.includes(params.orderType)) {
|
||||
return { error: `Invalid orderType: ${params.orderType}. Valid: ${validOrderTypes.join(", ")}` };
|
||||
}
|
||||
const allocationType = params.allocationType ?? "INT";
|
||||
if (!["INT", "EXT"].includes(allocationType)) {
|
||||
return { error: `Invalid allocationType: ${allocationType}. Valid: INT, EXT` };
|
||||
}
|
||||
const status = params.status ?? "DRAFT";
|
||||
const validStatuses = ["DRAFT", "ACTIVE", "ON_HOLD", "COMPLETED", "CANCELLED"];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return { error: `Invalid status: ${status}. Valid: ${validStatuses.join(", ")}` };
|
||||
}
|
||||
|
||||
// Validate short code format
|
||||
if (!/^[A-Z0-9_-]+$/.test(params.shortCode)) {
|
||||
return { error: `Invalid shortCode: "${params.shortCode}". Must be uppercase alphanumeric with hyphens/underscores.` };
|
||||
}
|
||||
|
||||
// Check uniqueness
|
||||
const existing = await ctx.db.project.findUnique({
|
||||
where: { shortCode: params.shortCode },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
return { error: `A project with short code "${params.shortCode}" already exists.` };
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
const startDate = new Date(params.startDate);
|
||||
const endDate = new Date(params.endDate);
|
||||
if (isNaN(startDate.getTime())) return { error: `Invalid startDate: ${params.startDate}` };
|
||||
if (isNaN(endDate.getTime())) return { error: `Invalid endDate: ${params.endDate}` };
|
||||
if (endDate < startDate) return { error: "endDate must be after startDate" };
|
||||
|
||||
// Optional: look up blueprint by name
|
||||
let blueprintId: string | undefined;
|
||||
if (params.blueprintName) {
|
||||
const bp = await ctx.db.blueprint.findFirst({
|
||||
where: { name: { contains: params.blueprintName, mode: "insensitive" } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (!bp) return { error: `Blueprint not found: "${params.blueprintName}"` };
|
||||
blueprintId = bp.id;
|
||||
}
|
||||
|
||||
// Optional: look up client by name
|
||||
let clientId: string | undefined;
|
||||
if (params.clientName) {
|
||||
const client = await ctx.db.client.findFirst({
|
||||
where: { name: { contains: params.clientName, mode: "insensitive" } },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
if (!client) return { error: `Client not found: "${params.clientName}"` };
|
||||
clientId = client.id;
|
||||
}
|
||||
|
||||
const project = await ctx.db.project.create({
|
||||
data: {
|
||||
shortCode: params.shortCode,
|
||||
name: params.name,
|
||||
orderType: params.orderType,
|
||||
allocationType,
|
||||
budgetCents: params.budgetCents,
|
||||
startDate,
|
||||
endDate,
|
||||
winProbability: params.winProbability ?? 100,
|
||||
status,
|
||||
...(params.responsiblePerson ? { responsiblePerson: params.responsiblePerson } : {}),
|
||||
...(params.color ? { color: params.color } : {}),
|
||||
...(blueprintId ? { blueprintId } : {}),
|
||||
...(clientId ? { clientId } : {}),
|
||||
staffingReqs: [],
|
||||
dynamicFields: {},
|
||||
} as Parameters<typeof ctx.db.project.create>[0]["data"],
|
||||
select: { id: true, shortCode: true, name: true, status: true },
|
||||
});
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Project",
|
||||
entityId: project.id,
|
||||
action: "CREATE",
|
||||
changes: { after: project },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
__action: "invalidate",
|
||||
scope: ["project"],
|
||||
success: true,
|
||||
message: `Created project: ${project.name} (${project.shortCode}), budget ${fmtEur(params.budgetCents)}, ${params.startDate} to ${params.endDate}, status: ${project.status}`,
|
||||
projectId: project.id,
|
||||
shortCode: project.shortCode,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Executor ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user