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
561 lines
15 KiB
JavaScript
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; |