todo-helden/routes/children.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

477 lines
12 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const Child = require('../models/Child');
const { auth } = require('../middleware/auth');
const router = express.Router();
// @route GET /api/children
// @desc Alle Kinder des angemeldeten Elternteils abrufen
// @access Private
router.get('/', auth, async (req, res) => {
try {
const children = await Child.find({
parent: req.user.id,
isActive: true
}).sort({ createdAt: 1 });
// Sichere Daten für Frontend (ohne hashedPin)
const safeChildren = children.map(child => child.toSafeObject());
res.json({
success: true,
count: safeChildren.length,
children: safeChildren
});
} catch (error) {
console.error('Kinder-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Kinder'
});
}
});
// @route GET /api/children/:id
// @desc Einzelnes Kind abrufen
// @access Private
router.get('/:id', auth, async (req, res) => {
try {
const child = await Child.findOne({
_id: req.params.id,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
res.json({
success: true,
child: child.toSafeObject()
});
} catch (error) {
console.error('Kind-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen des Kindes'
});
}
});
// @route POST /api/children
// @desc Neues Kind erstellen
// @access Private
router.post('/', [
auth,
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Name muss zwischen 2 und 50 Zeichen lang sein'),
body('age')
.isInt({ min: 3, max: 18 })
.withMessage('Alter muss zwischen 3 und 18 Jahren liegen'),
body('username')
.trim()
.isLength({ min: 3, max: 20 })
.withMessage('Benutzername muss zwischen 3 und 20 Zeichen lang sein')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten'),
body('pin')
.isLength({ min: 4, max: 6 })
.withMessage('PIN muss zwischen 4 und 6 Zeichen lang sein')
.matches(/^[0-9]+$/)
.withMessage('PIN darf nur Zahlen enthalten'),
body('avatar')
.optional()
.isIn([
'default-child.png', 'superhero-boy.png', 'superhero-girl.png',
'pirate-boy.png', 'pirate-girl.png', 'princess.png', 'knight.png',
'astronaut.png', 'detective.png', 'artist.png'
])
.withMessage('Ungültiger Avatar ausgewählt')
], 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 { name, age, username, pin, avatar, preferences } = req.body;
// Prüfen ob bereits ein Kind mit diesem Namen existiert
const existingChild = await Child.findOne({
parent: req.user.id,
name: name.trim(),
isActive: true
});
if (existingChild) {
return res.status(400).json({
success: false,
message: 'Ein Kind mit diesem Namen existiert bereits'
});
}
// Prüfen ob Benutzername bereits existiert
const existingUsername = await Child.findOne({
username: username.toLowerCase(),
isActive: true
});
if (existingUsername) {
return res.status(400).json({
success: false,
message: 'Dieser Benutzername ist bereits vergeben'
});
}
// Neues Kind erstellen
const child = new Child({
name: name.trim(),
age,
username: username.toLowerCase(),
pin, // Wird automatisch gehashed durch pre-save Hook
avatar: avatar || 'default-child.png',
parent: req.user.id,
preferences: preferences || {}
});
await child.save();
res.status(201).json({
success: true,
message: `${name} wurde erfolgreich als neuer Familien-Held hinzugefügt! 🎉\nBenutzername: ${username}\nPIN: Sicher gespeichert`,
child: child.toSafeObject()
});
} catch (error) {
console.error('Kind-Erstellungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Erstellen des Kindes'
});
}
});
// @route PUT /api/children/:id
// @desc Kind aktualisieren
// @access Private
router.put('/:id', [
auth,
body('name')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Name muss zwischen 2 und 50 Zeichen lang sein'),
body('age')
.optional()
.isInt({ min: 3, max: 18 })
.withMessage('Alter muss zwischen 3 und 18 Jahren liegen'),
body('avatar')
.optional()
.isIn([
'default-child.png', 'superhero-boy.png', 'superhero-girl.png',
'pirate-boy.png', 'pirate-girl.png', 'princess.png', 'knight.png',
'astronaut.png', 'detective.png', 'artist.png'
])
.withMessage('Ungültiger Avatar ausgewählt')
], 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 { name, age, avatar, preferences } = req.body;
const updateFields = {};
if (name) updateFields.name = name.trim();
if (age) updateFields.age = age;
if (avatar) updateFields.avatar = avatar;
if (preferences) updateFields.preferences = { ...preferences };
const child = await Child.findOneAndUpdate(
{ _id: req.params.id, parent: req.user.id, isActive: true },
{ $set: updateFields },
{ new: true, runValidators: true }
);
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
res.json({
success: true,
message: `${child.name}s Profil wurde erfolgreich aktualisiert! ✅`,
child
});
} catch (error) {
console.error('Kind-Update-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Aktualisieren des Kindes'
});
}
});
// @route DELETE /api/children/:id
// @desc Kind deaktivieren (soft delete)
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
const child = await Child.findOneAndUpdate(
{ _id: req.params.id, parent: req.user.id, isActive: true },
{ $set: { isActive: false } },
{ new: true }
);
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
res.json({
success: true,
message: `${child.name} wurde aus der Familie entfernt`
});
} catch (error) {
console.error('Kind-Löschungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Entfernen des Kindes'
});
}
});
// @route GET /api/children/:id/stats
// @desc Statistiken für ein Kind abrufen
// @access Private
router.get('/:id/stats', auth, async (req, res) => {
try {
const child = await Child.findOne({
_id: req.params.id,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
// Zusätzliche Statistiken berechnen
const Task = require('../models/Task');
const { RewardRequest } = require('../models/Reward');
const [completedTasks, pendingTasks, rewardRequests] = await Promise.all([
Task.countDocuments({ assignedTo: child._id, status: 'approved' }),
Task.countDocuments({ assignedTo: child._id, status: 'pending' }),
RewardRequest.countDocuments({ child: child._id })
]);
const stats = {
points: child.points,
level: child.level,
achievements: child.achievements,
tasks: {
completed: completedTasks,
pending: pendingTasks,
completedToday: child.stats.tasksCompletedToday
},
streak: child.stats.streak,
rewards: {
totalRequests: rewardRequests
},
memberSince: child.createdAt,
lastActivity: child.lastActivity
};
res.json({
success: true,
stats
});
} catch (error) {
console.error('Kind-Statistik-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Statistiken'
});
}
});
// @route POST /api/children/:id/points
// @desc Punkte manuell hinzufügen/abziehen (für Eltern)
// @access Private
router.post('/:id/points', [
auth,
body('points')
.isInt({ min: -1000, max: 1000 })
.withMessage('Punkte müssen zwischen -1000 und 1000 liegen'),
body('reason')
.trim()
.isLength({ min: 3, max: 200 })
.withMessage('Grund 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 { points, reason } = req.body;
const child = await Child.findOne({
_id: req.params.id,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
// Punkte hinzufügen oder abziehen
if (points > 0) {
await child.addPoints(points);
} else {
// Negative Punkte (Abzug)
const absPoints = Math.abs(points);
if (child.points.available < absPoints) {
return res.status(400).json({
success: false,
message: 'Nicht genügend Punkte zum Abziehen verfügbar'
});
}
child.points.available -= absPoints;
child.points.total -= absPoints;
await child.save();
}
const action = points > 0 ? 'hinzugefügt' : 'abgezogen';
const emoji = points > 0 ? '🎉' : '⚠️';
res.json({
success: true,
message: `${Math.abs(points)} Punkte wurden ${child.name} ${action}! ${emoji}`,
reason,
child: {
id: child._id,
name: child.name,
points: child.points,
level: child.level
}
});
} catch (error) {
console.error('Punkte-Änderungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Ändern der Punkte'
});
}
});
// @route POST /api/children/:id/achievements
// @desc Achievement hinzufügen
// @access Private
router.post('/:id/achievements', [
auth,
body('name')
.trim()
.isLength({ min: 3, max: 50 })
.withMessage('Achievement-Name muss zwischen 3 und 50 Zeichen lang sein'),
body('description')
.trim()
.isLength({ min: 5, max: 200 })
.withMessage('Beschreibung muss zwischen 5 und 200 Zeichen lang sein'),
body('icon')
.optional()
.trim()
.isLength({ min: 1, max: 20 })
.withMessage('Icon muss zwischen 1 und 20 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 { name, description, icon } = req.body;
const child = await Child.findOne({
_id: req.params.id,
parent: req.user.id,
isActive: true
});
if (!child) {
return res.status(404).json({
success: false,
message: 'Kind nicht gefunden'
});
}
// Achievement hinzufügen
const achievement = {
name: name.trim(),
description: description.trim(),
icon: icon || '🏆',
earnedAt: new Date()
};
child.achievements.push(achievement);
await child.save();
res.status(201).json({
success: true,
message: `${child.name} hat ein neues Achievement erhalten: ${name}! 🏆`,
achievement,
child: {
id: child._id,
name: child.name,
achievements: child.achievements
}
});
} catch (error) {
console.error('Achievement-Hinzufügungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Hinzufügen des Achievements'
});
}
});
module.exports = router;