/** * TASKMATE - Knowledge Management Routes * ====================================== * CRUD für Wissensmanagement: Kategorien, Einträge, Anhänge */ const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs'); const multer = require('multer'); const { getDb } = require('../database'); const logger = require('../utils/logger'); const { validators, stripHtml } = require('../middleware/validation'); const notificationService = require('../services/notificationService'); // Upload-Konfiguration für Knowledge-Anhänge const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'knowledge'); // Sicherstellen, dass das Upload-Verzeichnis existiert if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); } const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, UPLOAD_DIR); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const ext = path.extname(file.originalname); cb(null, `knowledge-${uniqueSuffix}${ext}`); } }); const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 // 50MB max } }); // ===================== // KATEGORIEN // ===================== /** * GET /api/knowledge/categories * Alle Kategorien abrufen */ router.get('/categories', (req, res) => { try { const db = getDb(); const categories = db.prepare(` SELECT kc.*, (SELECT COUNT(*) FROM knowledge_entries WHERE category_id = kc.id) as entry_count, u.display_name as creator_name FROM knowledge_categories kc LEFT JOIN users u ON kc.created_by = u.id ORDER BY kc.position, kc.created_at `).all(); res.json(categories.map(c => ({ id: c.id, name: c.name, description: c.description, color: c.color, icon: c.icon, position: c.position, entryCount: c.entry_count, createdBy: c.created_by, creatorName: c.creator_name, createdAt: c.created_at }))); } catch (error) { logger.error('Fehler beim Abrufen der Knowledge-Kategorien:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/knowledge/categories * Neue Kategorie erstellen */ router.post('/categories', (req, res) => { try { const { name, description, color, icon } = req.body; // Validierung const nameError = validators.required(name, 'Name') || validators.maxLength(name, 50, 'Name'); if (nameError) { return res.status(400).json({ error: nameError }); } if (color) { const colorError = validators.hexColor(color, 'Farbe'); if (colorError) { return res.status(400).json({ error: colorError }); } } const db = getDb(); // Duplikat-Prüfung const existing = db.prepare( 'SELECT id FROM knowledge_categories WHERE LOWER(name) = LOWER(?)' ).get(name); if (existing) { return res.status(400).json({ error: 'Eine Kategorie mit diesem Namen existiert bereits' }); } // Alle bestehenden Kategorien um 1 nach unten verschieben db.prepare(` UPDATE knowledge_categories SET position = position + 1 `).run(); // Neue Kategorie an Position 0 (ganz oben) einfügen const result = db.prepare(` INSERT INTO knowledge_categories (name, description, color, icon, position, created_by) VALUES (?, ?, ?, ?, ?, ?) `).run( stripHtml(name), description ? stripHtml(description) : null, color || '#3B82F6', icon || null, 0, // Neue Kategorien immer an Position 0 (oben) req.user.id ); const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?') .get(result.lastInsertRowid); logger.info(`Knowledge-Kategorie erstellt: ${name}`); res.status(201).json({ id: category.id, name: category.name, description: category.description, color: category.color, icon: category.icon, position: category.position, entryCount: 0, createdBy: category.created_by, createdAt: category.created_at }); } catch (error) { logger.error('Fehler beim Erstellen der Knowledge-Kategorie:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/knowledge/categories/:id * Kategorie aktualisieren */ router.put('/categories/:id', (req, res) => { try { const categoryId = req.params.id; const { name, description, color, icon } = req.body; const db = getDb(); const existing = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); if (!existing) { return res.status(404).json({ error: 'Kategorie nicht gefunden' }); } // Validierung if (name) { const nameError = validators.maxLength(name, 50, 'Name'); if (nameError) { return res.status(400).json({ error: nameError }); } // Duplikat-Prüfung (ausser eigene) const duplicate = db.prepare( 'SELECT id FROM knowledge_categories WHERE LOWER(name) = LOWER(?) AND id != ?' ).get(name, categoryId); if (duplicate) { return res.status(400).json({ error: 'Eine Kategorie mit diesem Namen existiert bereits' }); } } if (color) { const colorError = validators.hexColor(color, 'Farbe'); if (colorError) { return res.status(400).json({ error: colorError }); } } db.prepare(` UPDATE knowledge_categories SET name = COALESCE(?, name), description = COALESCE(?, description), color = COALESCE(?, color), icon = COALESCE(?, icon) WHERE id = ? `).run( name ? stripHtml(name) : null, description !== undefined ? (description ? stripHtml(description) : '') : null, color || null, icon !== undefined ? icon : null, categoryId ); const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); logger.info(`Knowledge-Kategorie aktualisiert: ${category.name}`); res.json({ id: category.id, name: category.name, description: category.description, color: category.color, icon: category.icon, position: category.position, createdBy: category.created_by, createdAt: category.created_at }); } catch (error) { logger.error('Fehler beim Aktualisieren der Knowledge-Kategorie:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/knowledge/categories/:id/position * Kategorie-Position ändern */ router.put('/categories/:id/position', (req, res) => { try { const categoryId = req.params.id; const { newPosition } = req.body; const db = getDb(); const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); if (!category) { return res.status(404).json({ error: 'Kategorie nicht gefunden' }); } const oldPosition = category.position; if (newPosition > oldPosition) { db.prepare(` UPDATE knowledge_categories SET position = position - 1 WHERE position > ? AND position <= ? `).run(oldPosition, newPosition); } else if (newPosition < oldPosition) { db.prepare(` UPDATE knowledge_categories SET position = position + 1 WHERE position >= ? AND position < ? `).run(newPosition, oldPosition); } db.prepare('UPDATE knowledge_categories SET position = ? WHERE id = ?').run(newPosition, categoryId); const categories = db.prepare( 'SELECT * FROM knowledge_categories ORDER BY position' ).all(); res.json({ categories: categories.map(c => ({ id: c.id, name: c.name, position: c.position })) }); } catch (error) { logger.error('Fehler beim Verschieben der Knowledge-Kategorie:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/knowledge/categories/:id * Kategorie löschen (inkl. aller Einträge und Anhänge) */ router.delete('/categories/:id', (req, res) => { try { const categoryId = req.params.id; const db = getDb(); const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); if (!category) { return res.status(404).json({ error: 'Kategorie nicht gefunden' }); } // Anhänge von allen Einträgen dieser Kategorie löschen const entries = db.prepare('SELECT id FROM knowledge_entries WHERE category_id = ?').all(categoryId); for (const entry of entries) { const attachments = db.prepare('SELECT * FROM knowledge_attachments WHERE entry_id = ?').all(entry.id); for (const attachment of attachments) { const filePath = path.join(UPLOAD_DIR, attachment.filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } } // Kategorie löschen (CASCADE löscht Einträge und Anhänge aus DB) db.prepare('DELETE FROM knowledge_categories WHERE id = ?').run(categoryId); // Positionen neu nummerieren const remaining = db.prepare( 'SELECT id FROM knowledge_categories ORDER BY position' ).all(); remaining.forEach((c, idx) => { db.prepare('UPDATE knowledge_categories SET position = ? WHERE id = ?').run(idx, c.id); }); logger.info(`Knowledge-Kategorie gelöscht: ${category.name}`); res.json({ message: 'Kategorie gelöscht' }); } catch (error) { logger.error('Fehler beim Löschen der Knowledge-Kategorie:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); // ===================== // EINTRÄGE // ===================== /** * GET /api/knowledge/entries * Einträge abrufen (optional gefiltert nach Kategorie) */ router.get('/entries', (req, res) => { try { const { categoryId } = req.query; const db = getDb(); let query = ` SELECT ke.*, kc.name as category_name, kc.color as category_color, u.display_name as creator_name, (SELECT COUNT(*) FROM knowledge_attachments WHERE entry_id = ke.id) as attachment_count FROM knowledge_entries ke LEFT JOIN knowledge_categories kc ON ke.category_id = kc.id LEFT JOIN users u ON ke.created_by = u.id `; const params = []; if (categoryId) { query += ' WHERE ke.category_id = ?'; params.push(categoryId); } query += ' ORDER BY ke.position, ke.created_at DESC'; const entries = db.prepare(query).all(...params); res.json(entries.map(e => ({ id: e.id, categoryId: e.category_id, categoryName: e.category_name, categoryColor: e.category_color, title: e.title, url: e.url, notes: e.notes, position: e.position, attachmentCount: e.attachment_count, createdBy: e.created_by, creatorName: e.creator_name, createdAt: e.created_at, updatedAt: e.updated_at }))); } catch (error) { logger.error('Fehler beim Abrufen der Knowledge-Einträge:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/knowledge/entries/:id * Einzelnen Eintrag abrufen (mit Anhängen) */ router.get('/entries/:id', (req, res) => { try { const entryId = req.params.id; const db = getDb(); const entry = db.prepare(` SELECT ke.*, kc.name as category_name, kc.color as category_color, u.display_name as creator_name FROM knowledge_entries ke LEFT JOIN knowledge_categories kc ON ke.category_id = kc.id LEFT JOIN users u ON ke.created_by = u.id WHERE ke.id = ? `).get(entryId); if (!entry) { return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } const attachments = db.prepare(` SELECT ka.*, u.display_name as uploader_name FROM knowledge_attachments ka LEFT JOIN users u ON ka.uploaded_by = u.id WHERE ka.entry_id = ? ORDER BY ka.uploaded_at DESC `).all(entryId); res.json({ id: entry.id, categoryId: entry.category_id, categoryName: entry.category_name, categoryColor: entry.category_color, title: entry.title, url: entry.url, notes: entry.notes, position: entry.position, createdBy: entry.created_by, creatorName: entry.creator_name, createdAt: entry.created_at, updatedAt: entry.updated_at, attachments: attachments.map(a => ({ id: a.id, filename: a.filename, originalName: a.original_name, mimeType: a.mime_type, sizeBytes: a.size_bytes, uploadedBy: a.uploaded_by, uploaderName: a.uploader_name, uploadedAt: a.uploaded_at })) }); } catch (error) { logger.error('Fehler beim Abrufen des Knowledge-Eintrags:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/knowledge/entries * Neuen Eintrag erstellen */ router.post('/entries', (req, res) => { try { const { categoryId, title, url, notes } = req.body; // Validierung const categoryError = validators.required(categoryId, 'Kategorie'); if (categoryError) { return res.status(400).json({ error: categoryError }); } const titleError = validators.required(title, 'Titel') || validators.maxLength(title, 200, 'Titel'); if (titleError) { return res.status(400).json({ error: titleError }); } if (url) { const urlError = validators.url(url, 'URL'); if (urlError) { return res.status(400).json({ error: urlError }); } } const db = getDb(); // Kategorie prüfen const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); if (!category) { return res.status(404).json({ error: 'Kategorie nicht gefunden' }); } // Alle bestehenden Einträge um 1 nach unten verschieben db.prepare(` UPDATE knowledge_entries SET position = position + 1 WHERE category_id = ? `).run(categoryId); // Neuen Eintrag an Position 0 (ganz oben) einfügen const result = db.prepare(` INSERT INTO knowledge_entries (category_id, title, url, notes, position, created_by) VALUES (?, ?, ?, ?, ?, ?) `).run( categoryId, stripHtml(title), url || null, notes || null, 0, // Neue Einträge immer an Position 0 (oben) req.user.id ); const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?') .get(result.lastInsertRowid); logger.info(`Knowledge-Eintrag erstellt: ${title}`); // Benachrichtigung an alle Nutzer senden const io = req.app.get('io'); if (io) { // Alle Nutzer abrufen (außer dem Ersteller) const users = db.prepare(` SELECT id FROM users WHERE id != ? `).all(req.user.id); // Benachrichtigung für jeden Nutzer erstellen users.forEach(user => { notificationService.create( user.id, 'knowledge:new_entry', { entryId: entry.id, entryTitle: entry.title, categoryName: category.name, categoryId: category.id, projectId: null, actorId: req.user.id }, io, false // nicht persistent ); }); // Socket.io Event für Echtzeit-Update senden io.emit('knowledge:created', { entry: { id: entry.id, categoryId: entry.category_id, categoryName: category.name, categoryColor: category.color, title: entry.title, url: entry.url, notes: entry.notes, position: entry.position, attachmentCount: 0, createdBy: entry.created_by, creatorName: req.user.display_name, createdAt: entry.created_at, updatedAt: entry.updated_at } }); } res.status(201).json({ id: entry.id, categoryId: entry.category_id, title: entry.title, url: entry.url, notes: entry.notes, position: entry.position, attachmentCount: 0, createdBy: entry.created_by, createdAt: entry.created_at, updatedAt: entry.updated_at }); } catch (error) { logger.error('Fehler beim Erstellen des Knowledge-Eintrags:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/knowledge/entries/:id * Eintrag aktualisieren */ router.put('/entries/:id', (req, res) => { try { const entryId = req.params.id; const { categoryId, title, url, notes } = req.body; const db = getDb(); const existing = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(entryId); if (!existing) { return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } // Validierung if (title) { const titleError = validators.maxLength(title, 200, 'Titel'); if (titleError) { return res.status(400).json({ error: titleError }); } } if (url) { const urlError = validators.url(url, 'URL'); if (urlError) { return res.status(400).json({ error: urlError }); } } if (categoryId) { const category = db.prepare('SELECT * FROM knowledge_categories WHERE id = ?').get(categoryId); if (!category) { return res.status(404).json({ error: 'Kategorie nicht gefunden' }); } } db.prepare(` UPDATE knowledge_entries SET category_id = COALESCE(?, category_id), title = COALESCE(?, title), url = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run( categoryId || null, title ? stripHtml(title) : null, url !== undefined ? url : existing.url, notes !== undefined ? notes : existing.notes, entryId ); const entry = db.prepare(` SELECT ke.*, kc.name as category_name, kc.color as category_color FROM knowledge_entries ke LEFT JOIN knowledge_categories kc ON ke.category_id = kc.id WHERE ke.id = ? `).get(entryId); logger.info(`Knowledge-Eintrag aktualisiert: ${entry.title}`); res.json({ id: entry.id, categoryId: entry.category_id, categoryName: entry.category_name, categoryColor: entry.category_color, title: entry.title, url: entry.url, notes: entry.notes, position: entry.position, createdBy: entry.created_by, createdAt: entry.created_at, updatedAt: entry.updated_at }); } catch (error) { logger.error('Fehler beim Aktualisieren des Knowledge-Eintrags:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/knowledge/entries/:id/position * Eintrag-Position ändern (oder in andere Kategorie verschieben) */ router.put('/entries/:id/position', (req, res) => { try { const entryId = req.params.id; const { newPosition, newCategoryId } = req.body; const db = getDb(); const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(entryId); if (!entry) { return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } const oldPosition = entry.position; const oldCategoryId = entry.category_id; const targetCategoryId = newCategoryId || oldCategoryId; // Wenn Kategorie wechselt if (targetCategoryId !== oldCategoryId) { // Alte Kategorie: Positionen nach dem entfernten Eintrag reduzieren db.prepare(` UPDATE knowledge_entries SET position = position - 1 WHERE category_id = ? AND position > ? `).run(oldCategoryId, oldPosition); // Neue Kategorie: Platz für neuen Eintrag schaffen db.prepare(` UPDATE knowledge_entries SET position = position + 1 WHERE category_id = ? AND position >= ? `).run(targetCategoryId, newPosition); // Eintrag verschieben db.prepare(` UPDATE knowledge_entries SET category_id = ?, position = ? WHERE id = ? `).run(targetCategoryId, newPosition, entryId); } else { // Innerhalb der gleichen Kategorie if (newPosition > oldPosition) { db.prepare(` UPDATE knowledge_entries SET position = position - 1 WHERE category_id = ? AND position > ? AND position <= ? `).run(oldCategoryId, oldPosition, newPosition); } else if (newPosition < oldPosition) { db.prepare(` UPDATE knowledge_entries SET position = position + 1 WHERE category_id = ? AND position >= ? AND position < ? `).run(oldCategoryId, newPosition, oldPosition); } db.prepare('UPDATE knowledge_entries SET position = ? WHERE id = ?').run(newPosition, entryId); } res.json({ message: 'Position aktualisiert' }); } catch (error) { logger.error('Fehler beim Verschieben des Knowledge-Eintrags:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/knowledge/entries/:id * Eintrag löschen */ router.delete('/entries/:id', (req, res) => { try { const entryId = req.params.id; const db = getDb(); const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(entryId); if (!entry) { return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } // Anhänge vom Dateisystem löschen const attachments = db.prepare('SELECT * FROM knowledge_attachments WHERE entry_id = ?').all(entryId); for (const attachment of attachments) { const filePath = path.join(UPLOAD_DIR, attachment.filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } const categoryId = entry.category_id; // Eintrag löschen db.prepare('DELETE FROM knowledge_entries WHERE id = ?').run(entryId); // Positionen neu nummerieren const remaining = db.prepare( 'SELECT id FROM knowledge_entries WHERE category_id = ? ORDER BY position' ).all(categoryId); remaining.forEach((e, idx) => { db.prepare('UPDATE knowledge_entries SET position = ? WHERE id = ?').run(idx, e.id); }); logger.info(`Knowledge-Eintrag gelöscht: ${entry.title}`); res.json({ message: 'Eintrag gelöscht' }); } catch (error) { logger.error('Fehler beim Löschen des Knowledge-Eintrags:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); // ===================== // ANHÄNGE // ===================== /** * GET /api/knowledge/attachments/:entryId * Anhänge eines Eintrags abrufen */ router.get('/attachments/:entryId', (req, res) => { try { const entryId = req.params.entryId; const db = getDb(); const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(entryId); if (!entry) { return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } const attachments = db.prepare(` SELECT ka.*, u.display_name as uploader_name FROM knowledge_attachments ka LEFT JOIN users u ON ka.uploaded_by = u.id WHERE ka.entry_id = ? ORDER BY ka.uploaded_at DESC `).all(entryId); res.json(attachments.map(a => ({ id: a.id, entryId: a.entry_id, filename: a.filename, originalName: a.original_name, mimeType: a.mime_type, sizeBytes: a.size_bytes, uploadedBy: a.uploaded_by, uploaderName: a.uploader_name, uploadedAt: a.uploaded_at }))); } catch (error) { logger.error('Fehler beim Abrufen der Knowledge-Anhänge:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/knowledge/attachments/:entryId * Anhang hochladen */ router.post('/attachments/:entryId', upload.single('files'), (req, res) => { try { const entryId = req.params.entryId; const db = getDb(); const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?').get(entryId); if (!entry) { // Hochgeladene Datei löschen if (req.file) { fs.unlinkSync(req.file.path); } return res.status(404).json({ error: 'Eintrag nicht gefunden' }); } if (!req.file) { return res.status(400).json({ error: 'Keine Datei hochgeladen' }); } const result = db.prepare(` INSERT INTO knowledge_attachments (entry_id, filename, original_name, mime_type, size_bytes, uploaded_by) VALUES (?, ?, ?, ?, ?, ?) `).run( entryId, req.file.filename, req.file.originalname, req.file.mimetype, req.file.size, req.user.id ); // Eintrag updated_at aktualisieren db.prepare('UPDATE knowledge_entries SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(entryId); const attachment = db.prepare('SELECT * FROM knowledge_attachments WHERE id = ?') .get(result.lastInsertRowid); logger.info(`Knowledge-Anhang hochgeladen: ${req.file.originalname} für Eintrag ${entry.title}`); res.status(201).json({ id: attachment.id, entryId: attachment.entry_id, filename: attachment.filename, originalName: attachment.original_name, mimeType: attachment.mime_type, sizeBytes: attachment.size_bytes, uploadedBy: attachment.uploaded_by, uploadedAt: attachment.uploaded_at }); } catch (error) { logger.error('Fehler beim Hochladen des Knowledge-Anhangs:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/knowledge/attachments/download/:id * Anhang herunterladen */ router.get('/attachments/download/:id', (req, res) => { try { const attachmentId = req.params.id; const db = getDb(); const attachment = db.prepare('SELECT * FROM knowledge_attachments WHERE id = ?').get(attachmentId); if (!attachment) { return res.status(404).json({ error: 'Anhang nicht gefunden' }); } const filePath = path.join(UPLOAD_DIR, attachment.filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ error: 'Datei nicht gefunden' }); } res.download(filePath, attachment.original_name); } catch (error) { logger.error('Fehler beim Herunterladen des Knowledge-Anhangs:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/knowledge/attachments/:id * Anhang löschen */ router.delete('/attachments/:id', (req, res) => { try { const attachmentId = req.params.id; const db = getDb(); const attachment = db.prepare('SELECT * FROM knowledge_attachments WHERE id = ?').get(attachmentId); if (!attachment) { return res.status(404).json({ error: 'Anhang nicht gefunden' }); } // Datei vom Dateisystem löschen const filePath = path.join(UPLOAD_DIR, attachment.filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } // Aus Datenbank löschen db.prepare('DELETE FROM knowledge_attachments WHERE id = ?').run(attachmentId); // Eintrag updated_at aktualisieren db.prepare('UPDATE knowledge_entries SET updated_at = CURRENT_TIMESTAMP WHERE id = ?') .run(attachment.entry_id); logger.info(`Knowledge-Anhang gelöscht: ${attachment.original_name}`); res.json({ message: 'Anhang gelöscht' }); } catch (error) { logger.error('Fehler beim Löschen des Knowledge-Anhangs:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); // ===================== // SUCHE // ===================== /** * GET /api/knowledge/search * Wissensmanagement durchsuchen */ router.get('/search', (req, res) => { try { const { q } = req.query; if (!q || q.trim().length < 2) { return res.json({ categories: [], entries: [] }); } const searchTerm = `%${q.toLowerCase()}%`; const db = getDb(); // Kategorien durchsuchen const categories = db.prepare(` SELECT kc.*, (SELECT COUNT(*) FROM knowledge_entries WHERE category_id = kc.id) as entry_count FROM knowledge_categories kc WHERE LOWER(kc.name) LIKE ? OR LOWER(kc.description) LIKE ? ORDER BY kc.position `).all(searchTerm, searchTerm); // Einträge durchsuchen const entries = db.prepare(` SELECT ke.*, kc.name as category_name, kc.color as category_color, (SELECT COUNT(*) FROM knowledge_attachments WHERE entry_id = ke.id) as attachment_count FROM knowledge_entries ke LEFT JOIN knowledge_categories kc ON ke.category_id = kc.id WHERE LOWER(ke.title) LIKE ? OR LOWER(ke.notes) LIKE ? OR LOWER(ke.url) LIKE ? ORDER BY CASE WHEN LOWER(ke.title) LIKE ? THEN 1 ELSE 2 END, ke.created_at DESC LIMIT 50 `).all(searchTerm, searchTerm, searchTerm, searchTerm); res.json({ categories: categories.map(c => ({ id: c.id, name: c.name, description: c.description, color: c.color, icon: c.icon, entryCount: c.entry_count })), entries: entries.map(e => ({ id: e.id, categoryId: e.category_id, categoryName: e.category_name, categoryColor: e.category_color, title: e.title, url: e.url, notes: e.notes, attachmentCount: e.attachment_count, createdAt: e.created_at })) }); } catch (error) { logger.error('Fehler bei der Knowledge-Suche:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); module.exports = router;