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