todo-helden/models/Child.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

275 lines
5.8 KiB
JavaScript

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Schema für Kinder-Profile
const childSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
maxlength: 50
},
age: {
type: Number,
required: true,
min: 3,
max: 18
},
avatar: {
type: String,
default: 'default-child.png',
enum: [
'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'
]
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
// Authentifizierung für Kinder
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 20,
match: /^[a-zA-Z0-9_]+$/
},
pin: {
type: String,
required: true,
minlength: 4,
maxlength: 6,
match: /^[0-9]+$/
},
// Für sichere PIN-Speicherung (gehashed)
hashedPin: {
type: String,
required: true
},
points: {
total: {
type: Number,
default: 0,
min: 0
},
available: {
type: Number,
default: 0,
min: 0
},
spent: {
type: Number,
default: 0,
min: 0
}
},
level: {
current: {
type: Number,
default: 1,
min: 1
},
title: {
type: String,
default: 'Nachwuchs-Held'
}
},
achievements: [{
name: String,
description: String,
icon: String,
earnedAt: {
type: Date,
default: Date.now
}
}],
preferences: {
theme: {
type: String,
enum: ['colorful', 'nature', 'space', 'ocean', 'forest'],
default: 'colorful'
},
favoriteColor: {
type: String,
default: '#4CAF50'
},
soundEffects: {
type: Boolean,
default: true
},
animations: {
type: Boolean,
default: true
}
},
stats: {
tasksCompleted: {
type: Number,
default: 0
},
tasksCompletedToday: {
type: Number,
default: 0
},
streak: {
current: {
type: Number,
default: 0
},
longest: {
type: Number,
default: 0
},
lastCompletionDate: Date
}
},
isActive: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now
},
lastActivity: {
type: Date,
default: Date.now
}
});
// Indizes für bessere Performance
childSchema.index({ parent: 1, isActive: 1 });
childSchema.index({ 'points.total': -1 });
childSchema.index({ username: 1 }, { unique: true });
// Methode zum Punkte hinzufügen
childSchema.methods.addPoints = function(points) {
this.points.total += points;
this.points.available += points;
this.lastActivity = new Date();
// Level-System aktualisieren
this.updateLevel();
return this.save();
};
// PIN-Hashing vor dem Speichern
childSchema.pre('save', async function(next) {
// Nur hashen wenn PIN geändert wurde
if (!this.isModified('pin')) return next();
try {
// PIN hashen
const salt = await bcrypt.genSalt(10);
this.hashedPin = await bcrypt.hash(this.pin, salt);
// Klartext-PIN entfernen (nicht in DB speichern)
this.pin = undefined;
next();
} catch (error) {
next(error);
}
});
// PIN-Verifikation
childSchema.methods.verifyPin = async function(pin) {
return await bcrypt.compare(pin, this.hashedPin);
};
// Sichere Daten für Frontend (ohne hashedPin)
childSchema.methods.toSafeObject = function() {
const obj = this.toObject();
delete obj.hashedPin;
delete obj.pin;
return obj;
};
// Methode zum Punkte ausgeben
childSchema.methods.spendPoints = function(points) {
if (this.points.available >= points) {
this.points.available -= points;
this.points.spent += points;
this.lastActivity = new Date();
return this.save();
}
throw new Error('Nicht genügend Punkte verfügbar');
};
// Level-System aktualisieren
childSchema.methods.updateLevel = function() {
const totalPoints = this.points.total;
const newLevel = Math.floor(totalPoints / 100) + 1; // Alle 100 Punkte ein Level
if (newLevel > this.level.current) {
this.level.current = newLevel;
this.level.title = this.getLevelTitle(newLevel);
}
};
// Level-Titel bestimmen
childSchema.methods.getLevelTitle = function(level) {
const titles = {
1: 'Nachwuchs-Held',
2: 'Aufgaben-Entdecker',
3: 'Fleißiger Helfer',
4: 'Verantwortungs-Champion',
5: 'Familien-Superheld',
6: 'Aufgaben-Meister',
7: 'Pflichten-Profi',
8: 'Haushalts-Held',
9: 'Verantwortungs-Legende',
10: 'Familien-Champion'
};
return titles[level] || `Super-Held Level ${level}`;
};
// Streak aktualisieren
childSchema.methods.updateStreak = function() {
const today = new Date();
today.setHours(0, 0, 0, 0);
const lastCompletion = this.stats.streak.lastCompletionDate;
if (!lastCompletion) {
// Erste Aufgabe
this.stats.streak.current = 1;
this.stats.streak.lastCompletionDate = today;
} else {
const lastDate = new Date(lastCompletion);
lastDate.setHours(0, 0, 0, 0);
const daysDiff = (today - lastDate) / (1000 * 60 * 60 * 24);
if (daysDiff === 1) {
// Aufeinanderfolgende Tage
this.stats.streak.current += 1;
this.stats.streak.lastCompletionDate = today;
} else if (daysDiff > 1) {
// Streak unterbrochen
this.stats.streak.current = 1;
this.stats.streak.lastCompletionDate = today;
}
// daysDiff === 0 bedeutet heute schon eine Aufgabe erledigt
}
// Längste Streak aktualisieren
if (this.stats.streak.current > this.stats.streak.longest) {
this.stats.streak.longest = this.stats.streak.current;
}
};
module.exports = mongoose.model('Child', childSchema);