/** * TASKMATE - Stats Routes * ======================= * Dashboard-Statistiken */ const express = require('express'); const router = express.Router(); const { getDb } = require('../database'); const logger = require('../utils/logger'); /** * GET /api/stats/dashboard * Haupt-Dashboard Statistiken */ router.get('/dashboard', (req, res) => { try { const db = getDb(); const { projectId } = req.query; let projectFilter = ''; const params = []; if (projectId) { projectFilter = ' AND t.project_id = ?'; params.push(projectId); } // Gesamtzahlen const total = db.prepare(` SELECT COUNT(*) as count FROM tasks t WHERE t.archived = 0 ${projectFilter} `).get(...params).count; // Offene Aufgaben (erste Spalte jedes Projekts) const open = db.prepare(` SELECT COUNT(*) as count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND c.position = 0 ${projectFilter} `).get(...params).count; // In Arbeit (mittlere Spalten) const inProgress = db.prepare(` SELECT COUNT(*) as count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND c.position > 0 AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ${projectFilter} `).get(...params).count; // Erledigt (letzte Spalte) const completed = db.prepare(` SELECT COUNT(*) as count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND c.position = (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ${projectFilter} `).get(...params).count; // Überfällig const overdue = db.prepare(` SELECT COUNT(*) as count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND t.due_date < date('now') AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ${projectFilter} `).get(...params).count; // Heute fällig const dueToday = db.prepare(` SELECT t.id, t.title, t.priority, t.assigned_to, u.display_name as assigned_name, u.color as assigned_color FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND t.due_date = date('now') AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ${projectFilter} ORDER BY t.priority DESC LIMIT 10 `).all(...params); // Bald fällig (nächste 7 Tage) const dueSoon = db.prepare(` SELECT COUNT(*) as count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND t.due_date BETWEEN date('now', '+1 day') AND date('now', '+7 days') AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ${projectFilter} `).get(...params).count; res.json({ total, open, inProgress, completed, overdue, dueSoon, dueToday: dueToday.map(t => ({ id: t.id, title: t.title, priority: t.priority, assignedTo: t.assigned_to, assignedName: t.assigned_name, assignedColor: t.assigned_color })) }); } catch (error) { logger.error('Fehler bei Dashboard-Stats:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/stats/completed-per-week * Erledigte Aufgaben pro Woche */ router.get('/completed-per-week', (req, res) => { try { const db = getDb(); const { projectId, weeks = 8 } = req.query; let projectFilter = ''; const params = [parseInt(weeks)]; if (projectId) { projectFilter = ' AND h.task_id IN (SELECT id FROM tasks WHERE project_id = ?)'; params.push(projectId); } // Erledigte Aufgaben pro Kalenderwoche const stats = db.prepare(` SELECT strftime('%Y-%W', h.timestamp) as week, COUNT(DISTINCT h.task_id) as count FROM history h WHERE h.action = 'moved' AND h.new_value IN ( SELECT name FROM columns c WHERE c.position = (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) ) AND h.timestamp >= date('now', '-' || ? || ' weeks') ${projectFilter} GROUP BY week ORDER BY week DESC `).all(...params); // Letzten X Wochen mit 0 auffüllen const result = []; const now = new Date(); for (let i = 0; i < parseInt(weeks); i++) { const date = new Date(now); date.setDate(date.getDate() - (i * 7)); const year = date.getFullYear(); const week = getWeekNumber(date); const weekKey = `${year}-${week.toString().padStart(2, '0')}`; const found = stats.find(s => s.week === weekKey); result.unshift({ week: weekKey, label: `KW${week}`, count: found ? found.count : 0 }); } res.json(result); } catch (error) { logger.error('Fehler bei Completed-per-Week Stats:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/stats/time-per-project * Geschätzte Zeit pro Projekt */ router.get('/time-per-project', (req, res) => { try { const db = getDb(); const stats = db.prepare(` SELECT p.id, p.name, COALESCE(SUM(t.time_estimate_min), 0) as total_minutes, COUNT(t.id) as task_count FROM projects p LEFT JOIN tasks t ON p.id = t.project_id AND t.archived = 0 WHERE p.archived = 0 GROUP BY p.id ORDER BY total_minutes DESC `).all(); res.json(stats.map(s => ({ id: s.id, name: s.name, totalMinutes: s.total_minutes, totalHours: Math.round(s.total_minutes / 60 * 10) / 10, taskCount: s.task_count }))); } catch (error) { logger.error('Fehler bei Time-per-Project Stats:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/stats/user-activity * Aktivität pro Benutzer */ router.get('/user-activity', (req, res) => { try { const db = getDb(); const { days = 30 } = req.query; const stats = db.prepare(` SELECT u.id, u.display_name, u.color, COUNT(DISTINCT CASE WHEN h.action = 'created' THEN h.task_id END) as tasks_created, COUNT(DISTINCT CASE WHEN h.action = 'moved' THEN h.task_id END) as tasks_moved, COUNT(DISTINCT CASE WHEN h.action = 'commented' THEN h.id END) as comments, COUNT(h.id) as total_actions FROM users u LEFT JOIN history h ON u.id = h.user_id AND h.timestamp >= date('now', '-' || ? || ' days') GROUP BY u.id ORDER BY total_actions DESC `).all(parseInt(days)); res.json(stats.map(s => ({ id: s.id, displayName: s.display_name, color: s.color, tasksCreated: s.tasks_created, tasksMoved: s.tasks_moved, comments: s.comments, totalActions: s.total_actions }))); } catch (error) { logger.error('Fehler bei User-Activity Stats:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/stats/calendar * Aufgaben nach Datum (für Kalender) */ router.get('/calendar', (req, res) => { try { const db = getDb(); const { projectId, month, year } = req.query; const currentYear = year || new Date().getFullYear(); const currentMonth = month || (new Date().getMonth() + 1); // Start und Ende des Monats const startDate = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-01`; const endDate = `${currentYear}-${currentMonth.toString().padStart(2, '0')}-31`; let query = ` SELECT t.due_date, COUNT(*) as count, SUM(CASE WHEN t.due_date < date('now') AND c.position < (SELECT MAX(position) FROM columns WHERE project_id = c.project_id) THEN 1 ELSE 0 END) as overdue_count FROM tasks t JOIN columns c ON t.column_id = c.id WHERE t.archived = 0 AND t.due_date BETWEEN ? AND ? `; const params = [startDate, endDate]; if (projectId) { query += ' AND t.project_id = ?'; params.push(projectId); } query += ' GROUP BY t.due_date ORDER BY t.due_date'; const stats = db.prepare(query).all(...params); res.json(stats.map(s => ({ date: s.due_date, count: s.count, overdueCount: s.overdue_count }))); } catch (error) { logger.error('Fehler bei Calendar Stats:', { error: error.message }); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * Hilfsfunktion: Kalenderwoche berechnen */ function getWeekNumber(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d - yearStart) / 86400000) + 1) / 7); } module.exports = router;