/** * TASKMATE - Task Routes * ====================== * CRUD für Aufgaben */ const express = require('express'); const router = express.Router(); const { getDb } = require('../database'); const logger = require('../utils/logger'); const { validators } = require('../middleware/validation'); const notificationService = require('../services/notificationService'); /** * Hilfsfunktion: Historie-Eintrag erstellen */ function addHistory(db, taskId, userId, action, fieldChanged = null, oldValue = null, newValue = null) { db.prepare(` INSERT INTO history (task_id, user_id, action, field_changed, old_value, new_value) VALUES (?, ?, ?, ?, ?, ?) `).run(taskId, userId, action, fieldChanged, oldValue, newValue); } /** * Hilfsfunktion: Vollständige Task-Daten laden */ function getFullTask(db, taskId) { const task = db.prepare(` SELECT t.*, c.username as creator_name FROM tasks t LEFT JOIN users c ON t.created_by = c.id WHERE t.id = ? `).get(taskId); if (!task) return null; // Zugewiesene Mitarbeiter laden (Mehrfachzuweisung) const assignees = db.prepare(` SELECT u.id, u.username, u.display_name, u.color FROM task_assignees ta JOIN users u ON ta.user_id = u.id WHERE ta.task_id = ? ORDER BY u.username `).all(taskId); // Labels laden const labels = db.prepare(` SELECT l.* FROM labels l JOIN task_labels tl ON l.id = tl.label_id WHERE tl.task_id = ? `).all(taskId); // Subtasks laden const subtasks = db.prepare(` SELECT * FROM subtasks WHERE task_id = ? ORDER BY position `).all(taskId); // Anhänge zählen const attachmentCount = db.prepare( 'SELECT COUNT(*) as count FROM attachments WHERE task_id = ?' ).get(taskId).count; // Links zählen const linkCount = db.prepare( 'SELECT COUNT(*) as count FROM links WHERE task_id = ?' ).get(taskId).count; // Kommentare zählen const commentCount = db.prepare( 'SELECT COUNT(*) as count FROM comments WHERE task_id = ?' ).get(taskId).count; // Verknüpfte Genehmigungen laden const proposals = db.prepare(` SELECT p.id, p.title, p.approved, p.approved_by, u.display_name as approved_by_name FROM proposals p LEFT JOIN users u ON p.approved_by = u.id WHERE p.task_id = ? AND p.archived = 0 ORDER BY p.created_at DESC `).all(taskId); return { id: task.id, projectId: task.project_id, columnId: task.column_id, title: task.title, description: task.description, priority: task.priority, startDate: task.start_date, dueDate: task.due_date, // Neues Format: Array von Mitarbeitern assignees: assignees.map(a => ({ id: a.id, username: a.username, display_name: a.display_name, color: a.color })), // Rückwärtskompatibilität: assignedTo als erster Mitarbeiter (falls vorhanden) assignedTo: assignees.length > 0 ? assignees[0].id : null, assignedName: assignees.length > 0 ? assignees[0].username : null, assignedColor: assignees.length > 0 ? assignees[0].color : null, timeEstimateMin: task.time_estimate_min, dependsOn: task.depends_on, position: task.position, archived: !!task.archived, createdAt: task.created_at, createdBy: task.created_by, creatorName: task.creator_name, updatedAt: task.updated_at, labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })), subtasks: subtasks.map(s => ({ id: s.id, title: s.title, completed: !!s.completed, position: s.position })), subtaskProgress: { total: subtasks.length, completed: subtasks.filter(s => s.completed).length }, attachmentCount, linkCount, commentCount, proposals: proposals.map(p => ({ id: p.id, title: p.title, approved: !!p.approved, approvedByName: p.approved_by_name })) }; } /** * GET /api/tasks/all * Alle aktiven Aufgaben (nicht archiviert) fuer Auswahl in Vorschlaegen */ router.get('/all', (req, res) => { try { const db = getDb(); const tasks = db.prepare(` SELECT t.id, t.title, t.project_id, p.name as project_name FROM tasks t LEFT JOIN projects p ON t.project_id = p.id WHERE t.archived = 0 ORDER BY p.name, t.title `).all(); res.json(tasks); } catch (error) { logger.error('Fehler beim Abrufen aller Aufgaben:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/tasks/project/:projectId * Alle Aufgaben eines Projekts */ router.get('/project/:projectId', (req, res) => { try { const db = getDb(); const projectId = req.params.projectId; const includeArchived = req.query.archived === 'true'; let query = ` SELECT t.id FROM tasks t WHERE t.project_id = ? `; if (!includeArchived) { query += ' AND t.archived = 0'; } query += ' ORDER BY t.column_id, t.position'; const taskIds = db.prepare(query).all(projectId); const tasks = taskIds.map(t => getFullTask(db, t.id)); res.json(tasks); } catch (error) { logger.error('Fehler beim Abrufen der Aufgaben:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/tasks/search * Aufgaben suchen - durchsucht auch Subtasks, Links, Anhänge und Kommentare * WICHTIG: Diese Route MUSS vor /:id definiert werden! */ router.get('/search', (req, res) => { try { const { q, projectId, assignedTo, priority, dueBefore, dueAfter, labels, archived } = req.query; const db = getDb(); const params = []; // Basis-Query mit LEFT JOINs für tiefe Suche let query = ` SELECT DISTINCT t.id FROM tasks t LEFT JOIN subtasks s ON t.id = s.task_id LEFT JOIN links l ON t.id = l.task_id LEFT JOIN attachments a ON t.id = a.task_id LEFT JOIN comments c ON t.id = c.task_id WHERE 1=1 `; if (projectId) { query += ' AND t.project_id = ?'; params.push(projectId); } // Erweiterte Textsuche: Titel, Beschreibung, Subtasks, Links, Anhänge, Kommentare if (q) { const searchTerm = `%${q}%`; query += ` AND ( t.title LIKE ? OR t.description LIKE ? OR s.title LIKE ? OR l.url LIKE ? OR l.title LIKE ? OR a.original_name LIKE ? OR c.content LIKE ? )`; params.push(searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm); } if (assignedTo) { query += ' AND t.assigned_to = ?'; params.push(assignedTo); } if (priority) { query += ' AND t.priority = ?'; params.push(priority); } if (dueBefore) { query += ' AND t.due_date <= ?'; params.push(dueBefore); } if (dueAfter) { query += ' AND t.due_date >= ?'; params.push(dueAfter); } if (archived !== 'true') { query += ' AND t.archived = 0'; } if (labels) { const labelIds = labels.split(',').map(id => parseInt(id)).filter(id => !isNaN(id)); if (labelIds.length > 0) { query += ` AND t.id IN ( SELECT task_id FROM task_labels WHERE label_id IN (${labelIds.map(() => '?').join(',')}) )`; params.push(...labelIds); } } query += ' ORDER BY t.due_date ASC, t.priority DESC, t.updated_at DESC LIMIT 100'; const taskIds = db.prepare(query).all(...params); const tasks = taskIds.map(t => getFullTask(db, t.id)); logger.info(`Suche nach "${q}" in Projekt ${projectId}: ${tasks.length} Treffer`); res.json(tasks); } catch (error) { logger.error('Fehler bei der Suche:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/tasks/:id * Einzelne Aufgabe mit allen Details */ router.get('/:id', (req, res) => { try { const db = getDb(); const task = getFullTask(db, req.params.id); if (!task) { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } // Historie laden const history = db.prepare(` SELECT h.*, u.display_name, u.color FROM history h JOIN users u ON h.user_id = u.id WHERE h.task_id = ? ORDER BY h.timestamp DESC LIMIT 50 `).all(req.params.id); task.history = history.map(h => ({ id: h.id, action: h.action, fieldChanged: h.field_changed, oldValue: h.old_value, newValue: h.new_value, timestamp: h.timestamp, userName: h.display_name, userColor: h.color })); res.json(task); } catch (error) { logger.error('Fehler beim Abrufen der Aufgabe:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/tasks * Neue Aufgabe erstellen */ router.post('/', (req, res) => { try { const { projectId, columnId, title, description, priority, startDate, dueDate, assignees, assignedTo, timeEstimateMin, dependsOn, labels } = req.body; // Validierung const errors = []; errors.push(validators.required(projectId, 'Projekt-ID')); errors.push(validators.required(columnId, 'Spalten-ID')); errors.push(validators.required(title, 'Titel')); errors.push(validators.maxLength(title, 200, 'Titel')); if (priority) errors.push(validators.enum(priority, ['low', 'medium', 'high'], 'Priorität')); if (startDate) errors.push(validators.date(startDate, 'Startdatum')); if (dueDate) errors.push(validators.date(dueDate, 'Fälligkeitsdatum')); if (timeEstimateMin) errors.push(validators.positiveInteger(timeEstimateMin, 'Zeitschätzung')); const firstError = errors.find(e => e !== null); if (firstError) { return res.status(400).json({ error: firstError }); } const db = getDb(); // Höchste Position in der Spalte ermitteln const maxPos = db.prepare( 'SELECT COALESCE(MAX(position), -1) as max FROM tasks WHERE column_id = ?' ).get(columnId).max; // Aufgabe erstellen (ohne assigned_to, wird über task_assignees gemacht) const result = db.prepare(` INSERT INTO tasks ( project_id, column_id, title, description, priority, start_date, due_date, time_estimate_min, depends_on, position, created_by ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( projectId, columnId, title, description || null, priority || 'medium', startDate || null, dueDate || null, timeEstimateMin || null, dependsOn || null, maxPos + 1, req.user.id ); const taskId = result.lastInsertRowid; // Mitarbeiter zuweisen (Mehrfachzuweisung) const assigneeIds = assignees && Array.isArray(assignees) ? assignees : (assignedTo ? [assignedTo] : []); if (assigneeIds.length > 0) { const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)'); assigneeIds.forEach(userId => { try { insertAssignee.run(taskId, userId); } catch (e) { // User existiert nicht oder bereits zugewiesen } }); } // Labels zuweisen if (labels && Array.isArray(labels)) { const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)'); labels.forEach(labelId => { try { insertLabel.run(taskId, labelId); } catch (e) { // Label existiert nicht, ignorieren } }); } // Historie addHistory(db, taskId, req.user.id, 'created'); const task = getFullTask(db, taskId); logger.info(`Aufgabe erstellt: ${title} (ID: ${taskId}) von ${req.user.username}`); // WebSocket const io = req.app.get('io'); io.to(`project:${projectId}`).emit('task:created', task); // Benachrichtigungen an zugewiesene Mitarbeiter (außer Ersteller) if (assigneeIds.length > 0) { assigneeIds.forEach(assigneeId => { if (assigneeId !== req.user.id) { notificationService.create(assigneeId, 'task:assigned', { taskId: taskId, taskTitle: title, projectId: projectId, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); } res.status(201).json(task); } catch (error) { logger.error('Fehler beim Erstellen der Aufgabe:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/tasks/:id * Aufgabe aktualisieren */ router.put('/:id', (req, res) => { try { const taskId = req.params.id; const { title, description, priority, columnId, startDate, dueDate, assignees, assignedTo, timeEstimateMin, dependsOn, labels } = req.body; const db = getDb(); const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId); if (!existing) { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } // Validierung if (title) { const titleError = validators.maxLength(title, 200, 'Titel'); if (titleError) return res.status(400).json({ error: titleError }); } if (priority) { const prioError = validators.enum(priority, ['low', 'medium', 'high'], 'Priorität'); if (prioError) return res.status(400).json({ error: prioError }); } if (startDate) { const startDateError = validators.date(startDate, 'Startdatum'); if (startDateError) return res.status(400).json({ error: startDateError }); } if (dueDate) { const dateError = validators.date(dueDate, 'Fälligkeitsdatum'); if (dateError) return res.status(400).json({ error: dateError }); } // Änderungen tracken für Historie const changes = []; if (title !== undefined && title !== existing.title) { changes.push({ field: 'title', old: existing.title, new: title }); } if (description !== undefined && description !== existing.description) { changes.push({ field: 'description', old: existing.description, new: description }); } if (priority !== undefined && priority !== existing.priority) { changes.push({ field: 'priority', old: existing.priority, new: priority }); } if (columnId !== undefined && columnId !== existing.column_id) { const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(existing.column_id); const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(columnId); changes.push({ field: 'column', old: oldColumn?.name, new: newColumn?.name }); } if (startDate !== undefined && startDate !== existing.start_date) { changes.push({ field: 'start_date', old: existing.start_date, new: startDate }); } if (dueDate !== undefined && dueDate !== existing.due_date) { changes.push({ field: 'due_date', old: existing.due_date, new: dueDate }); } if (timeEstimateMin !== undefined && timeEstimateMin !== existing.time_estimate_min) { changes.push({ field: 'time_estimate', old: String(existing.time_estimate_min), new: String(timeEstimateMin) }); } if (dependsOn !== undefined && dependsOn !== existing.depends_on) { changes.push({ field: 'depends_on', old: String(existing.depends_on), new: String(dependsOn) }); } // Aufgabe aktualisieren (ohne assigned_to) db.prepare(` UPDATE tasks SET title = COALESCE(?, title), description = ?, priority = COALESCE(?, priority), column_id = ?, start_date = ?, due_date = ?, time_estimate_min = ?, depends_on = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run( title || null, description !== undefined ? description : existing.description, priority || null, columnId !== undefined ? columnId : existing.column_id, startDate !== undefined ? startDate : existing.start_date, dueDate !== undefined ? dueDate : existing.due_date, timeEstimateMin !== undefined ? timeEstimateMin : existing.time_estimate_min, dependsOn !== undefined ? dependsOn : existing.depends_on, taskId ); // Mitarbeiter aktualisieren (Mehrfachzuweisung) if (assignees !== undefined && Array.isArray(assignees)) { // Alte Zuweisungen entfernen db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId); // Neue Zuweisungen hinzufügen const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)'); assignees.forEach(userId => { try { insertAssignee.run(taskId, userId); } catch (e) { // User existiert nicht oder bereits zugewiesen } }); changes.push({ field: 'assignees', old: 'changed', new: 'changed' }); } else if (assignedTo !== undefined) { // Rückwärtskompatibilität: einzelne Zuweisung db.prepare('DELETE FROM task_assignees WHERE task_id = ?').run(taskId); if (assignedTo) { try { db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)').run(taskId, assignedTo); } catch (e) { // Ignorieren } } changes.push({ field: 'assignees', old: 'changed', new: 'changed' }); } // Labels aktualisieren if (labels !== undefined && Array.isArray(labels)) { // Alte Labels entfernen db.prepare('DELETE FROM task_labels WHERE task_id = ?').run(taskId); // Neue Labels zuweisen const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)'); labels.forEach(labelId => { try { insertLabel.run(taskId, labelId); } catch (e) { // Label existiert nicht } }); changes.push({ field: 'labels', old: 'changed', new: 'changed' }); } // Historie-Einträge changes.forEach(change => { addHistory(db, taskId, req.user.id, 'updated', change.field, change.old, change.new); }); const task = getFullTask(db, taskId); logger.info(`Aufgabe aktualisiert: ${task.title} (ID: ${taskId})`); // WebSocket const io = req.app.get('io'); io.to(`project:${existing.project_id}`).emit('task:updated', task); // Benachrichtigungen für Änderungen senden const taskTitle = title || existing.title; // Zuweisung geändert - neue Mitarbeiter benachrichtigen if (assignees !== undefined && Array.isArray(assignees)) { // Alte Mitarbeiter ermitteln const oldAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId); const oldAssigneeIds = oldAssignees.map(a => a.user_id); // Neue Mitarbeiter (die vorher nicht zugewiesen waren) const newAssigneeIds = assignees.filter(id => !oldAssigneeIds.includes(id)); newAssigneeIds.forEach(assigneeId => { if (assigneeId !== req.user.id) { notificationService.create(assigneeId, 'task:assigned', { taskId: parseInt(taskId), taskTitle: taskTitle, projectId: existing.project_id, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); // Entfernte Mitarbeiter const removedAssigneeIds = oldAssigneeIds.filter(id => !assignees.includes(id)); removedAssigneeIds.forEach(assigneeId => { if (assigneeId !== req.user.id) { notificationService.create(assigneeId, 'task:unassigned', { taskId: parseInt(taskId), taskTitle: taskTitle, projectId: existing.project_id, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); } // Priorität auf hoch gesetzt if (priority === 'high' && existing.priority !== 'high') { // Alle Assignees benachrichtigen const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId); currentAssignees.forEach(a => { if (a.user_id !== req.user.id) { notificationService.create(a.user_id, 'task:priority_up', { taskId: parseInt(taskId), taskTitle: taskTitle, projectId: existing.project_id, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); } // Fälligkeitsdatum geändert if (dueDate !== undefined && dueDate !== existing.due_date) { const currentAssignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId); currentAssignees.forEach(a => { if (a.user_id !== req.user.id) { notificationService.create(a.user_id, 'task:due_changed', { taskId: parseInt(taskId), taskTitle: taskTitle, projectId: existing.project_id, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); } res.json(task); } catch (error) { logger.error('Fehler beim Aktualisieren der Aufgabe:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/tasks/:id/move * Aufgabe verschieben (Spalte/Position) */ router.put('/:id/move', (req, res) => { try { const taskId = req.params.id; const { columnId, position } = req.body; const db = getDb(); const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId); if (!task) { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } const oldColumnId = task.column_id; const oldPosition = task.position; const newColumnId = columnId || oldColumnId; const newPosition = position !== undefined ? position : oldPosition; // Spaltenname für Historie const oldColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(oldColumnId); const newColumn = db.prepare('SELECT name FROM columns WHERE id = ?').get(newColumnId); if (oldColumnId !== newColumnId) { // In andere Spalte verschoben // Positionen in alter Spalte anpassen db.prepare(` UPDATE tasks SET position = position - 1 WHERE column_id = ? AND position > ? `).run(oldColumnId, oldPosition); // Positionen in neuer Spalte anpassen db.prepare(` UPDATE tasks SET position = position + 1 WHERE column_id = ? AND position >= ? `).run(newColumnId, newPosition); // Task verschieben db.prepare(` UPDATE tasks SET column_id = ?, position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(newColumnId, newPosition, taskId); addHistory(db, taskId, req.user.id, 'moved', 'column', oldColumn?.name, newColumn?.name); } else if (oldPosition !== newPosition) { // Innerhalb der Spalte verschoben if (newPosition > oldPosition) { db.prepare(` UPDATE tasks SET position = position - 1 WHERE column_id = ? AND position > ? AND position <= ? `).run(newColumnId, oldPosition, newPosition); } else { db.prepare(` UPDATE tasks SET position = position + 1 WHERE column_id = ? AND position >= ? AND position < ? `).run(newColumnId, newPosition, oldPosition); } db.prepare(` UPDATE tasks SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(newPosition, taskId); } const updatedTask = getFullTask(db, taskId); logger.info(`Aufgabe verschoben: ${task.title} -> ${newColumn?.name || 'Position ' + newPosition}`); // WebSocket const io = req.app.get('io'); io.to(`project:${task.project_id}`).emit('task:moved', { task: updatedTask, oldColumnId, newColumnId, oldPosition, newPosition }); // Benachrichtigung wenn in Erledigt-Spalte verschoben if (oldColumnId !== newColumnId) { const newColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(newColumnId); const oldColumnFull = db.prepare('SELECT filter_category FROM columns WHERE id = ?').get(oldColumnId); // Prüfen ob in Erledigt-Spalte verschoben (und vorher nicht dort war) if (newColumnFull?.filter_category === 'completed' && oldColumnFull?.filter_category !== 'completed') { // Alle Assignees benachrichtigen const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId); assignees.forEach(a => { if (a.user_id !== req.user.id) { notificationService.create(a.user_id, 'task:completed', { taskId: parseInt(taskId), taskTitle: task.title, projectId: task.project_id, actorId: req.user.id, actorName: req.user.display_name || req.user.username }, io); } }); } } res.json(updatedTask); } catch (error) { logger.error('Fehler beim Verschieben der Aufgabe:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/tasks/:id/duplicate * Aufgabe duplizieren */ router.post('/:id/duplicate', (req, res) => { try { const taskId = req.params.id; const db = getDb(); const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId); if (!task) { 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 tasks WHERE column_id = ?' ).get(task.column_id).max; // Aufgabe duplizieren const result = db.prepare(` INSERT INTO tasks ( project_id, column_id, title, description, priority, due_date, assigned_to, time_estimate_min, position, created_by ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( task.project_id, task.column_id, task.title + ' (Kopie)', task.description, task.priority, task.due_date, task.assigned_to, task.time_estimate_min, maxPos + 1, req.user.id ); const newTaskId = result.lastInsertRowid; // Labels kopieren const taskLabels = db.prepare('SELECT label_id FROM task_labels WHERE task_id = ?').all(taskId); const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)'); taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id)); // Subtasks kopieren const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId); const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)'); subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, idx)); addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`); const newTask = getFullTask(db, newTaskId); logger.info(`Aufgabe dupliziert: ${task.title} -> ${newTask.title}`); // WebSocket const io = req.app.get('io'); io.to(`project:${task.project_id}`).emit('task:created', newTask); res.status(201).json(newTask); } catch (error) { logger.error('Fehler beim Duplizieren:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/tasks/:id/archive * Aufgabe archivieren */ router.put('/:id/archive', (req, res) => { try { const taskId = req.params.id; const { archived } = req.body; const db = getDb(); const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId); if (!task) { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } db.prepare('UPDATE tasks SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') .run(archived ? 1 : 0, taskId); addHistory(db, taskId, req.user.id, archived ? 'archived' : 'restored'); logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`); // WebSocket const io = req.app.get('io'); io.to(`project:${task.project_id}`).emit('task:archived', { id: taskId, archived: !!archived }); res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' }); } catch (error) { logger.error('Fehler beim Archivieren:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/tasks/:id * Aufgabe löschen */ router.delete('/:id', (req, res) => { try { const taskId = req.params.id; const db = getDb(); const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId); if (!task) { return res.status(404).json({ error: 'Aufgabe nicht gefunden' }); } db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId); // Positionen neu nummerieren const remainingTasks = db.prepare( 'SELECT id FROM tasks WHERE column_id = ? ORDER BY position' ).all(task.column_id); remainingTasks.forEach((t, idx) => { db.prepare('UPDATE tasks SET position = ? WHERE id = ?').run(idx, t.id); }); logger.info(`Aufgabe gelöscht: ${task.title} (ID: ${taskId})`); // WebSocket const io = req.app.get('io'); io.to(`project:${task.project_id}`).emit('task:deleted', { id: taskId, columnId: task.column_id }); res.json({ message: 'Aufgabe gelöscht' }); } catch (error) { logger.error('Fehler beim Löschen:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); module.exports = router;