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 ─────────────────────────────────────────────────────────
|
// ─── 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 ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user