311 Zeilen
9.0 KiB
JavaScript
311 Zeilen
9.0 KiB
JavaScript
/**
|
|
* 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;
|