Implementierung Wissensmanagement
Dieser Commit ist enthalten in:
944
backend/routes/knowledge.js
Normale Datei
944
backend/routes/knowledge.js
Normale Datei
@ -0,0 +1,944 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -57,16 +57,16 @@ router.post('/', (req, res) => {
|
||||
return res.status(404).json({ error: 'Aufgabe nicht gefunden' });
|
||||
}
|
||||
|
||||
// Höchste Position ermitteln
|
||||
const maxPos = db.prepare(
|
||||
'SELECT COALESCE(MAX(position), -1) as max FROM subtasks WHERE task_id = ?'
|
||||
).get(taskId).max;
|
||||
// Alle bestehenden Subtasks um eine Position nach unten verschieben
|
||||
db.prepare(`
|
||||
UPDATE subtasks SET position = position + 1 WHERE task_id = ?
|
||||
`).run(taskId);
|
||||
|
||||
// Subtask erstellen
|
||||
// Neue Subtask an Position 0 erstellen (immer an erster Stelle)
|
||||
const result = db.prepare(`
|
||||
INSERT INTO subtasks (task_id, title, position)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(taskId, title, maxPos + 1);
|
||||
VALUES (?, ?, 0)
|
||||
`).run(taskId, title);
|
||||
|
||||
// Task updated_at aktualisieren
|
||||
db.prepare('UPDATE tasks SET updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(taskId);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren