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

561 lines
15 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const Task = require('../models/Task');
const Child = require('../models/Child');
const { auth } = require('../middleware/auth');
const router = express.Router();
// @route GET /api/tasks
// @desc Alle Aufgaben des angemeldeten Elternteils abrufen
// @access Private
router.get('/', auth, async (req, res) => {
try {
const { status, childId, date, category } = req.query;
// Filter aufbauen
const filter = { createdBy: req.user.id, isActive: true };
if (status) filter.status = status;
if (childId) filter.assignedTo = childId;
if (category) filter.category = category;
// Datum-Filter
if (date) {
const targetDate = new Date(date);
targetDate.setHours(0, 0, 0, 0);
const nextDay = new Date(targetDate);
nextDay.setDate(nextDay.getDate() + 1);
filter.dueDate = { $gte: targetDate, $lt: nextDay };
}
const tasks = await Task.find(filter)
.populate('assignedTo', 'name avatar age')
.populate('approvedBy', 'parentName')
.sort({ dueDate: 1, priority: -1, createdAt: -1 });
res.json({
success: true,
count: tasks.length,
tasks
});
} catch (error) {
console.error('Aufgaben-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Aufgaben'
});
}
});
// @route GET /api/tasks/child/:childId
// @desc Aufgaben für ein bestimmtes Kind abrufen (Kinder-Ansicht)
// @access Private
router.get('/child/:childId', auth, async (req, res) => {
try {
const { status = 'pending', date } = req.query;
const childId = req.params.childId;
// Prüfen ob Kind zu diesem Elternteil gehört
const child = await Child.findOne({
_id: childId,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
let tasks;
if (date === 'today') {
// Heutige Aufgaben
tasks = await Task.findTodayTasks(childId);
} else {
// Alle Aufgaben mit Filter
const filter = {
assignedTo: childId,
isActive: true
};
if (status !== 'all') {
filter.status = status;
}
tasks = await Task.find(filter)
.sort({ dueDate: 1, priority: -1, createdAt: -1 });
}
res.json({
success: true,
child: {
id: child._id,
name: child.name,
avatar: child.avatar,
points: child.points
},
count: tasks.length,
tasks
});
} catch (error) {
console.error('Kind-Aufgaben-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Aufgaben'
});
}
});
// @route GET /api/tasks/:id
// @desc Einzelne Aufgabe abrufen
// @access Private
router.get('/:id', auth, async (req, res) => {
try {
const task = await Task.findOne({
_id: req.params.id,
createdBy: req.user.id,
isActive: true
})
.populate('assignedTo', 'name avatar age')
.populate('approvedBy', 'parentName');
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden'
});
}
res.json({
success: true,
task
});
} catch (error) {
console.error('Aufgabe-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Aufgabe'
});
}
});
// @route POST /api/tasks
// @desc Neue Aufgabe erstellen
// @access Private
router.post('/', [
auth,
body('title')
.trim()
.isLength({ min: 3, max: 100 })
.withMessage('Titel muss zwischen 3 und 100 Zeichen lang sein'),
body('assignedTo')
.isMongoId()
.withMessage('Ungültige Kind-ID'),
body('dueDate')
.isISO8601()
.withMessage('Ungültiges Fälligkeitsdatum'),
body('points')
.isInt({ min: 1, max: 500 })
.withMessage('Punkte müssen zwischen 1 und 500 liegen'),
body('category')
.optional()
.isIn(['household', 'personal', 'homework', 'chores', 'hygiene', 'pets', 'garden', 'helping', 'learning', 'exercise', 'other'])
.withMessage('Ungültige Kategorie'),
body('difficulty')
.optional()
.isIn(['easy', 'medium', 'hard'])
.withMessage('Ungültige Schwierigkeit'),
body('priority')
.optional()
.isIn(['low', 'normal', 'high', 'urgent'])
.withMessage('Ungültige Priorität')
], 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 {
title, description, assignedTo, dueDate, points,
category, difficulty, priority, icon, estimatedTime,
recurring, tags, notes
} = req.body;
// Prüfen ob Kind zu diesem Elternteil gehört
const child = await Child.findOne({
_id: assignedTo,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden oder gehört nicht zu diesem Account'
});
}
// Neue Aufgabe erstellen
const task = new Task({
title: title.trim(),
description: description?.trim(),
assignedTo,
createdBy: req.user.id,
dueDate: new Date(dueDate),
points,
category: category || 'household',
difficulty: difficulty || 'easy',
priority: priority || 'normal',
icon: icon || 'task',
estimatedTime,
recurring: recurring || { isRecurring: false },
tags: tags || [],
notes: notes?.trim()
});
await task.save();
// Aufgabe mit Kind-Daten laden
await task.populate('assignedTo', 'name avatar age');
res.status(201).json({
success: true,
message: `Neue Aufgabe "${title}" wurde für ${child.name} erstellt! 📝`,
task
});
} catch (error) {
console.error('Aufgabe-Erstellungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Erstellen der Aufgabe'
});
}
});
// @route PUT /api/tasks/:id
// @desc Aufgabe aktualisieren
// @access Private
router.put('/:id', [
auth,
body('title')
.optional()
.trim()
.isLength({ min: 3, max: 100 })
.withMessage('Titel muss zwischen 3 und 100 Zeichen lang sein'),
body('points')
.optional()
.isInt({ min: 1, max: 500 })
.withMessage('Punkte müssen zwischen 1 und 500 liegen')
], 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 updateFields = {};
const allowedFields = [
'title', 'description', 'dueDate', 'points', 'category',
'difficulty', 'priority', 'icon', 'estimatedTime', 'tags', 'notes'
];
// Nur erlaubte Felder aktualisieren
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateFields[field] = req.body[field];
}
});
const task = await Task.findOneAndUpdate(
{
_id: req.params.id,
createdBy: req.user.id,
isActive: true,
status: { $in: ['pending', 'completed'] } // Nur nicht-genehmigte Aufgaben änderbar
},
{ $set: updateFields },
{ new: true, runValidators: true }
).populate('assignedTo', 'name avatar age');
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden oder kann nicht bearbeitet werden'
});
}
res.json({
success: true,
message: `Aufgabe "${task.title}" wurde erfolgreich aktualisiert! ✅`,
task
});
} catch (error) {
console.error('Aufgabe-Update-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Aktualisieren der Aufgabe'
});
}
});
// @route POST /api/tasks/:id/complete
// @desc Aufgabe als erledigt markieren (von Kind)
// @access Private
router.post('/:id/complete', auth, async (req, res) => {
try {
const task = await Task.findOne({
_id: req.params.id,
isActive: true,
status: 'pending'
}).populate('assignedTo', 'name avatar');
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden oder bereits erledigt'
});
}
// Prüfen ob Kind zu diesem Elternteil gehört
const child = await Child.findOne({
_id: task.assignedTo._id,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(403).json({
success: false,
message: 'Keine Berechtigung für diese Aufgabe'
});
}
await task.markCompleted();
res.json({
success: true,
message: `Super! "${task.title}" wurde als erledigt markiert! 🎉 Warte auf die Bestätigung deiner Eltern.`,
task: {
id: task._id,
title: task.title,
status: task.status,
points: task.points,
completedAt: task.completedAt
}
});
} catch (error) {
console.error('Aufgabe-Abschluss-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Markieren der Aufgabe als erledigt'
});
}
});
// @route POST /api/tasks/:id/approve
// @desc Aufgabe genehmigen (von Eltern)
// @access Private
router.post('/:id/approve', auth, async (req, res) => {
try {
const task = await Task.findOne({
_id: req.params.id,
createdBy: req.user.id,
isActive: true,
status: 'completed'
}).populate('assignedTo', 'name avatar');
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden oder nicht zur Genehmigung bereit'
});
}
await task.approve(req.user.id);
res.json({
success: true,
message: `Aufgabe "${task.title}" wurde genehmigt! ${task.assignedTo.name} erhält ${task.points} Punkte! 🌟`,
task: {
id: task._id,
title: task.title,
status: task.status,
points: task.points,
approvedAt: task.approvedAt,
child: task.assignedTo
}
});
} catch (error) {
console.error('Aufgabe-Genehmigungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Genehmigen der Aufgabe'
});
}
});
// @route POST /api/tasks/:id/reject
// @desc Aufgabe ablehnen (von Eltern)
// @access Private
router.post('/:id/reject', [
auth,
body('reason')
.trim()
.isLength({ min: 3, max: 200 })
.withMessage('Ablehnungsgrund muss zwischen 3 und 200 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 { reason } = req.body;
const task = await Task.findOne({
_id: req.params.id,
createdBy: req.user.id,
isActive: true,
status: 'completed'
}).populate('assignedTo', 'name avatar');
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden oder nicht zur Ablehnung bereit'
});
}
await task.reject(reason.trim(), req.user.id);
res.json({
success: true,
message: `Aufgabe "${task.title}" wurde abgelehnt. ${task.assignedTo.name} kann es nochmal versuchen.`,
task: {
id: task._id,
title: task.title,
status: task.status,
rejectionReason: task.rejectionReason,
child: task.assignedTo
}
});
} catch (error) {
console.error('Aufgabe-Ablehnungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Ablehnen der Aufgabe'
});
}
});
// @route DELETE /api/tasks/:id
// @desc Aufgabe löschen (soft delete)
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
const task = await Task.findOneAndUpdate(
{
_id: req.params.id,
createdBy: req.user.id,
isActive: true,
status: { $in: ['pending', 'rejected'] } // Nur nicht-erledigte Aufgaben löschbar
},
{ $set: { isActive: false } },
{ new: true }
);
if (!task) {
return res.status(404).json({
success: false,
message: 'Aufgabe nicht gefunden oder kann nicht gelöscht werden'
});
}
res.json({
success: true,
message: `Aufgabe "${task.title}" wurde gelöscht`
});
} catch (error) {
console.error('Aufgabe-Löschungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Löschen der Aufgabe'
});
}
});
// @route GET /api/tasks/stats/overview
// @desc Aufgaben-Statistiken für Dashboard
// @access Private
router.get('/stats/overview', auth, async (req, res) => {
try {
const [totalTasks, pendingTasks, completedTasks, approvedTasks, overdueTasks] = 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 }),
Task.countDocuments({
createdBy: req.user.id,
status: 'pending',
dueDate: { $lt: new Date() },
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.countDocuments({
createdBy: req.user.id,
dueDate: { $gte: today, $lt: tomorrow },
isActive: true
});
const stats = {
total: totalTasks,
pending: pendingTasks,
completed: completedTasks,
approved: approvedTasks,
overdue: overdueTasks,
today: todayTasks,
completionRate: totalTasks > 0 ? Math.round((approvedTasks / totalTasks) * 100) : 0
};
res.json({
success: true,
stats
});
} catch (error) {
console.error('Aufgaben-Statistik-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Statistiken'
});
}
});
module.exports = router;