945 Zeilen
28 KiB
JavaScript
945 Zeilen
28 KiB
JavaScript
/**
|
|
* 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');
|
|
|
|
// 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' });
|
|
}
|
|
|
|
// Position ermitteln
|
|
const lastPosition = db.prepare(
|
|
'SELECT MAX(position) as max_pos FROM knowledge_categories'
|
|
).get();
|
|
const position = (lastPosition.max_pos ?? -1) + 1;
|
|
|
|
// 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,
|
|
position,
|
|
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' });
|
|
}
|
|
|
|
// Position ermitteln
|
|
const lastPosition = db.prepare(
|
|
'SELECT MAX(position) as max_pos FROM knowledge_entries WHERE category_id = ?'
|
|
).get(categoryId);
|
|
const position = (lastPosition.max_pos ?? -1) + 1;
|
|
|
|
// 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,
|
|
position,
|
|
req.user.id
|
|
);
|
|
|
|
const entry = db.prepare('SELECT * FROM knowledge_entries WHERE id = ?')
|
|
.get(result.lastInsertRowid);
|
|
|
|
logger.info(`Knowledge-Eintrag erstellt: ${title}`);
|
|
|
|
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('file'), (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;
|