340 lines
14 KiB
TypeScript
340 lines
14 KiB
TypeScript
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<string, { table: any, labelField: any, rootLabel: string, idField: 'id' | 'uuid' }> = {
|
|
// --- 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 }
|
|
})
|
|
} |