From f0986fe721a1482c69eed658462f52530d49f1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 17 Mar 2026 14:58:01 +0100 Subject: [PATCH] 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 --- packages/api/src/router/assistant-tools.ts | 134 +++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/packages/api/src/router/assistant-tools.ts b/packages/api/src/router/assistant-tools.ts index 527d1d8..227368d 100644 --- a/packages/api/src/router/assistant-tools.ts +++ b/packages/api/src/router/assistant-tools.ts @@ -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[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 ───────────────────────────────────────────────────────────────