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:
2026-03-17 14:58:01 +01:00
parent 2a9a400cd4
commit f0986fe721
+134
View File
@@ -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 ───────────────────────────────────────────────────────── // ─── 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) }; 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 ─────────────────────────────────────────────────────────────── // ─── Executor ───────────────────────────────────────────────────────────────