todo-helden/routes/families.js
Michi 0ebe7fa13d
Some checks failed
🚀 Continuous Integration / 🔧 Backend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🔧 Backend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (18.x) (push) Has been cancelled
🚀 Continuous Integration / 🎨 Frontend Tests (20.x) (push) Has been cancelled
🚀 Continuous Integration / 🔍 Code Quality (push) Has been cancelled
🚀 Continuous Integration / 🔒 Security Checks (push) Has been cancelled
🚀 Continuous Integration / 🎨 Theme Tests (push) Has been cancelled
🚀 Continuous Integration / ♿ Accessibility Tests (push) Has been cancelled
🚀 Continuous Integration / 📱 Cross-Browser Tests (push) Has been cancelled
🚀 Continuous Integration / 🏗️ Build Tests (push) Has been cancelled
🚀 Continuous Integration / 📊 Performance Tests (push) Has been cancelled
🚀 Continuous Integration / 🎯 Integration Tests (push) Has been cancelled
🚀 Continuous Integration / ✅ All Tests Passed (push) Has been cancelled
Initial commit: ToDo Kids v1.0.0
2025-08-04 15:46:08 +02:00

580 lines
16 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const User = require('../models/User');
const Child = require('../models/Child');
const Task = require('../models/Task');
const { Reward, RewardRequest } = require('../models/Reward');
const { auth } = require('../middleware/auth');
const router = express.Router();
// @route GET /api/families/dashboard
// @desc Dashboard-Daten für Eltern abrufen
// @access Private
router.get('/dashboard', auth, async (req, res) => {
try {
// Alle Kinder der Familie abrufen
const children = await Child.find({
parent: req.user.id,
isActive: true
}).sort({ createdAt: 1 });
const childIds = children.map(child => child._id);
// Aufgaben-Statistiken
const [totalTasks, pendingTasks, completedTasks, approvedTasks] = await Promise.all([
Task.countDocuments({ createdBy: req.user.id, isActive: true }),
Task.countDocuments({ createdBy: req.user.id, status: 'pending', isActive: true }),
Task.countDocuments({ createdBy: req.user.id, status: 'completed', isActive: true }),
Task.countDocuments({ createdBy: req.user.id, status: 'approved', isActive: true })
]);
// Heutige Aufgaben
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const todayTasks = await Task.find({
createdBy: req.user.id,
dueDate: { $gte: today, $lt: tomorrow },
isActive: true
}).populate('assignedTo', 'name avatar');
// Überfällige Aufgaben
const overdueTasks = await Task.find({
createdBy: req.user.id,
status: 'pending',
dueDate: { $lt: today },
isActive: true
}).populate('assignedTo', 'name avatar');
// Wartende Genehmigungen
const pendingApprovals = await Task.find({
createdBy: req.user.id,
status: 'completed',
isActive: true
}).populate('assignedTo', 'name avatar').sort({ completedAt: 1 });
// Belohnungs-Anfragen
const pendingRewards = await RewardRequest.find({
child: { $in: childIds },
status: 'pending'
})
.populate('reward', 'title cost icon')
.populate('child', 'name avatar')
.sort({ requestedAt: 1 });
// Familien-Statistiken
const familyStats = {
totalChildren: children.length,
totalPoints: children.reduce((sum, child) => sum + child.points.total, 0),
availablePoints: children.reduce((sum, child) => sum + child.points.available, 0),
totalTasks,
completionRate: totalTasks > 0 ? Math.round((approvedTasks / totalTasks) * 100) : 0
};
// Aktivitäts-Feed (letzte 10 Aktivitäten)
const recentActivities = await Task.find({
createdBy: req.user.id,
status: { $in: ['approved', 'completed'] },
isActive: true
})
.populate('assignedTo', 'name avatar')
.sort({ updatedAt: -1 })
.limit(10);
res.json({
success: true,
dashboard: {
family: {
name: req.user.familyName,
parent: req.user.parentName,
stats: familyStats
},
children: children.map(child => ({
id: child._id,
name: child.name,
avatar: child.avatar,
age: child.age,
points: child.points,
level: child.level,
stats: child.stats,
lastActivity: child.lastActivity
})),
tasks: {
today: todayTasks.length,
pending: pendingTasks,
completed: completedTasks,
approved: approvedTasks,
overdue: overdueTasks.length,
todayList: todayTasks.slice(0, 5), // Nur erste 5 für Dashboard
overdueList: overdueTasks.slice(0, 5),
pendingApprovals: pendingApprovals.slice(0, 5)
},
rewards: {
pendingRequests: pendingRewards.length,
pendingList: pendingRewards.slice(0, 5)
},
recentActivities: recentActivities.map(activity => ({
id: activity._id,
title: activity.title,
child: activity.assignedTo,
status: activity.status,
points: activity.points,
completedAt: activity.completedAt,
approvedAt: activity.approvedAt
}))
}
});
} catch (error) {
console.error('Dashboard-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen des Dashboards'
});
}
});
// @route GET /api/families/stats
// @desc Detaillierte Familien-Statistiken abrufen
// @access Private
router.get('/stats', auth, async (req, res) => {
try {
const { period = '30' } = req.query; // Tage
const days = parseInt(period);
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
startDate.setHours(0, 0, 0, 0);
// Alle Kinder der Familie
const children = await Child.find({
parent: req.user.id,
isActive: true
});
const childIds = children.map(child => child._id);
// Aufgaben-Statistiken für den Zeitraum
const [tasksCreated, tasksCompleted, tasksApproved] = await Promise.all([
Task.countDocuments({
createdBy: req.user.id,
createdAt: { $gte: startDate },
isActive: true
}),
Task.countDocuments({
createdBy: req.user.id,
status: 'completed',
completedAt: { $gte: startDate },
isActive: true
}),
Task.countDocuments({
createdBy: req.user.id,
status: 'approved',
approvedAt: { $gte: startDate },
isActive: true
})
]);
// Punkte-Statistiken
const pointsEarned = await Task.aggregate([
{
$match: {
createdBy: req.user._id,
status: 'approved',
approvedAt: { $gte: startDate },
isActive: true
}
},
{
$group: {
_id: null,
totalPoints: { $sum: '$points' }
}
}
]);
const pointsSpent = await RewardRequest.aggregate([
{
$match: {
child: { $in: childIds },
status: { $in: ['approved', 'redeemed'] },
requestedAt: { $gte: startDate }
}
},
{
$group: {
_id: null,
totalSpent: { $sum: '$pointsSpent' }
}
}
]);
// Kategorie-Verteilung
const tasksByCategory = await Task.aggregate([
{
$match: {
createdBy: req.user._id,
status: 'approved',
approvedAt: { $gte: startDate },
isActive: true
}
},
{
$group: {
_id: '$category',
count: { $sum: 1 },
points: { $sum: '$points' }
}
},
{ $sort: { count: -1 } }
]);
// Tägliche Aktivität (letzte 7 Tage)
const last7Days = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
const dayTasks = await Task.countDocuments({
createdBy: req.user.id,
status: 'approved',
approvedAt: { $gte: date, $lt: nextDay },
isActive: true
});
last7Days.push({
date: date.toISOString().split('T')[0],
tasks: dayTasks
});
}
// Kind-spezifische Statistiken
const childStats = await Promise.all(
children.map(async (child) => {
const [childTasks, childPoints] = await Promise.all([
Task.countDocuments({
assignedTo: child._id,
status: 'approved',
approvedAt: { $gte: startDate }
}),
Task.aggregate([
{
$match: {
assignedTo: child._id,
status: 'approved',
approvedAt: { $gte: startDate }
}
},
{
$group: {
_id: null,
totalPoints: { $sum: '$points' }
}
}
])
]);
return {
id: child._id,
name: child.name,
avatar: child.avatar,
tasksCompleted: childTasks,
pointsEarned: childPoints[0]?.totalPoints || 0,
currentPoints: child.points.available,
level: child.level,
streak: child.stats.streak.current
};
})
);
const stats = {
period: `${days} Tage`,
overview: {
tasksCreated,
tasksCompleted,
tasksApproved,
completionRate: tasksCreated > 0 ? Math.round((tasksApproved / tasksCreated) * 100) : 0,
pointsEarned: pointsEarned[0]?.totalPoints || 0,
pointsSpent: pointsSpent[0]?.totalSpent || 0
},
categories: tasksByCategory,
dailyActivity: last7Days,
children: childStats,
family: {
totalChildren: children.length,
totalPoints: children.reduce((sum, child) => sum + child.points.total, 0),
averageLevel: children.length > 0
? Math.round(children.reduce((sum, child) => sum + child.level.current, 0) / children.length * 10) / 10
: 0
}
};
res.json({
success: true,
stats
});
} catch (error) {
console.error('Statistik-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Statistiken'
});
}
});
// @route GET /api/families/leaderboard
// @desc Familien-Bestenliste abrufen
// @access Private
router.get('/leaderboard', auth, async (req, res) => {
try {
const { period = 'week' } = req.query; // week, month, all
let startDate;
switch (period) {
case 'week':
startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
break;
case 'month':
startDate = new Date();
startDate.setMonth(startDate.getMonth() - 1);
break;
default:
startDate = new Date(0); // Alle Zeit
}
startDate.setHours(0, 0, 0, 0);
// Alle Kinder der Familie
const children = await Child.find({
parent: req.user.id,
isActive: true
});
// Leaderboard-Daten für jedes Kind
const leaderboard = await Promise.all(
children.map(async (child) => {
const [tasksCompleted, pointsEarned] = await Promise.all([
Task.countDocuments({
assignedTo: child._id,
status: 'approved',
approvedAt: { $gte: startDate }
}),
Task.aggregate([
{
$match: {
assignedTo: child._id,
status: 'approved',
approvedAt: { $gte: startDate }
}
},
{
$group: {
_id: null,
totalPoints: { $sum: '$points' }
}
}
])
]);
return {
id: child._id,
name: child.name,
avatar: child.avatar,
age: child.age,
level: child.level,
tasksCompleted,
pointsEarned: pointsEarned[0]?.totalPoints || 0,
currentStreak: child.stats.streak.current,
longestStreak: child.stats.streak.longest,
totalPoints: child.points.total,
achievements: child.achievements.length
};
})
);
// Nach Punkten sortieren
leaderboard.sort((a, b) => b.pointsEarned - a.pointsEarned);
// Ränge zuweisen
leaderboard.forEach((child, index) => {
child.rank = index + 1;
child.medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : null;
});
res.json({
success: true,
period,
leaderboard
});
} catch (error) {
console.error('Leaderboard-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Bestenliste'
});
}
});
// @route GET /api/families/activity-feed
// @desc Aktivitäts-Feed der Familie abrufen
// @access Private
router.get('/activity-feed', auth, async (req, res) => {
try {
const { limit = 20, offset = 0 } = req.query;
// Alle Kinder der Familie
const children = await Child.find({
parent: req.user.id,
isActive: true
});
const childIds = children.map(child => child._id);
// Aktivitäten sammeln
const activities = [];
// Genehmigte Aufgaben
const approvedTasks = await Task.find({
createdBy: req.user.id,
status: 'approved',
isActive: true
})
.populate('assignedTo', 'name avatar')
.sort({ approvedAt: -1 })
.limit(parseInt(limit))
.skip(parseInt(offset));
approvedTasks.forEach(task => {
activities.push({
id: task._id,
type: 'task_approved',
title: `${task.assignedTo.name} hat "${task.title}" erledigt`,
description: `+${task.points} Punkte erhalten`,
child: task.assignedTo,
points: task.points,
timestamp: task.approvedAt,
icon: '✅'
});
});
// Eingelöste Belohnungen
const redeemedRewards = await RewardRequest.find({
child: { $in: childIds },
status: { $in: ['approved', 'redeemed'] }
})
.populate('reward', 'title cost icon')
.populate('child', 'name avatar')
.sort({ processedAt: -1 })
.limit(parseInt(limit))
.skip(parseInt(offset));
redeemedRewards.forEach(request => {
activities.push({
id: request._id,
type: 'reward_redeemed',
title: `${request.child.name} hat "${request.reward.title}" eingelöst`,
description: `-${request.pointsSpent} Punkte ausgegeben`,
child: request.child,
points: -request.pointsSpent,
timestamp: request.processedAt || request.requestedAt,
icon: '🎁'
});
});
// Level-Ups (aus Achievements)
children.forEach(child => {
child.achievements.forEach(achievement => {
if (achievement.name.includes('Level')) {
activities.push({
id: `${child._id}_${achievement._id}`,
type: 'level_up',
title: `${child.name} hat ein neues Level erreicht!`,
description: achievement.name,
child: {
_id: child._id,
name: child.name,
avatar: child.avatar
},
timestamp: achievement.earnedAt,
icon: '🆙'
});
}
});
});
// Nach Zeitstempel sortieren
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Limit anwenden
const paginatedActivities = activities.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
res.json({
success: true,
count: paginatedActivities.length,
total: activities.length,
activities: paginatedActivities
});
} catch (error) {
console.error('Activity-Feed-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen des Aktivitäts-Feeds'
});
}
});
// @route PUT /api/families/settings
// @desc Familien-Einstellungen aktualisieren
// @access Private
router.put('/settings', [
auth,
body('familyName')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Familienname muss zwischen 2 und 50 Zeichen lang sein')
], async (req, res) => {
try {
// Validierungsfehler prüfen
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: errors.array()
});
}
const { familyName, preferences } = req.body;
const updateFields = {};
if (familyName) updateFields.familyName = familyName.trim();
if (preferences) updateFields.preferences = { ...req.user.preferences, ...preferences };
const user = await User.findByIdAndUpdate(
req.user.id,
{ $set: updateFields },
{ new: true, runValidators: true }
).select('-password');
res.json({
success: true,
message: 'Familien-Einstellungen wurden erfolgreich aktualisiert! ⚙️',
user
});
} catch (error) {
console.error('Einstellungs-Update-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Aktualisieren der Einstellungen'
});
}
});
module.exports = router;