/** * TASKMATE - Project Routes * ========================= * CRUD für Projekte */ const express = require('express'); const router = express.Router(); const { getDb } = require('../database'); const logger = require('../utils/logger'); const { validators } = require('../middleware/validation'); /** * GET /api/projects * Alle Projekte abrufen */ router.get('/', (req, res) => { try { const db = getDb(); const includeArchived = req.query.archived === 'true'; let query = ` SELECT p.*, u.display_name as creator_name, (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0) as task_count, (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived = 0 AND t.column_id IN (SELECT c.id FROM columns c WHERE c.project_id = p.id ORDER BY c.position DESC LIMIT 1)) as completed_count FROM projects p LEFT JOIN users u ON p.created_by = u.id `; if (!includeArchived) { query += ' WHERE p.archived = 0'; } query += ' ORDER BY p.created_at DESC'; const projects = db.prepare(query).all(); res.json(projects.map(p => ({ id: p.id, name: p.name, description: p.description, archived: !!p.archived, createdAt: p.created_at, createdBy: p.created_by, creatorName: p.creator_name, taskCount: p.task_count, completedCount: p.completed_count }))); } catch (error) { logger.error('Fehler beim Abrufen der Projekte:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/projects/:id * Einzelnes Projekt mit Spalten und Aufgaben */ router.get('/:id', (req, res) => { try { const db = getDb(); const projectId = req.params.id; // Projekt abrufen const project = db.prepare(` SELECT p.*, u.display_name as creator_name FROM projects p LEFT JOIN users u ON p.created_by = u.id WHERE p.id = ? `).get(projectId); if (!project) { return res.status(404).json({ error: 'Projekt nicht gefunden' }); } // Spalten abrufen const columns = db.prepare(` SELECT * FROM columns WHERE project_id = ? ORDER BY position `).all(projectId); // Labels abrufen const labels = db.prepare(` SELECT * FROM labels WHERE project_id = ? `).all(projectId); res.json({ id: project.id, name: project.name, description: project.description, archived: !!project.archived, createdAt: project.created_at, createdBy: project.created_by, creatorName: project.creator_name, columns: columns.map(c => ({ id: c.id, name: c.name, position: c.position, color: c.color })), labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })) }); } catch (error) { logger.error('Fehler beim Abrufen des Projekts:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/projects * Neues Projekt erstellen */ router.post('/', (req, res) => { try { const { name, description } = req.body; // Validierung const nameError = validators.required(name, 'Name') || validators.maxLength(name, 100, 'Name'); if (nameError) { return res.status(400).json({ error: nameError }); } const db = getDb(); // Projekt erstellen const result = db.prepare(` INSERT INTO projects (name, description, created_by) VALUES (?, ?, ?) `).run(name, description || null, req.user.id); const projectId = result.lastInsertRowid; // Standard-Spalten erstellen const insertColumn = db.prepare(` INSERT INTO columns (project_id, name, position) VALUES (?, ?, ?) `); insertColumn.run(projectId, 'Offen', 0); insertColumn.run(projectId, 'In Arbeit', 1); insertColumn.run(projectId, 'Erledigt', 2); // Standard-Labels erstellen const insertLabel = db.prepare(` INSERT INTO labels (project_id, name, color) VALUES (?, ?, ?) `); insertLabel.run(projectId, 'Bug', '#DC2626'); insertLabel.run(projectId, 'Feature', '#059669'); insertLabel.run(projectId, 'Dokumentation', '#3182CE'); // Projekt mit Spalten und Labels zurückgeben const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId); const columns = db.prepare('SELECT * FROM columns WHERE project_id = ? ORDER BY position').all(projectId); const labels = db.prepare('SELECT * FROM labels WHERE project_id = ?').all(projectId); logger.info(`Projekt erstellt: ${name} (ID: ${projectId}) von ${req.user.username}`); // WebSocket: Andere Clients benachrichtigen const io = req.app.get('io'); io.emit('project:created', { id: projectId, name: project.name, description: project.description, createdBy: req.user.id }); res.status(201).json({ id: project.id, name: project.name, description: project.description, archived: false, createdAt: project.created_at, createdBy: project.created_by, columns: columns.map(c => ({ id: c.id, name: c.name, position: c.position, color: c.color })), labels: labels.map(l => ({ id: l.id, name: l.name, color: l.color })) }); } catch (error) { logger.error('Fehler beim Erstellen des Projekts:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/projects/:id * Projekt aktualisieren */ router.put('/:id', (req, res) => { try { const projectId = req.params.id; const { name, description } = req.body; // Validierung if (name) { const nameError = validators.maxLength(name, 100, 'Name'); if (nameError) { return res.status(400).json({ error: nameError }); } } const db = getDb(); // Prüfen ob Projekt existiert const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId); if (!existing) { return res.status(404).json({ error: 'Projekt nicht gefunden' }); } // Aktualisieren db.prepare(` UPDATE projects SET name = COALESCE(?, name), description = COALESCE(?, description) WHERE id = ? `).run(name || null, description !== undefined ? description : null, projectId); const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId); logger.info(`Projekt aktualisiert: ${project.name} (ID: ${projectId})`); // WebSocket const io = req.app.get('io'); io.emit('project:updated', { id: project.id, name: project.name, description: project.description }); res.json({ id: project.id, name: project.name, description: project.description, archived: !!project.archived, createdAt: project.created_at }); } catch (error) { logger.error('Fehler beim Aktualisieren des Projekts:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/projects/:id/archive * Projekt archivieren/wiederherstellen */ router.put('/:id/archive', (req, res) => { try { const projectId = req.params.id; const { archived } = req.body; const db = getDb(); const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId); if (!existing) { return res.status(404).json({ error: 'Projekt nicht gefunden' }); } db.prepare('UPDATE projects SET archived = ? WHERE id = ?') .run(archived ? 1 : 0, projectId); logger.info(`Projekt ${archived ? 'archiviert' : 'wiederhergestellt'}: ${existing.name}`); // WebSocket const io = req.app.get('io'); io.emit('project:archived', { id: projectId, archived: !!archived }); res.json({ message: archived ? 'Projekt archiviert' : 'Projekt wiederhergestellt' }); } catch (error) { logger.error('Fehler beim Archivieren:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/projects/:id * Projekt löschen * Query param: force=true um alle zugehörigen Aufgaben mitzulöschen */ router.delete('/:id', (req, res) => { try { const projectId = req.params.id; const forceDelete = req.query.force === 'true'; const db = getDb(); const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId); if (!project) { return res.status(404).json({ error: 'Projekt nicht gefunden' }); } // Anzahl der Aufgaben ermitteln const taskCount = db.prepare( 'SELECT COUNT(*) as count FROM tasks WHERE project_id = ?' ).get(projectId).count; // Ohne force: Prüfen ob noch aktive Aufgaben existieren if (!forceDelete && taskCount > 0) { return res.status(400).json({ error: 'Projekt enthält noch Aufgaben. Verwende force=true um alles zu löschen.', taskCount: taskCount }); } // Bei force=true: Explizit alle zugehörigen Daten löschen if (forceDelete && taskCount > 0) { // Alle Task-IDs für das Projekt holen const taskIds = db.prepare('SELECT id FROM tasks WHERE project_id = ?') .all(projectId) .map(t => t.id); if (taskIds.length > 0) { const placeholders = taskIds.map(() => '?').join(','); // Anhänge löschen db.prepare(`DELETE FROM attachments WHERE task_id IN (${placeholders})`).run(...taskIds); // Kommentare löschen db.prepare(`DELETE FROM comments WHERE task_id IN (${placeholders})`).run(...taskIds); // Task-Labels löschen db.prepare(`DELETE FROM task_labels WHERE task_id IN (${placeholders})`).run(...taskIds); // Task-Assignees löschen (Mehrfachzuweisung) db.prepare(`DELETE FROM task_assignees WHERE task_id IN (${placeholders})`).run(...taskIds); // Unteraufgaben löschen db.prepare(`DELETE FROM subtasks WHERE task_id IN (${placeholders})`).run(...taskIds); // Links löschen db.prepare(`DELETE FROM links WHERE task_id IN (${placeholders})`).run(...taskIds); // Historie löschen db.prepare(`DELETE FROM history WHERE task_id IN (${placeholders})`).run(...taskIds); // Tasks löschen db.prepare(`DELETE FROM tasks WHERE project_id = ?`).run(projectId); } logger.info(`${taskCount} Aufgaben gelöscht für Projekt: ${project.name}`); } // Labels des Projekts löschen db.prepare('DELETE FROM labels WHERE project_id = ?').run(projectId); // Spalten löschen db.prepare('DELETE FROM columns WHERE project_id = ?').run(projectId); // Projekt löschen db.prepare('DELETE FROM projects WHERE id = ?').run(projectId); logger.info(`Projekt gelöscht: ${project.name} (ID: ${projectId}), ${taskCount} Aufgaben entfernt`); // WebSocket const io = req.app.get('io'); io.emit('project:deleted', { id: projectId }); res.json({ message: 'Projekt gelöscht', deletedTasks: taskCount }); } catch (error) { logger.error('Fehler beim Löschen des Projekts:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); module.exports = router;