Files
FEDEO/backend/src/mcp/tools/organisation.ts
root 6afc799f69
All checks were successful
Build and Push Docker Images / build-website (push) Successful in 22s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
Build and Push Docker Images / build-backend (push) Successful in 45s
Build and Push Docker Images / build-frontend (push) Successful in 22s
MCP-Projektphasen aktualisierbar machen
2026-08-31 11:08:53 +00:00

481 lines
19 KiB
TypeScript

import { and, desc, eq, ilike, or } from "drizzle-orm"
import { customers, events, plants, projects, tasks } from "../../../db/schema"
import { insertHistoryItem } from "../../utils/history"
import { activateProjectPhase } from "../projectPhases"
import { McpTool } from "../types"
const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
const raw = Number(args.limit ?? fallback)
if (!Number.isFinite(raw)) return fallback
return Math.min(Math.max(Math.trunc(raw), 1), 100)
}
const stringArg = (args: Record<string, unknown>, key: string) => {
const value = args[key]
return typeof value === "string" && value.trim() ? value.trim() : null
}
const numberArg = (args: Record<string, unknown>, key: string) => {
const value = Number(args[key])
return Number.isFinite(value) ? value : null
}
export const organisationTools: McpTool[] = [
{
name: "organisation.customers.search",
title: "Kunden suchen",
description: "Sucht aktive Kunden des aktiven Mandanten.",
requiredPermissions: ["organisation.customers.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Kundennummer, Vorname, Nachname oder Notizen." },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(customers.tenant, context.tenantId)]
const query = stringArg(args, "query")
if (query) {
conditions.push(or(
ilike(customers.name, `%${query}%`),
ilike(customers.customerNumber, `%${query}%`),
ilike(customers.firstname, `%${query}%`),
ilike(customers.lastname, `%${query}%`),
ilike(customers.notes, `%${query}%`)
))
}
if (args.includeArchived !== true) conditions.push(eq(customers.archived, false))
const rows = await context.server.db
.select({
id: customers.id,
customerNumber: customers.customerNumber,
name: customers.name,
firstname: customers.firstname,
lastname: customers.lastname,
type: customers.type,
isCompany: customers.isCompany,
active: customers.active,
archived: customers.archived,
})
.from(customers)
.where(and(...conditions))
.orderBy(customers.name)
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "organisation.projects.list",
title: "Projekte auflisten",
description: "Listet Projekte des aktiven Mandanten mit optionalen Filtern.",
requiredPermissions: ["organisation.projects.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Projektnummer, Kundenreferenz oder Notizen." },
customer: { type: "number" },
activePhase: { type: "string" },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(projects.tenant, context.tenantId)]
const query = stringArg(args, "query")
const customer = numberArg(args, "customer")
const activePhase = stringArg(args, "activePhase")
if (query) {
conditions.push(or(
ilike(projects.name, `%${query}%`),
ilike(projects.projectNumber, `%${query}%`),
ilike(projects.customerRef, `%${query}%`),
ilike(projects.notes, `%${query}%`)
))
}
if (customer) conditions.push(eq(projects.customer, customer))
if (activePhase) conditions.push(eq(projects.active_phase, activePhase))
if (args.includeArchived !== true) conditions.push(eq(projects.archived, false))
const rows = await context.server.db
.select()
.from(projects)
.where(and(...conditions))
.orderBy(desc(projects.createdAt))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "organisation.projects.get",
title: "Projekt laden",
description: "Lädt ein Projekt des aktiven Mandanten anhand seiner ID.",
requiredPermissions: ["organisation.projects.read"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const rows = await context.server.db
.select()
.from(projects)
.where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId)))
.limit(1)
if (!rows[0]) throw new Error("Projekt nicht gefunden")
return { project: rows[0] }
},
},
{
name: "organisation.projects.phase.update",
title: "Projektphase aktualisieren",
description: "Aktiviert eine Projektphase anhand ihres Schlüssels oder ihrer eindeutigen Bezeichnung. Pflichtphasen können nicht übersprungen werden; optionale Phasen und der direkte Abschluss entsprechen dem Verhalten der Oberfläche.",
requiredPermissions: ["organisation.projects.write"],
inputSchema: {
type: "object",
required: ["id"],
oneOf: [
{ required: ["phaseKey"] },
{ required: ["phaseLabel"] },
],
properties: {
id: { type: "number", description: "Projekt-ID." },
phaseKey: { type: "string", description: "Technischer Schlüssel der zu aktivierenden Phase; alternativ zu phaseLabel." },
phaseLabel: { type: "string", description: "Eindeutige Bezeichnung der zu aktivierenden Phase, falls kein phaseKey bekannt ist." },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const [existing] = await context.server.db
.select()
.from(projects)
.where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId)))
.limit(1)
if (!existing) throw new Error("Projekt nicht gefunden")
if (existing.archived) throw new Error("Die Phase eines archivierten Projekts kann nicht geändert werden")
const transition = activateProjectPhase(existing.phases, {
phaseKey: stringArg(args, "phaseKey"),
phaseLabel: stringArg(args, "phaseLabel"),
}, context.userId)
const [updated] = await context.server.db
.update(projects)
.set({
phases: transition.phases,
active_phase: transition.activePhase,
updatedAt: new Date(),
updatedBy: context.userId,
})
.where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId)))
.returning()
if (!updated) throw new Error("Projekt nicht gefunden")
await insertHistoryItem(context.server, {
tenant_id: context.tenantId,
created_by: context.userId,
entity: "projects",
entityId: id,
action: "updated",
oldVal: existing,
newVal: updated,
text: transition.previousPhase
? `Projektphase von „${transition.previousPhase}“ auf „${transition.activePhase}“ geändert`
: `Projektphase „${transition.activePhase}“ aktiviert`,
})
return {
project: updated,
phaseTransition: {
previousPhase: transition.previousPhase,
activePhase: transition.activePhase,
},
}
},
},
{
name: "organisation.plants.list",
title: "Anlagen auflisten",
description: "Listet Anlagen des aktiven Mandanten mit optionalem Kundenfilter.",
requiredPermissions: ["organisation.plants.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name." },
customer: { type: "number" },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(plants.tenant, context.tenantId)]
const query = stringArg(args, "query")
const customer = numberArg(args, "customer")
if (query) conditions.push(ilike(plants.name, `%${query}%`))
if (customer) conditions.push(eq(plants.customer, customer))
if (args.includeArchived !== true) conditions.push(eq(plants.archived, false))
const rows = await context.server.db
.select()
.from(plants)
.where(and(...conditions))
.orderBy(plants.name)
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "organisation.events.list",
title: "Termine auflisten",
description: "Listet Termine des aktiven Mandanten mit optionalen Projekt- oder Kundenfiltern.",
requiredPermissions: ["organisation.events.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Notizen oder Link." },
project: { type: "number" },
customer: { type: "number" },
eventtype: { type: "string" },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(events.tenant, context.tenantId)]
const query = stringArg(args, "query")
const project = numberArg(args, "project")
const customer = numberArg(args, "customer")
const eventtype = stringArg(args, "eventtype")
if (query) {
conditions.push(or(
ilike(events.name, `%${query}%`),
ilike(events.notes, `%${query}%`),
ilike(events.link, `%${query}%`)
))
}
if (project) conditions.push(eq(events.project, project))
if (customer) conditions.push(eq(events.customer, customer))
if (eventtype) conditions.push(eq(events.eventtype, eventtype))
if (args.includeArchived !== true) conditions.push(eq(events.archived, false))
const rows = await context.server.db
.select()
.from(events)
.where(and(...conditions))
.orderBy(desc(events.startDate))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "organisation.tasks.list",
title: "Aufgaben auflisten",
description: "Listet Aufgaben des aktiven Mandanten mit optionalen Filtern.",
requiredPermissions: ["organisation.tasks.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Name, Beschreibung oder Kategorie." },
project: { type: "number" },
customer: { type: "number" },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(tasks.tenant, context.tenantId)]
const query = stringArg(args, "query")
const project = numberArg(args, "project")
const customer = numberArg(args, "customer")
if (query) {
conditions.push(or(
ilike(tasks.name, `%${query}%`),
ilike(tasks.description, `%${query}%`),
ilike(tasks.categorie, `%${query}%`)
))
}
if (project) conditions.push(eq(tasks.project, project))
if (customer) conditions.push(eq(tasks.customer, customer))
if (args.includeArchived !== true) conditions.push(eq(tasks.archived, false))
const rows = await context.server.db
.select()
.from(tasks)
.where(and(...conditions))
.orderBy(desc(tasks.createdAt))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "organisation.tasks.get",
title: "Aufgabe laden",
description: "Lädt eine Aufgabe des aktiven Mandanten anhand ihrer ID.",
requiredPermissions: ["organisation.tasks.read"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const rows = await context.server.db
.select()
.from(tasks)
.where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId)))
.limit(1)
if (!rows[0]) throw new Error("Aufgabe nicht gefunden")
return { task: rows[0] }
},
},
{
name: "organisation.tasks.create",
title: "Aufgabe erstellen",
description: "Erstellt eine neue Aufgabe im aktiven Mandanten.",
requiredPermissions: ["organisation.tasks.write"],
inputSchema: {
type: "object",
required: ["name"],
properties: {
name: { type: "string" },
description: { type: "string" },
categorie: { type: "string" },
project: { type: "number" },
plant: { type: "number" },
customer: { type: "number" },
userId: { type: "string" },
profiles: { type: "array", items: { type: "string" } },
},
},
async handler(context, args) {
const name = stringArg(args, "name")
if (!name) throw new Error("name ist erforderlich")
const [created] = await context.server.db
.insert(tasks)
.values({
name,
description: stringArg(args, "description"),
categorie: stringArg(args, "categorie"),
tenant: context.tenantId,
userId: stringArg(args, "userId") || context.userId,
project: numberArg(args, "project"),
plant: numberArg(args, "plant"),
customer: numberArg(args, "customer"),
profiles: Array.isArray(args.profiles) ? args.profiles : [],
updatedAt: new Date(),
updatedBy: context.userId,
})
.returning()
return { task: created }
},
},
{
name: "organisation.tasks.update",
title: "Aufgabe aktualisieren",
description: "Aktualisiert Felder einer Aufgabe im aktiven Mandanten.",
requiredPermissions: ["organisation.tasks.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
name: { type: "string" },
description: { type: "string" },
categorie: { type: "string" },
project: { type: "number" },
plant: { type: "number" },
customer: { type: "number" },
userId: { type: "string" },
profiles: { type: "array", items: { type: "string" } },
archived: { type: "boolean" },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const update: Record<string, unknown> = {
updatedAt: new Date(),
updatedBy: context.userId,
}
for (const key of ["name", "description", "categorie", "userId"] as const) {
if (args[key] !== undefined) update[key] = stringArg(args, key)
}
for (const key of ["project", "plant", "customer"] as const) {
if (args[key] !== undefined) update[key] = numberArg(args, key)
}
if (Array.isArray(args.profiles)) update.profiles = args.profiles
if (typeof args.archived === "boolean") update.archived = args.archived
const [updated] = await context.server.db
.update(tasks)
.set(update)
.where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId)))
.returning()
if (!updated) throw new Error("Aufgabe nicht gefunden")
return { task: updated }
},
},
{
name: "organisation.tasks.archive",
title: "Aufgabe archivieren",
description: "Archiviert eine Aufgabe im aktiven Mandanten.",
requiredPermissions: ["organisation.tasks.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const [updated] = await context.server.db
.update(tasks)
.set({
archived: true,
updatedAt: new Date(),
updatedBy: context.userId,
})
.where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId)))
.returning()
if (!updated) throw new Error("Aufgabe nicht gefunden")
return { task: updated }
},
},
]