Initial commit
Dieser Commit ist enthalten in:
310
backend/routes/stats.js
Normale Datei
310
backend/routes/stats.js
Normale Datei
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren