diff --git a/backend/src/routes/wiki.ts b/backend/src/routes/wiki.ts index 4a466c3..6525343 100644 --- a/backend/src/routes/wiki.ts +++ b/backend/src/routes/wiki.ts @@ -1,17 +1,37 @@ import { FastifyInstance } from "fastify" -import { and, eq, isNull, asc, desc } from "drizzle-orm" -import { wikiPages, authUsers } from "../../db/schema/" +import { and, eq, isNull, asc, inArray } from "drizzle-orm" +// WICHTIG: Hier müssen die Schemas der Entitäten importiert werden! +import { + wikiPages, + authUsers, + customers, + projects, + plants, + products, + inventoryitems, + // ... weitere Schemas hier importieren +} from "../../db/schema/" -// Types für Request Body & Query (statt Zod) +// Konfiguration: Welche Entitäten sollen im Wiki auftauchen? +const ENTITY_CONFIG: Record = { + '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' }, + // Hier weitere hinzufügen (z.B. vehicles, products...) +} + +// Types interface WikiTreeQuery { entityType?: string - entityId?: number // Kommt als string oder number an, je nach parser + entityId?: number entityUuid?: string } interface WikiCreateBody { title: string - parentId?: string // UUID + parentId?: string isFolder?: boolean entityType?: string entityId?: number @@ -20,42 +40,53 @@ interface WikiCreateBody { interface WikiUpdateBody { title?: string - content?: any // Das Tiptap JSON Object + content?: any parentId?: string | null sortOrder?: number isFolder?: boolean } -export default async function wikiRoutes (server: FastifyInstance) { +export default async function wikiRoutes(server: FastifyInstance) { // --------------------------------------------------------- // 1. GET /wiki/tree - // Lädt die Struktur für die Sidebar (OHNE Content -> Schnell) + // 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 - // Basis-Filter: Tenant Sicherheit - const filters = [eq(wikiPages.tenantId, user.tenant_id)] + // 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) + ] - // Logik: Laden wir "Allgemein" oder "Entitäts-Spezifisch"? - if (entityType) { - // Spezifisch (z.B. Kunde) - filters.push(eq(wikiPages.entityType, entityType)) + if (entityId) filters.push(eq(wikiPages.entityId, Number(entityId))) + else if (entityUuid) filters.push(eq(wikiPages.entityUuid, entityUuid)) - if (entityId) { - filters.push(eq(wikiPages.entityId, Number(entityId))) - } else if (entityUuid) { - filters.push(eq(wikiPages.entityUuid, entityUuid)) - } - } else { - // Allgemein (alles wo entityType NULL ist) - filters.push(isNull(wikiPages.entityType)) + 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)) } - // Performance: Nur Metadaten laden, kein riesiges JSON-Content - const pages = await server.db + // 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, @@ -63,24 +94,115 @@ export default async function wikiRoutes (server: FastifyInstance) { 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(and(...filters)) + .where(eq(wikiPages.tenantId, user.tenant_id)) .orderBy(asc(wikiPages.sortOrder), asc(wikiPages.title)) - return pages + // 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 (für den Editor) + // 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 - // Drizzle Query API nutzen für Relation (optional Author laden) const page = await server.db.query.wikiPages.findFirst({ where: and( eq(wikiPages.id, id), @@ -88,18 +210,12 @@ export default async function wikiRoutes (server: FastifyInstance) { ), with: { author: { - columns: { - id: true, - // name: true, // Falls im authUsers schema vorhanden - } + columns: { id: true } // Name falls vorhanden } } }) - if (!page) { - return reply.code(404).send({ error: "Page not found" }) - } - + if (!page) return reply.code(404).send({ error: "Page not found" }) return page }) @@ -111,10 +227,8 @@ export default async function wikiRoutes (server: FastifyInstance) { const user = req.user const body = req.body - // Simple Validierung if (!body.title) return reply.code(400).send({ error: "Title required" }) - // Split Logik für Polymorphie const hasEntity = !!body.entityType const [newPage] = await server.db @@ -122,10 +236,8 @@ export default async function wikiRoutes (server: FastifyInstance) { .values({ tenantId: user.tenant_id, title: body.title, - parentId: body.parentId || null, // undefined abfangen + parentId: body.parentId || null, isFolder: body.isFolder ?? false, - - // Polymorphe Zuweisung entityType: hasEntity ? body.entityType : null, entityId: hasEntity && body.entityId ? body.entityId : null, entityUuid: hasEntity && body.entityUuid ? body.entityUuid : null, @@ -141,7 +253,7 @@ export default async function wikiRoutes (server: FastifyInstance) { // --------------------------------------------------------- // 4. PATCH /wiki/:id - // Universal-Update (Inhalt, Titel, Verschieben, Sortieren) + // Universal-Update // --------------------------------------------------------- server.patch<{ Params: { id: string }; Body: WikiUpdateBody }>( "/wiki/:id", @@ -150,21 +262,19 @@ export default async function wikiRoutes (server: FastifyInstance) { const { id } = req.params const body = req.body - // 1. Prüfen ob Eintrag existiert & Tenant gehört 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" }) + if (!existing) return reply.code(404).send({ error: "Not found" }) - // 2. Update durchführen const [updatedPage] = await server.db .update(wikiPages) .set({ - title: body.title, // update title if defined - content: body.content, // update content if defined - parentId: body.parentId, // update parent (move) if defined + title: body.title, + content: body.content, + parentId: body.parentId, sortOrder: body.sortOrder, isFolder: body.isFolder, updatedAt: new Date(), @@ -180,7 +290,7 @@ export default async function wikiRoutes (server: FastifyInstance) { // --------------------------------------------------------- // 5. DELETE /wiki/:id - // Löscht Eintrag (DB Cascade erledigt die Kinder) + // Löscht Eintrag // --------------------------------------------------------- server.delete<{ Params: { id: string } }>("/wiki/:id", async (req, reply) => { const user = req.user @@ -190,13 +300,11 @@ export default async function wikiRoutes (server: FastifyInstance) { .delete(wikiPages) .where(and( eq(wikiPages.id, id), - eq(wikiPages.tenantId, user.tenant_id) // WICHTIG: Security + eq(wikiPages.tenantId, user.tenant_id) )) .returning({ id: wikiPages.id }) - if (result.length === 0) { - return reply.code(404).send({ error: "Not found or not authorized" }) - } + if (result.length === 0) return reply.code(404).send({ error: "Not found" }) return { success: true, deletedId: result[0].id } }) diff --git a/frontend/components/EntityShow.vue b/frontend/components/EntityShow.vue index 815dc7f..c9133d2 100644 --- a/frontend/components/EntityShow.vue +++ b/frontend/components/EntityShow.vue @@ -1,6 +1,7 @@ + + \ No newline at end of file diff --git a/frontend/stores/data.js b/frontend/stores/data.js index 6a3e686..fd737c0 100644 --- a/frontend/stores/data.js +++ b/frontend/stores/data.js @@ -857,6 +857,8 @@ export const useDataStore = defineStore('data', () => { label: "Aufgaben" },{ label: "Dateien" + },{ + label: "Wiki" }] }, products: { @@ -975,7 +977,9 @@ export const useDataStore = defineStore('data', () => { ], showTabs: [ { - label: "Informationen" + label: "Informationen", + },{ + label: "Wiki", } ] },