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
580 lines
16 KiB
JavaScript
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; |