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

581 lines
16 KiB
JavaScript

const express = require('express');
const { body, validationResult } = require('express-validator');
const { Reward, RewardRequest } = require('../models/Reward');
const Child = require('../models/Child');
const { auth } = require('../middleware/auth');
const router = express.Router();
// @route GET /api/rewards
// @desc Alle Belohnungen des angemeldeten Elternteils abrufen
// @access Private
router.get('/', auth, async (req, res) => {
try {
const { category, isActive = 'true' } = req.query;
const filter = { createdBy: req.user.id };
if (category) filter.category = category;
if (isActive !== 'all') filter.isActive = isActive === 'true';
const rewards = await Reward.find(filter)
.sort({ isPopular: -1, sortOrder: 1, cost: 1 });
res.json({
success: true,
count: rewards.length,
rewards
});
} catch (error) {
console.error('Belohnungen-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Belohnungen'
});
}
});
// @route GET /api/rewards/requests/all
// @desc Alle Belohnungsanfragen des angemeldeten Elternteils abrufen
// @access Private
router.get('/requests/all', auth, async (req, res) => {
try {
const { status, childId } = req.query;
// Alle Kinder des Elternteils abrufen
const children = await Child.find({ parent: req.user.id, isActive: true });
const childIds = children.map(child => child._id);
const filter = { child: { $in: childIds } };
if (status) filter.status = status;
if (childId) filter.child = childId;
const requests = await RewardRequest.find(filter)
.populate('child', 'name avatar')
.populate('reward', 'title cost category')
.sort({ createdAt: -1 });
res.json({
success: true,
count: requests.length,
requests
});
} catch (error) {
console.error('Belohnungsanfragen-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Belohnungsanfragen'
});
}
});
// @route GET /api/rewards/shop/:childId
// @desc Verfügbare Belohnungen für ein Kind abrufen (Kinder-Shop)
// @access Private
router.get('/shop/:childId', auth, async (req, res) => {
try {
const childId = req.params.childId;
const { category } = req.query;
// 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'
});
}
// Verfügbare Belohnungen für dieses Kind abrufen
let rewards = await Reward.findAvailableFor(child);
if (category) {
rewards = rewards.filter(reward => reward.category === category);
}
// Verfügbarkeit für jede Belohnung prüfen
const availableRewards = rewards.filter(reward => reward.isAvailableFor(child));
res.json({
success: true,
child: {
id: child._id,
name: child.name,
avatar: child.avatar,
points: child.points
},
count: availableRewards.length,
rewards: availableRewards
});
} catch (error) {
console.error('Shop-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen des Belohnungs-Shops'
});
}
});
// @route POST /api/rewards/requests/:id/approve
// @desc Belohnungs-Anfrage genehmigen
// @access Private
router.post('/requests/:id/approve', auth, async (req, res) => {
try {
const request = await RewardRequest.findOne({
_id: req.params.id,
status: 'pending'
}).populate('child', 'name parent').populate('reward', 'title cost');
if (!request) {
return res.status(404).json({
success: false,
message: 'Anfrage nicht gefunden oder bereits bearbeitet'
});
}
// Prüfen ob das Kind zu diesem Elternteil gehört
if (request.child.parent.toString() !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Keine Berechtigung für diese Anfrage'
});
}
// Anfrage genehmigen
request.status = 'approved';
request.processedAt = new Date();
request.processedBy = req.user.id;
await request.save();
res.json({
success: true,
message: `Belohnungsanfrage für ${request.reward.title} wurde genehmigt`,
request
});
} catch (error) {
console.error('Anfrage-Genehmigung-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Genehmigen der Anfrage'
});
}
});
// @route POST /api/rewards/requests/:id/reject
// @desc Belohnungs-Anfrage ablehnen
// @access Private
router.post('/requests/: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 {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: errors.array()
});
}
const { reason } = req.body;
const request = await RewardRequest.findOne({
_id: req.params.id,
status: 'pending'
}).populate('child', 'name parent').populate('reward', 'title cost');
if (!request) {
return res.status(404).json({
success: false,
message: 'Anfrage nicht gefunden oder bereits bearbeitet'
});
}
// Prüfen ob das Kind zu diesem Elternteil gehört
if (request.child.parent.toString() !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Keine Berechtigung für diese Anfrage'
});
}
// Anfrage ablehnen
request.status = 'rejected';
request.rejectionReason = reason;
request.processedAt = new Date();
request.processedBy = req.user.id;
await request.save();
res.json({
success: true,
message: `Belohnungsanfrage für ${request.reward.title} wurde abgelehnt`,
request
});
} catch (error) {
console.error('Anfrage-Ablehnung-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Ablehnen der Anfrage'
});
}
});
// @route POST /api/rewards/requests/:id/redeem
// @desc Genehmigte Belohnungsanfrage einlösen
// @access Private
router.post('/requests/:id/redeem', auth, async (req, res) => {
try {
const request = await RewardRequest.findOne({
_id: req.params.id,
status: 'approved'
}).populate('child', 'name parent points').populate('reward', 'title cost');
if (!request) {
return res.status(404).json({
success: false,
message: 'Genehmigte Anfrage nicht gefunden'
});
}
// Prüfen ob das Kind zu diesem Elternteil gehört
if (request.child.parent.toString() !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Keine Berechtigung für diese Anfrage'
});
}
// Anfrage als eingelöst markieren
request.status = 'redeemed';
request.redeemedAt = new Date();
await request.save();
res.json({
success: true,
message: `Belohnung ${request.reward.title} wurde eingelöst`,
request
});
} catch (error) {
console.error('Anfrage-Einlösung-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Einlösen der Anfrage'
});
}
});
// @route GET /api/rewards/:id
// @desc Einzelne Belohnung abrufen
// @access Private
router.get('/:id', auth, async (req, res) => {
try {
const reward = await Reward.findOne({
_id: req.params.id,
createdBy: req.user.id
});
if (!reward) {
return res.status(404).json({
success: false,
message: 'Belohnung nicht gefunden'
});
}
res.json({
success: true,
reward
});
} catch (error) {
console.error('Belohnung-Abruf-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Abrufen der Belohnung'
});
}
});
// @route POST /api/rewards
// @desc Neue Belohnung 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('cost')
.isInt({ min: 1, max: 10000 })
.withMessage('Kosten müssen zwischen 1 und 10000 Punkten liegen'),
body('category')
.optional()
.isIn([
'screen-time', 'treats', 'toys', 'activities', 'outings',
'privileges', 'money', 'special', 'electronics', 'books',
'sports', 'art', 'other'
])
.withMessage('Ungültige Kategorie')
], 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, cost, category, icon, availableFor,
ageRestriction, availability, timeRestrictions,
requiresApproval, isInstant, tags, instructions
} = req.body;
// Neue Belohnung erstellen
const reward = new Reward({
title: title.trim(),
description: description?.trim(),
cost,
category: category || 'treats',
icon: icon || 'gift',
createdBy: req.user.id,
availableFor: availableFor || [],
ageRestriction: ageRestriction || {},
availability: availability || { isLimited: false },
timeRestrictions: timeRestrictions || {},
requiresApproval: requiresApproval !== false, // Standard: true
isInstant: isInstant || false,
tags: tags || [],
instructions: instructions?.trim()
});
await reward.save();
res.status(201).json({
success: true,
message: `Neue Belohnung "${title}" wurde erstellt! 🎁`,
reward
});
} catch (error) {
console.error('Belohnung-Erstellungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Erstellen der Belohnung'
});
}
});
// @route PUT /api/rewards/:id
// @desc Belohnung 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('cost')
.optional()
.isInt({ min: 1, max: 10000 })
.withMessage('Kosten müssen zwischen 1 und 10000 Punkten 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', 'cost', 'category', 'icon',
'availableFor', 'ageRestriction', 'availability',
'timeRestrictions', 'requiresApproval', 'isInstant',
'tags', 'instructions', 'isActive', 'isPopular', 'sortOrder'
];
// Nur erlaubte Felder aktualisieren
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateFields[field] = req.body[field];
}
});
const reward = await Reward.findOneAndUpdate(
{ _id: req.params.id, createdBy: req.user.id },
{ $set: updateFields },
{ new: true, runValidators: true }
);
if (!reward) {
return res.status(404).json({
success: false,
message: 'Belohnung nicht gefunden'
});
}
res.json({
success: true,
message: `Belohnung "${reward.title}" wurde erfolgreich aktualisiert! ✅`,
reward
});
} catch (error) {
console.error('Belohnung-Update-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Aktualisieren der Belohnung'
});
}
});
// @route POST /api/rewards/:id/redeem
// @desc Belohnung einlösen (von Kind)
// @access Private
router.post('/:id/redeem', [
auth,
body('childId')
.isMongoId()
.withMessage('Ungültige Kind-ID')
], 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 { childId } = req.body;
const rewardId = req.params.id;
// 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'
});
}
// Belohnung abrufen
const reward = await Reward.findOne({
_id: rewardId,
createdBy: req.user.id,
isActive: true
});
if (!reward) {
return res.status(404).json({
success: false,
message: 'Belohnung nicht gefunden'
});
}
// Verfügbarkeit prüfen
if (!reward.isAvailableFor(child)) {
return res.status(400).json({
success: false,
message: 'Diese Belohnung ist für dieses Kind nicht verfügbar'
});
}
// Punkte prüfen
if (child.points.available < reward.cost) {
return res.status(400).json({
success: false,
message: `Nicht genügend Punkte! Du brauchst ${reward.cost} Punkte, hast aber nur ${child.points.available}.`
});
}
// Belohnung einlösen
const request = await reward.redeem(childId);
const message = reward.requiresApproval
? `Super! ${child.name} möchte "${reward.title}" einlösen. Warte auf die Bestätigung! 🎁`
: `Glückwunsch! ${child.name} hat "${reward.title}" erfolgreich eingelöst! 🎉`;
res.json({
success: true,
message,
request: {
id: request._id,
status: request.status,
pointsSpent: request.pointsSpent,
requestedAt: request.requestedAt
},
child: {
id: child._id,
name: child.name,
points: {
available: child.points.available - reward.cost,
total: child.points.total,
spent: child.points.spent + reward.cost
}
}
});
} catch (error) {
console.error('Belohnung-Einlösungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: error.message || 'Server-Fehler beim Einlösen der Belohnung'
});
}
});
// @route DELETE /api/rewards/:id
// @desc Belohnung löschen (soft delete)
// @access Private
router.delete('/:id', auth, async (req, res) => {
try {
const reward = await Reward.findOneAndUpdate(
{ _id: req.params.id, createdBy: req.user.id },
{ $set: { isActive: false } },
{ new: true }
);
if (!reward) {
return res.status(404).json({
success: false,
message: 'Belohnung nicht gefunden'
});
}
res.json({
success: true,
message: `Belohnung "${reward.title}" wurde gelöscht`
});
} catch (error) {
console.error('Belohnung-Löschungs-Fehler:', error.message);
res.status(500).json({
success: false,
message: 'Server-Fehler beim Löschen der Belohnung'
});
}
});
module.exports = router;