import { FastifyInstance } from "fastify" import { and, eq, isNull, asc, inArray } from "drizzle-orm" // WICHTIG: Hier müssen die Schemas der Entitäten importiert werden! import { wikiPages, authUsers, // Bereits vorhanden customers, projects, plants, products, inventoryitems, // NEU HINZUGEFÜGT (Basierend auf deinem DataStore) tasks, contacts, contracts, vehicles, vendors, spaces, inventoryitemgroups, services, hourrates, events, productcategories, servicecategories, ownaccounts } from "../../db/schema/" // Konfiguration: Welche Entitäten sollen im Wiki auftauchen? const ENTITY_CONFIG: Record = { // --- BEREITS VORHANDEN --- 'customers': { table: customers, labelField: customers.name, rootLabel: 'Kunden', idField: 'id' }, 'projects': { table: projects, labelField: projects.name, rootLabel: 'Projekte', idField: 'id' }, 'plants': { table: plants, labelField: plants.name, rootLabel: 'Objekte', idField: 'id' }, 'products': { table: products, labelField: products.name, rootLabel: 'Artikel', idField: 'id' }, 'inventoryitems': { table: inventoryitems, labelField: inventoryitems.name, rootLabel: 'Inventarartikel', idField: 'id' }, // --- NEU BASIEREND AUF DATASTORE --- 'tasks': { table: tasks, labelField: tasks.name, rootLabel: 'Aufgaben', idField: 'id' }, 'contacts': { table: contacts, labelField: contacts.fullName, rootLabel: 'Kontakte', idField: 'id' }, 'contracts': { table: contracts, labelField: contracts.name, rootLabel: 'Verträge', idField: 'id' }, 'vehicles': { table: vehicles, labelField: vehicles.license_plate, rootLabel: 'Fahrzeuge', idField: 'id' }, 'vendors': { table: vendors, labelField: vendors.name, rootLabel: 'Lieferanten', idField: 'id' }, 'spaces': { table: spaces, labelField: spaces.name, rootLabel: 'Lagerplätze', idField: 'id' }, 'inventoryitemgroups': { table: inventoryitemgroups, labelField: inventoryitemgroups.name, rootLabel: 'Inventarartikelgruppen', idField: 'id' }, 'services': { table: services, labelField: services.name, rootLabel: 'Leistungen', idField: 'id' }, 'hourrates': { table: hourrates, labelField: hourrates.name, rootLabel: 'Stundensätze', idField: 'id' }, 'events': { table: events, labelField: events.name, rootLabel: 'Termine', idField: 'id' }, 'productcategories': { table: productcategories, labelField: productcategories.name, rootLabel: 'Artikelkategorien', idField: 'id' }, 'servicecategories': { table: servicecategories, labelField: servicecategories.name, rootLabel: 'Leistungskategorien', idField: 'id' }, 'ownaccounts': { table: ownaccounts, labelField: ownaccounts.name, rootLabel: 'Zusätzliche Buchungskonten', idField: 'id' }, } // Types interface WikiTreeQuery { entityType?: string entityId?: number entityUuid?: string } interface WikiCreateBody { title: string parentId?: string isFolder?: boolean entityType?: string entityId?: number entityUuid?: string } interface WikiUpdateBody { title?: string content?: any parentId?: string | null sortOrder?: number isFolder?: boolean } export default async function wikiRoutes(server: FastifyInstance) { // --------------------------------------------------------- // 1. GET /wiki/tree // Lädt Struktur: Entweder gefiltert (Widget) oder Global (mit virtuellen Ordnern) // --------------------------------------------------------- server.get<{ Querystring: WikiTreeQuery }>("/wiki/tree", async (req, reply) => { const user = req.user const { entityType, entityId, entityUuid } = req.query // FALL A: WIDGET-ANSICHT (Spezifische Entität) // Wenn wir spezifisch filtern, wollen wir nur die echten Seiten ohne virtuelle Ordner if (entityType && (entityId || entityUuid)) { const filters = [ eq(wikiPages.tenantId, user.tenant_id), eq(wikiPages.entityType, entityType) ] if (entityId) filters.push(eq(wikiPages.entityId, Number(entityId))) else if (entityUuid) filters.push(eq(wikiPages.entityUuid, entityUuid)) return server.db .select({ id: wikiPages.id, parentId: wikiPages.parentId, title: wikiPages.title, isFolder: wikiPages.isFolder, sortOrder: wikiPages.sortOrder, entityType: wikiPages.entityType, updatedAt: wikiPages.updatedAt, }) .from(wikiPages) .where(and(...filters)) .orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title)) } // FALL B: GLOBALE ANSICHT (Haupt-Wiki) // Wir laden ALLES und bauen virtuelle Ordner für die Entitäten // 1. Alle Wiki-Seiten des Tenants laden const allPages = await server.db .select({ id: wikiPages.id, parentId: wikiPages.parentId, title: wikiPages.title, isFolder: wikiPages.isFolder, sortOrder: wikiPages.sortOrder, entityType: wikiPages.entityType, entityId: wikiPages.entityId, // Wichtig für Zuordnung entityUuid: wikiPages.entityUuid, // Wichtig für Zuordnung updatedAt: wikiPages.updatedAt, }) .from(wikiPages) .where(eq(wikiPages.tenantId, user.tenant_id)) .orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title)) // Trennen in Standard-Seiten und Entity-Seiten const standardPages = allPages.filter(p => !p.entityType) const entityPages = allPages.filter(p => p.entityType) const virtualNodes: any[] = [] // 2. Virtuelle Ordner generieren // Wir iterieren durch unsere Config (Kunden, Projekte...) await Promise.all(Object.entries(ENTITY_CONFIG).map(async ([typeKey, config]) => { // Haben wir überhaupt Notizen für diesen Typ? const pagesForType = entityPages.filter(p => p.entityType === typeKey) if (pagesForType.length === 0) return // IDs sammeln, um Namen aus der DB zu holen // Wir unterscheiden zwischen ID (int) und UUID let entities: any[] = [] if (config.idField === 'id') { const ids = [...new Set(pagesForType.map(p => p.entityId).filter((id): id is number => id !== null))] if (ids.length > 0) { //@ts-ignore - Drizzle Typisierung bei dynamischen Tables ist tricky entities = await server.db.select({ id: config.table.id, label: config.labelField }) .from(config.table) //@ts-ignore .where(inArray(config.table.id, ids)) } } else { // Falls UUID genutzt wird (z.B. IoT Devices) const uuids = [...new Set(pagesForType.map(p => p.entityUuid).filter((uuid): uuid is string => uuid !== null))] if (uuids.length > 0) { //@ts-ignore entities = await server.db.select({ id: config.table.id, label: config.labelField }) .from(config.table) //@ts-ignore .where(inArray(config.table.id, uuids)) } } if (entities.length === 0) return // 3. Virtuellen Root Ordner erstellen (z.B. "Kunden") const rootId = `virtual-root-${typeKey}` virtualNodes.push({ id: rootId, parentId: null, // Ganz oben im Baum title: config.rootLabel, isFolder: true, isVirtual: true, // Flag fürs Frontend (read-only Folder) sortOrder: 1000 // Ganz unten anzeigen }) // 4. Virtuelle Entity Ordner erstellen (z.B. "Müller GmbH") entities.forEach(entity => { const entityNodeId = `virtual-entity-${typeKey}-${entity.id}` virtualNodes.push({ id: entityNodeId, parentId: rootId, title: entity.label || 'Unbekannt', isFolder: true, isVirtual: true, sortOrder: 0 }) // 5. Die echten Notizen verschieben // Wir suchen alle Notizen, die zu dieser Entity gehören const myPages = pagesForType.filter(p => (config.idField === 'id' && p.entityId === entity.id) || (config.idField === 'uuid' && p.entityUuid === entity.id) ) myPages.forEach(page => { // Nur Root-Notizen der Entity verschieben. // Sub-Pages bleiben wo sie sind (parentId zeigt ja schon auf die richtige Seite) if (!page.parentId) { // Wir modifizieren das Objekt für die Response (nicht in der DB!) // Wir müssen es clonen, sonst ändern wir es für alle Referenzen const pageClone = { ...page } pageClone.parentId = entityNodeId virtualNodes.push(pageClone) } else { // Sub-Pages einfach so hinzufügen virtualNodes.push(page) } }) }) })) // Ergebnis: Normale Seiten + Virtuelle Struktur return [...standardPages, ...virtualNodes] }) // --------------------------------------------------------- // 2. GET /wiki/:id // Lädt EINEN Eintrag komplett MIT Content // --------------------------------------------------------- server.get<{ Params: { id: string } }>("/wiki/:id", async (req, reply) => { const user = req.user const { id } = req.params const page = await server.db.query.wikiPages.findFirst({ where: and( eq(wikiPages.id, id), eq(wikiPages.tenantId, user.tenant_id) ), with: { author: { columns: { id: true } // Name falls vorhanden } } }) if (!page) return reply.code(404).send({ error: "Page not found" }) return page }) // --------------------------------------------------------- // 3. POST /wiki // Erstellt neuen Eintrag // --------------------------------------------------------- server.post<{ Body: WikiCreateBody }>("/wiki", async (req, reply) => { const user = req.user const body = req.body if (!body.title) return reply.code(400).send({ error: "Title required" }) const hasEntity = !!body.entityType const [newPage] = await server.db .insert(wikiPages) .values({ tenantId: user.tenant_id, title: body.title, parentId: body.parentId || null, isFolder: body.isFolder ?? false, entityType: hasEntity ? body.entityType : null, entityId: hasEntity && body.entityId ? body.entityId : null, entityUuid: hasEntity && body.entityUuid ? body.entityUuid : null, //@ts-ignore createdBy: user.id, //@ts-ignore updatedBy: user.id }) .returning() return newPage }) // --------------------------------------------------------- // 4. PATCH /wiki/:id // Universal-Update // --------------------------------------------------------- server.patch<{ Params: { id: string }; Body: WikiUpdateBody }>( "/wiki/:id", async (req, reply) => { const user = req.user const { id } = req.params const body = req.body const existing = await server.db.query.wikiPages.findFirst({ where: and(eq(wikiPages.id, id), eq(wikiPages.tenantId, user.tenant_id)), columns: { id: true } }) if (!existing) return reply.code(404).send({ error: "Not found" }) const [updatedPage] = await server.db .update(wikiPages) .set({ title: body.title, content: body.content, parentId: body.parentId, sortOrder: body.sortOrder, isFolder: body.isFolder, updatedAt: new Date(), //@ts-ignore updatedBy: user.id }) .where(eq(wikiPages.id, id)) .returning() return updatedPage } ) // --------------------------------------------------------- // 5. DELETE /wiki/:id // Löscht Eintrag // --------------------------------------------------------- server.delete<{ Params: { id: string } }>("/wiki/:id", async (req, reply) => { const user = req.user const { id } = req.params const result = await server.db .delete(wikiPages) .where(and( eq(wikiPages.id, id), eq(wikiPages.tenantId, user.tenant_id) )) .returning({ id: wikiPages.id }) if (result.length === 0) return reply.code(404).send({ error: "Not found" }) return { success: true, deletedId: result[0].id } }) }