diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php new file mode 100644 index 0000000..0a1547e --- /dev/null +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -0,0 +1,20 @@ +count(); + $questions = \App\Models\Question::count(); + $attempts_today = QuestionAttempt::whereDate('created_at', today())->count(); + $pending = RewardRedemption::where('status','pending')->count(); + $topKids = User::where('role','child')->orderByDesc('points')->take(5)->get(); + $recentAttempts = QuestionAttempt::with(['user','question.subject']) + ->latest('id')->take(10)->get(); + return view('admin.dashboard', compact( + 'children','questions','attempts_today','pending','topKids','recentAttempts' + )); + } +} diff --git a/app/Http/Controllers/Admin/QuestionController.php b/app/Http/Controllers/Admin/QuestionController.php new file mode 100644 index 0000000..dc6421f --- /dev/null +++ b/app/Http/Controllers/Admin/QuestionController.php @@ -0,0 +1,100 @@ +latest(); + if ($r->filled('subject')) $query->where('subject_id', $r->subject); + $questions = $query->paginate(20)->withQueryString(); + return view('admin.questions.index', compact('questions','subjects')); + } + public function create() { + $subjects = Subject::all(); + return view('admin.questions.create', compact('subjects')); + } + public function store(Request $r) { + $r->validate([ + 'subject_id' => 'required|exists:subjects,id', + 'question_text' => 'required|string', + 'type' => 'required|in:multiple_choice,number_input', + 'difficulty' => 'required|in:1,2,3', + 'options' => 'required_if:type,multiple_choice|array|min:2', + 'options.*' => 'required|string', + 'correct' => 'required_if:type,multiple_choice|integer', + 'number_answer' => 'required_if:type,number_input', + ]); + $pts = [1=>5, 2=>10, 3=>20][$r->difficulty]; + $q = Question::create([ + 'subject_id' => $r->subject_id, + 'question_text' => $r->question_text, + 'type' => $r->type, + 'difficulty' => $r->difficulty, + 'points_value' => $pts, + 'active' => true, + ]); + if ($r->type === 'multiple_choice') { + foreach ($r->options as $i => $text) { + if (trim($text) === '') continue; + AnswerOption::create([ + 'question_id' => $q->id, + 'text' => $text, + 'is_correct' => ($i == $r->correct), + 'sort_order' => $i, + ]); + } + } else { + AnswerOption::create([ + 'question_id' => $q->id, + 'text' => $r->number_answer, + 'is_correct' => true, + 'sort_order' => 0, + ]); + } + return redirect()->route('admin.questions.index')->with('success','Frage gespeichert.'); + } + public function edit(Question $question) { + $subjects = Subject::all(); + $question->load('answerOptions'); + return view('admin.questions.edit', compact('question','subjects')); + } + public function update(Request $r, Question $question) { + $r->validate([ + 'subject_id' => 'required|exists:subjects,id', + 'question_text' => 'required|string', + 'difficulty' => 'required|in:1,2,3', + 'options' => 'array', + 'options.*' => 'string', + 'correct' => 'integer', + ]); + $pts = [1=>5, 2=>10, 3=>20][$r->difficulty]; + $question->update([ + 'subject_id' => $r->subject_id, + 'question_text' => $r->question_text, + 'difficulty' => $r->difficulty, + 'points_value' => $pts, + 'active' => $r->boolean('active', true), + ]); + if ($r->filled('options')) { + $question->answerOptions()->delete(); + foreach ($r->options as $i => $text) { + if (trim($text) === '') continue; + AnswerOption::create([ + 'question_id' => $question->id, + 'text' => $text, + 'is_correct' => ($i == $r->correct), + 'sort_order' => $i, + ]); + } + } + return redirect()->route('admin.questions.index')->with('success','Gespeichert.'); + } + public function destroy(Question $question) { + $question->delete(); + return redirect()->route('admin.questions.index')->with('success','Frage gelöscht.'); + } +} diff --git a/app/Http/Controllers/Admin/RedemptionController.php b/app/Http/Controllers/Admin/RedemptionController.php new file mode 100644 index 0000000..9bc489c --- /dev/null +++ b/app/Http/Controllers/Admin/RedemptionController.php @@ -0,0 +1,33 @@ +where('status','pending')->latest()->get(); + $resolved = RewardRedemption::with(['user','reward','resolver'])->whereIn('status',['approved','rejected'])->latest()->take(50)->get(); + return view('admin.redemptions.index', compact('pending','resolved')); + } + public function approve(RewardRedemption $redemption) { + if ($redemption->status !== 'pending') abort(422); + $redemption->update([ + 'status' => 'approved', + 'resolved_at' => now(), + 'resolved_by' => auth()->id(), + ]); + return back()->with('success','Freigegeben!'); + } + public function reject(Request $r, RewardRedemption $redemption) { + if ($redemption->status !== 'pending') abort(422); + $redemption->update([ + 'status' => 'rejected', + 'note' => $r->note, + 'resolved_at' => now(), + 'resolved_by' => auth()->id(), + ]); + // Punkte zurückgeben + $redemption->user->increment('points', $redemption->points_spent); + return back()->with('success','Abgelehnt, Punkte zurückgebucht.'); + } +} diff --git a/app/Http/Controllers/Admin/RewardController.php b/app/Http/Controllers/Admin/RewardController.php new file mode 100644 index 0000000..d05baeb --- /dev/null +++ b/app/Http/Controllers/Admin/RewardController.php @@ -0,0 +1,42 @@ +get(); + return view('admin.rewards.index', compact('rewards')); + } + public function create() { return view('admin.rewards.create'); } + public function store(Request $r) { + $r->validate([ + 'name' => 'required|string|max:80', + 'description' => 'nullable|string|max:200', + 'icon' => 'required|string|max:10', + 'points_cost' => 'required|integer|min:1', + 'minutes' => 'nullable|integer|min:1', + ]); + Reward::create(array_merge($r->only('name','description','icon','points_cost','minutes'), ['active'=>true])); + return redirect()->route('admin.rewards.index')->with('success','Belohnung erstellt.'); + } + public function edit(Reward $reward) { return view('admin.rewards.edit', compact('reward')); } + public function update(Request $r, Reward $reward) { + $r->validate([ + 'name' => 'required|string|max:80', + 'description' => 'nullable|string|max:200', + 'icon' => 'required|string|max:10', + 'points_cost' => 'required|integer|min:1', + 'minutes' => 'nullable|integer|min:1', + ]); + $reward->update(array_merge( + $r->only('name','description','icon','points_cost','minutes'), + ['active' => $r->boolean('active', true)] + )); + return redirect()->route('admin.rewards.index')->with('success','Gespeichert.'); + } + public function destroy(Reward $reward) { + $reward->delete(); + return redirect()->route('admin.rewards.index')->with('success','Belohnung gelöscht.'); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php new file mode 100644 index 0000000..c734419 --- /dev/null +++ b/app/Http/Controllers/Admin/UserController.php @@ -0,0 +1,45 @@ +withCount('attempts')->orderBy('name')->get(); + return view('admin.users.index', compact('users')); + } + public function create() { return view('admin.users.create'); } + public function store(Request $r) { + $r->validate([ + 'name' => 'required|string|max:60', + 'email' => 'required|email|unique:users', + 'password' => 'required|min:6', + ]); + User::create([ + 'name' => $r->name, + 'email' => $r->email, + 'password' => Hash::make($r->password), + 'role' => 'child', + 'points' => 0, + ]); + return redirect()->route('admin.users.index')->with('success','Kind-Konto erstellt.'); + } + public function edit(User $user) { return view('admin.users.edit', compact('user')); } + public function update(Request $r, User $user) { + $r->validate([ + 'name' => 'required|string|max:60', + 'email' => 'required|email|unique:users,email,'.$user->id, + 'password' => 'nullable|min:6', + 'points' => 'required|integer|min:0', + ]); + $user->fill(['name'=>$r->name,'email'=>$r->email,'points'=>$r->points]); + if ($r->filled('password')) $user->password = Hash::make($r->password); + $user->save(); + return redirect()->route('admin.users.index')->with('success','Gespeichert.'); + } + public function destroy(User $user) { + $user->delete(); + return redirect()->route('admin.users.index')->with('success','Konto gelöscht.'); + } +} diff --git a/app/Http/Controllers/Child/DashboardController.php b/app/Http/Controllers/Child/DashboardController.php new file mode 100644 index 0000000..15f858b --- /dev/null +++ b/app/Http/Controllers/Child/DashboardController.php @@ -0,0 +1,22 @@ +user(); + $level = $user->level(); + $streak = $user->currentStreak(); + $subjects = Subject::withCount(['questions as total' => fn($q) => $q->where('active',true)])->get() + ->map(function($s) use ($user) { + $s->correct = QuestionAttempt::whereHas('question', fn($q) => $q->where('subject_id',$s->id)) + ->where('user_id',$user->id)->where('is_correct',true)->count(); + return $s; + }); + $recentAttempts = QuestionAttempt::with('question.subject') + ->where('user_id',$user->id)->latest('id')->take(5)->get(); + $pendingRedemptions = $user->redemptions()->where('status','pending')->count(); + return view('child.dashboard', compact('user','level','streak','subjects','recentAttempts','pendingRedemptions')); + } +} diff --git a/app/Http/Controllers/Child/LearnController.php b/app/Http/Controllers/Child/LearnController.php new file mode 100644 index 0000000..db8d96b --- /dev/null +++ b/app/Http/Controllers/Child/LearnController.php @@ -0,0 +1,64 @@ +user(); + $subjects = Subject::all()->map(function($s) use ($user) { + $s->correct = QuestionAttempt::whereHas('question', fn($q) => $q->where('subject_id',$s->id)) + ->where('user_id',$user->id)->where('is_correct',true)->count(); + $s->total = $s->questions()->where('active',true)->count(); + return $s; + }); + return view('child.learn.subjects', compact('subjects')); + } + public function quiz(Subject $subject) { + $user = auth()->user(); + // Bevorzuge Fragen die heute noch nicht beantwortet wurden + $answeredToday = QuestionAttempt::where('user_id',$user->id) + ->whereDate('created_at', today()) + ->pluck('question_id'); + $question = $subject->activeQuestions() + ->whereNotIn('id', $answeredToday) + ->with('answerOptions') + ->inRandomOrder() + ->first(); + // Falls alle beantwortet: ganz random + if (!$question) { + $question = $subject->activeQuestions()->with('answerOptions')->inRandomOrder()->first(); + } + if (!$question) { + return redirect()->route('learn.subjects')->with('info','Noch keine Fragen für dieses Fach.'); + } + // Optionen mischen + $question->answerOptions = $question->answerOptions->shuffle(); + return view('child.learn.quiz', compact('subject','question')); + } + public function answer(Request $r, Subject $subject) { + $r->validate(['question_id'=>'required|exists:questions,id','answer'=>'required']); + $user = auth()->user(); + $question = Question::with('answerOptions')->findOrFail($r->question_id); + $correct = $question->answerOptions->where('is_correct',true)->first(); + $isRight = (string)$r->answer === (string)$correct->id; + $earned = 0; + if ($isRight) { + $earned = $question->points_value; + // Streak-Bonus: 3+ in Folge = +5 + $streak = $user->currentStreak(); + if ($streak >= 2) $earned += 5; + $user->increment('points', $earned); + } + QuestionAttempt::create([ + 'user_id' => $user->id, + 'question_id' => $question->id, + 'is_correct' => $isRight, + 'points_earned'=> $earned, + ]); + $newStreak = $user->fresh()->currentStreak(); + return view('child.learn.result', compact('subject','question','correct','isRight','earned','newStreak')); + } +} diff --git a/app/Http/Controllers/Child/RewardController.php b/app/Http/Controllers/Child/RewardController.php new file mode 100644 index 0000000..08f952c --- /dev/null +++ b/app/Http/Controllers/Child/RewardController.php @@ -0,0 +1,29 @@ +user(); + $rewards = Reward::where('active',true)->orderBy('points_cost')->get(); + $history = RewardRedemption::with('reward')->where('user_id',$user->id)->latest()->take(20)->get(); + return view('child.rewards.index', compact('user','rewards','history')); + } + public function redeem(Request $r, Reward $reward) { + $user = auth()->user(); + if (!$reward->active) abort(422); + if ($user->points < $reward->points_cost) { + return back()->with('error','Nicht genug Münzen!'); + } + $user->decrement('points', $reward->points_cost); + RewardRedemption::create([ + 'user_id' => $user->id, + 'reward_id' => $reward->id, + 'status' => 'pending', + 'points_spent'=> $reward->points_cost, + ]); + return back()->with('success','Eingelöst! Warte auf Freigabe.'); + } +} diff --git a/app/Http/Middleware/EnsureAdmin.php b/app/Http/Middleware/EnsureAdmin.php new file mode 100644 index 0000000..0bfa03d --- /dev/null +++ b/app/Http/Middleware/EnsureAdmin.php @@ -0,0 +1,12 @@ +user() || !$request->user()->isAdmin()) { + abort(403); + } + return $next($request); + } +} diff --git a/app/Http/Middleware/EnsureChild.php b/app/Http/Middleware/EnsureChild.php new file mode 100644 index 0000000..15ee4ff --- /dev/null +++ b/app/Http/Middleware/EnsureChild.php @@ -0,0 +1,12 @@ +user() || !$request->user()->isChild()) { + abort(403); + } + return $next($request); + } +} diff --git a/app/Models/AnswerOption.php b/app/Models/AnswerOption.php new file mode 100644 index 0000000..e0433d3 --- /dev/null +++ b/app/Models/AnswerOption.php @@ -0,0 +1,9 @@ + 'boolean']; + public function question() { return $this->belongsTo(Question::class); } +} diff --git a/app/Models/Question.php b/app/Models/Question.php new file mode 100644 index 0000000..54742b5 --- /dev/null +++ b/app/Models/Question.php @@ -0,0 +1,13 @@ + 'boolean']; + public function subject() { return $this->belongsTo(Subject::class); } + public function answerOptions() { return $this->hasMany(AnswerOption::class)->orderBy('sort_order'); } + public function attempts() { return $this->hasMany(QuestionAttempt::class); } + public function difficultyStars(): string { + return str_repeat('⭐', $this->difficulty); + } +} diff --git a/app/Models/QuestionAttempt.php b/app/Models/QuestionAttempt.php new file mode 100644 index 0000000..fda2ea0 --- /dev/null +++ b/app/Models/QuestionAttempt.php @@ -0,0 +1,10 @@ + 'boolean', 'created_at' => 'datetime']; + public function user() { return $this->belongsTo(User::class); } + public function question() { return $this->belongsTo(Question::class); } +} diff --git a/app/Models/Reward.php b/app/Models/Reward.php new file mode 100644 index 0000000..0d24c91 --- /dev/null +++ b/app/Models/Reward.php @@ -0,0 +1,8 @@ + 'boolean']; + public function redemptions() { return $this->hasMany(RewardRedemption::class); } +} diff --git a/app/Models/RewardRedemption.php b/app/Models/RewardRedemption.php new file mode 100644 index 0000000..77b5ce1 --- /dev/null +++ b/app/Models/RewardRedemption.php @@ -0,0 +1,10 @@ + 'datetime']; + public function user() { return $this->belongsTo(User::class); } + public function reward() { return $this->belongsTo(Reward::class); } + public function resolver() { return $this->belongsTo(User::class, 'resolved_by'); } +} diff --git a/app/Models/Subject.php b/app/Models/Subject.php new file mode 100644 index 0000000..1085704 --- /dev/null +++ b/app/Models/Subject.php @@ -0,0 +1,8 @@ +hasMany(Question::class); } + public function activeQuestions() { return $this->questions()->where('active', true); } +} diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..04b1669 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1,32 +1,35 @@ */ +class User extends Authenticatable { use HasFactory, Notifiable; + protected $fillable = ['name','email','password','role','points']; + protected $hidden = ['password','remember_token']; + protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed']; - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; + public function isAdmin(): bool { return $this->role === 'admin'; } + public function isChild(): bool { return $this->role === 'child'; } + + public function attempts() { return $this->hasMany(QuestionAttempt::class); } + public function redemptions() { return $this->hasMany(RewardRedemption::class); } + + public function level(): array { + $p = $this->points; + if ($p >= 600) return ['label' => 'Meister', 'icon' => '👑', 'color' => 'text-yellow-500', 'next' => null, 'progress' => 100]; + if ($p >= 300) return ['label' => 'Wissensprofi', 'icon' => '🌟', 'color' => 'text-purple-500', 'next' => 600, 'progress' => (int)(($p-300)/3)]; + if ($p >= 100) return ['label' => 'Lernstar', 'icon' => '⭐', 'color' => 'text-blue-500', 'next' => 300, 'progress' => (int)(($p-100)/2)]; + return ['label' => 'Anfänger', 'icon' => '🌱', 'color' => 'text-green-500', 'next' => 100, 'progress' => $p]; + } + + public function currentStreak(): int { + $last = $this->attempts()->latest('id')->take(10)->get(); + $streak = 0; + foreach ($last as $a) { + if (!$a->is_correct) break; + $streak++; + } + return $streak; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..2dea583 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,4 @@ withMiddleware(function (Middleware $middleware): void { - // + ->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'admin' => \App\Http\Middleware\EnsureAdmin::class, + 'child' => \App\Http\Middleware\EnsureChild::class, + ]); }) - ->withExceptions(function (Exceptions $exceptions): void { - // - })->create(); + ->withExceptions(function (Exceptions $exceptions) {}) + ->create(); diff --git a/database/migrations/2026_05_05_200000_add_role_points_to_users_table.php b/database/migrations/2026_05_05_200000_add_role_points_to_users_table.php new file mode 100644 index 0000000..3fed049 --- /dev/null +++ b/database/migrations/2026_05_05_200000_add_role_points_to_users_table.php @@ -0,0 +1,15 @@ +enum('role', ['admin', 'child'])->default('child')->after('email'); + $table->unsignedInteger('points')->default(0)->after('role'); + }); + } + public function down(): void { + Schema::table('users', fn(Blueprint $t) => $t->dropColumn(['role','points'])); + } +}; diff --git a/database/migrations/2026_05_05_200001_create_subjects_table.php b/database/migrations/2026_05_05_200001_create_subjects_table.php new file mode 100644 index 0000000..619e63b --- /dev/null +++ b/database/migrations/2026_05_05_200001_create_subjects_table.php @@ -0,0 +1,17 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('icon')->default('📚'); + $table->string('color')->default('blue'); + $table->timestamps(); + }); + } + public function down(): void { Schema::dropIfExists('subjects'); } +}; diff --git a/database/migrations/2026_05_05_200002_create_questions_table.php b/database/migrations/2026_05_05_200002_create_questions_table.php new file mode 100644 index 0000000..6646969 --- /dev/null +++ b/database/migrations/2026_05_05_200002_create_questions_table.php @@ -0,0 +1,19 @@ +id(); + $table->foreignId('subject_id')->constrained()->onDelete('cascade'); + $table->text('question_text'); + $table->enum('type', ['multiple_choice', 'number_input'])->default('multiple_choice'); + $table->unsignedTinyInteger('difficulty')->default(1); + $table->unsignedTinyInteger('points_value')->default(5); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + public function down(): void { Schema::dropIfExists('questions'); } +}; diff --git a/database/migrations/2026_05_05_200003_create_answer_options_table.php b/database/migrations/2026_05_05_200003_create_answer_options_table.php new file mode 100644 index 0000000..17330ba --- /dev/null +++ b/database/migrations/2026_05_05_200003_create_answer_options_table.php @@ -0,0 +1,16 @@ +id(); + $table->foreignId('question_id')->constrained()->onDelete('cascade'); + $table->string('text'); + $table->boolean('is_correct')->default(false); + $table->unsignedTinyInteger('sort_order')->default(0); + }); + } + public function down(): void { Schema::dropIfExists('answer_options'); } +}; diff --git a/database/migrations/2026_05_05_200004_create_question_attempts_table.php b/database/migrations/2026_05_05_200004_create_question_attempts_table.php new file mode 100644 index 0000000..2bc3c06 --- /dev/null +++ b/database/migrations/2026_05_05_200004_create_question_attempts_table.php @@ -0,0 +1,17 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('question_id')->constrained()->onDelete('cascade'); + $table->boolean('is_correct'); + $table->unsignedTinyInteger('points_earned')->default(0); + $table->timestamp('created_at')->useCurrent(); + }); + } + public function down(): void { Schema::dropIfExists('question_attempts'); } +}; diff --git a/database/migrations/2026_05_05_200005_create_rewards_table.php b/database/migrations/2026_05_05_200005_create_rewards_table.php new file mode 100644 index 0000000..15d702e --- /dev/null +++ b/database/migrations/2026_05_05_200005_create_rewards_table.php @@ -0,0 +1,19 @@ +id(); + $table->string('name'); + $table->string('description')->nullable(); + $table->string('icon')->default('🎁'); + $table->unsignedInteger('points_cost'); + $table->unsignedInteger('minutes')->nullable(); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + } + public function down(): void { Schema::dropIfExists('rewards'); } +}; diff --git a/database/migrations/2026_05_05_200006_create_reward_redemptions_table.php b/database/migrations/2026_05_05_200006_create_reward_redemptions_table.php new file mode 100644 index 0000000..564fbc2 --- /dev/null +++ b/database/migrations/2026_05_05_200006_create_reward_redemptions_table.php @@ -0,0 +1,20 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('reward_id')->constrained()->onDelete('cascade'); + $table->enum('status', ['pending','approved','rejected'])->default('pending'); + $table->unsignedInteger('points_spent'); + $table->string('note')->nullable(); + $table->timestamp('resolved_at')->nullable(); + $table->foreignId('resolved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + public function down(): void { Schema::dropIfExists('reward_redemptions'); } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..3e1e2e6 100755 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -1,25 +1,117 @@ create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', +class DatabaseSeeder extends Seeder { + public function run(): void { + User::create([ + 'name' => 'Admin', + 'email' => 'admin@lernapp.de', + 'password' => Hash::make('Admin1234!'), + 'role' => 'admin', + 'points' => 0, ]); + + $subjects = [ + ['name'=>'Mathe', 'slug'=>'mathe', 'icon'=>'🔢', 'color'=>'green'], + ['name'=>'Deutsch', 'slug'=>'deutsch', 'icon'=>'📖', 'color'=>'blue'], + ['name'=>'Englisch','slug'=>'englisch', 'icon'=>'🌍', 'color'=>'orange'], + ]; + foreach ($subjects as $s) { + Subject::create($s); + } + + $questions = [ + // Mathe + ['subject'=>'mathe','text'=>'7 × 8 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['56',true],['48',false],['54',false],['64',false]]], + ['subject'=>'mathe','text'=>'144 ÷ 12 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['12',true],['10',false],['11',false],['14',false]]], + ['subject'=>'mathe','text'=>'234 + 567 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['801',true],['791',false],['800',false],['811',false]]], + ['subject'=>'mathe','text'=>'1000 − 387 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['613',true],['623',false],['713',false],['587',false]]], + ['subject'=>'mathe','text'=>'Eine Schachtel hat 24 Pralinen. Wie viele Schachteln brauchst du für 96 Pralinen?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['4',true],['3',false],['5',false],['6',false]]], + ['subject'=>'mathe','text'=>'Was ist die Hälfte von 136?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['68',true],['64',false],['70',false],['72',false]]], + ['subject'=>'mathe','text'=>'Ein Rechteck ist 9 cm lang und 6 cm breit. Wie groß ist sein Flächeninhalt?','type'=>'multiple_choice','diff'=>3,'pts'=>20, + 'options'=>[['54 cm²',true],['30 cm²',false],['45 cm²',false],['60 cm²',false]]], + ['subject'=>'mathe','text'=>'3 × 4 + 2 × 5 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['22',true],['34',false],['26',false],['20',false]]], + // Deutsch + ['subject'=>'deutsch','text'=>'Welches Wort ist ein Nomen?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['Hund',true],['laufen',false],['schnell',false],['grün',false]]], + ['subject'=>'deutsch','text'=>'Was ist die Einzahl von „Häuser"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['Haus',true],['Häusen',false],['Häuse',false],['Hausung',false]]], + ['subject'=>'deutsch','text'=>'Welche Zeitform ist: „Er spielte Fußball."?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['Präteritum',true],['Präsens',false],['Futur',false],['Perfekt',false]]], + ['subject'=>'deutsch','text'=>'Welche Satzart ist das: „Wie schön der Tag ist!"?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['Ausrufesatz',true],['Aussagesatz',false],['Fragesatz',false],['Aufforderungssatz',false]]], + ['subject'=>'deutsch','text'=>'Was ist das Gegenteil von „fröhlich"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['traurig',true],['lustig',false],['mutig',false],['fleißig',false]]], + ['subject'=>'deutsch','text'=>'Welches Wort ist ein Verb?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['rennen',true],['Tisch',false],['blau',false],['schnell',false]]], + ['subject'=>'deutsch','text'=>'Wie heißt der Plural von „das Kind"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['die Kinder',true],['die Kinde',false],['die Kinds',false],['die Kindern',false]]], + ['subject'=>'deutsch','text'=>'Welches Komma ist richtig? „Ich esse ___ Brot, Butter und Käse."','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['kein Komma nötig',true],['nach „esse"',false],['nach „Brot"',false],['nach „Butter"',false]]], + // Englisch + ['subject'=>'englisch','text'=>'What is "der Hund" in English?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['the dog',true],['the cat',false],['the horse',false],['the pig',false]]], + ['subject'=>'englisch','text'=>'Which word means "groß"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['big',true],['small',false],['old',false],['fast',false]]], + ['subject'=>'englisch','text'=>'"I ___ to school every day." – Which word fits?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['go',true],['going',false],['goes',false],['gone',false]]], + ['subject'=>'englisch','text'=>'What is the plural of "child"?','type'=>'multiple_choice','diff'=>2,'pts'=>10, + 'options'=>[['children',true],['childs',false],['childes',false],['child',false]]], + ['subject'=>'englisch','text'=>'What color is "gelb"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['yellow',true],['red',false],['green',false],['blue',false]]], + ['subject'=>'englisch','text'=>'How do you say "Guten Morgen"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['Good morning',true],['Good night',false],['Good evening',false],['Good afternoon',false]]], + ['subject'=>'englisch','text'=>'"She ___ a book yesterday." – Which word fits?','type'=>'multiple_choice','diff'=>3,'pts'=>20, + 'options'=>[['read',true],['reads',false],['reading',false],['is reading',false]]], + ['subject'=>'englisch','text'=>'What is the opposite of "hot"?','type'=>'multiple_choice','diff'=>1,'pts'=>5, + 'options'=>[['cold',true],['warm',false],['cool',false],['fast',false]]], + ]; + + $subjectMap = Subject::pluck('id','slug')->all(); + + foreach ($questions as $i => $q) { + $question = Question::create([ + 'subject_id' => $subjectMap[$q['subject']], + 'question_text' => $q['text'], + 'type' => $q['type'], + 'difficulty' => $q['diff'], + 'points_value' => $q['pts'], + 'active' => true, + ]); + foreach ($q['options'] as $j => [$text, $correct]) { + AnswerOption::create([ + 'question_id' => $question->id, + 'text' => $text, + 'is_correct' => $correct, + 'sort_order' => $j, + ]); + } + } + + $rewards = [ + ['name'=>'30 Min. Fernsehen', 'description'=>'30 Minuten Fernsehzeit', 'icon'=>'📺','points_cost'=>50, 'minutes'=>30], + ['name'=>'1 Std. Fernsehen', 'description'=>'Eine Stunde Fernsehzeit', 'icon'=>'📺','points_cost'=>90, 'minutes'=>60], + ['name'=>'30 Min. Konsole', 'description'=>'30 Minuten Spielkonsole', 'icon'=>'🎮','points_cost'=>75, 'minutes'=>30], + ['name'=>'1 Std. Konsole', 'description'=>'Eine Stunde Spielkonsole', 'icon'=>'🎮','points_cost'=>130,'minutes'=>60], + ['name'=>'Wunschdessert', 'description'=>'Ein Dessert deiner Wahl', 'icon'=>'🍰','points_cost'=>80, 'minutes'=>null], + ['name'=>'1 Std. länger aufbleiben','description'=>'Heute eine Stunde später ins Bett','icon'=>'🌙','points_cost'=>120,'minutes'=>null], + ]; + foreach ($rewards as $r) { + Reward::create(array_merge($r, ['active' => true])); + } } } diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php new file mode 100644 index 0000000..7b93fd7 --- /dev/null +++ b/resources/views/admin/dashboard.blade.php @@ -0,0 +1,51 @@ +@extends('layouts.admin') +@section('title','Dashboard') +@section('content') +
+
+
{{ $children }}
+
👦 Kinder
+
+
+
{{ $questions }}
+
❓ Fragen
+
+
+
{{ $attempts_today }}
+
📝 Antworten heute
+
+
+
{{ $pending }}
+
🎁 Offene Einlösungen
+
+
+
+
+

🏆 Bestenliste

+ @forelse($topKids as $i => $kid) +
+
+ {{ ['🥇','🥈','🥉','4.','5.'][$i] }} + {{ $kid->name }} +
+ 🪙 {{ $kid->points }} +
+ @empty

Noch keine Kinder angelegt.

@endforelse +
+
+

📝 Letzte Aktivität

+
+ @forelse($recentAttempts as $a) +
+
+ {{ $a->is_correct ? '✅' : '❌' }} + {{ $a->user->name }} + – {{ $a->question->subject->name }} +
+ {{ $a->created_at->diffForHumans() }} +
+ @empty

Noch keine Aktivität.

@endforelse +
+
+
+@endsection diff --git a/resources/views/admin/questions/create.blade.php b/resources/views/admin/questions/create.blade.php new file mode 100644 index 0000000..84e39fd --- /dev/null +++ b/resources/views/admin/questions/create.blade.php @@ -0,0 +1,54 @@ +@extends('layouts.admin') +@section('title','Neue Frage') +@section('content') +
+ ← Zurück +
+

Neue Frage erstellen

+
+ @csrf +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + @for($i=0;$i<4;$i++) +
+ + +
+ @endfor +
+
+ + +
+ +
+
+
+@endsection diff --git a/resources/views/admin/questions/edit.blade.php b/resources/views/admin/questions/edit.blade.php new file mode 100644 index 0000000..f5e14c6 --- /dev/null +++ b/resources/views/admin/questions/edit.blade.php @@ -0,0 +1,49 @@ +@extends('layouts.admin') +@section('title','Frage bearbeiten') +@section('content') +
+ ← Zurück +
+

Frage bearbeiten

+
+ @csrf @method('PUT') +
+
+ + +
+
+ + +
+
+
+ + +
+ @if($question->type==='multiple_choice') +
+ + @foreach($question->answerOptions as $i=>$opt) +
+ is_correct?'checked':'' }} class="accent-violet-600"> + +
+ @endforeach +
+ @endif +
+ active?'checked':'' }} class="accent-violet-600"> + +
+ +
+
+
+@endsection diff --git a/resources/views/admin/questions/index.blade.php b/resources/views/admin/questions/index.blade.php new file mode 100644 index 0000000..e9827ee --- /dev/null +++ b/resources/views/admin/questions/index.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.admin') +@section('title','Fragen') +@section('content') +
+
+ + +
+ + Neue Frage +
+
+ + + + + + + + + + + + @forelse($questions as $q) + + + + + + + + @empty + + @endforelse + +
FrageFachSchwierigkeitAktiv
{{ $q->question_text }}{{ $q->subject->icon }} {{ $q->subject->name }}{{ $q->difficultyStars() }}{{ $q->active ? '✅' : '⏸️' }} + Bearbeiten +
+ @csrf @method('DELETE') + +
+
Keine Fragen vorhanden.
+
+
{{ $questions->links() }}
+@endsection diff --git a/resources/views/admin/redemptions/index.blade.php b/resources/views/admin/redemptions/index.blade.php new file mode 100644 index 0000000..10206a3 --- /dev/null +++ b/resources/views/admin/redemptions/index.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.admin') +@section('title','Einlösungen') +@section('content') +@if($pending->count()) +

⏳ Warten auf Freigabe

+
+@foreach($pending as $red) +
+
+ {{ $red->reward->icon }} +
+
{{ $red->user->name }} möchte: {{ $red->reward->name }}
+
🪙 {{ $red->points_spent }} Münzen · {{ $red->created_at->diffForHumans() }}
+
+
+
+
+ @csrf @method('PATCH') + +
+
+ @csrf @method('PATCH') + +
+
+
+@endforeach +
+@else +
✅ Keine offenen Einlösungen.
+@endif +

📋 Verlauf

+
+ + + + + + + + + + + + @forelse($resolved as $red) + + + + + + + + @empty + + @endforelse + +
KindBelohnungStatusMünzenDatum
{{ $red->user->name }}{{ $red->reward->icon }} {{ $red->reward->name }} + + {{ $red->status==='approved' ? 'Freigegeben' : 'Abgelehnt' }} + + 🪙 {{ $red->points_spent }}{{ $red->created_at->format('d.m.Y') }}
Noch keine Einlösungen.
+
+@endsection diff --git a/resources/views/admin/rewards/create.blade.php b/resources/views/admin/rewards/create.blade.php new file mode 100644 index 0000000..96e4575 --- /dev/null +++ b/resources/views/admin/rewards/create.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.admin') +@section('title','Neue Belohnung') +@section('content') +
+ ← Zurück +
+

Neue Belohnung

+
+ @csrf +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+@endsection diff --git a/resources/views/admin/rewards/edit.blade.php b/resources/views/admin/rewards/edit.blade.php new file mode 100644 index 0000000..055a3e1 --- /dev/null +++ b/resources/views/admin/rewards/edit.blade.php @@ -0,0 +1,42 @@ +@extends('layouts.admin') +@section('title','Belohnung bearbeiten') +@section('content') +
+ ← Zurück +
+

Belohnung bearbeiten

+
+ @csrf @method('PUT') +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ active?'checked':'' }} class="accent-violet-600"> + +
+ +
+
+
+@endsection diff --git a/resources/views/admin/rewards/index.blade.php b/resources/views/admin/rewards/index.blade.php new file mode 100644 index 0000000..607aabc --- /dev/null +++ b/resources/views/admin/rewards/index.blade.php @@ -0,0 +1,33 @@ +@extends('layouts.admin') +@section('title','Belohnungen') +@section('content') +
+

Belohnungen

+ + Neue Belohnung +
+
+@forelse($rewards as $r) +
+
+ {{ $r->icon }} + {{ $r->active ? 'Aktiv' : 'Inaktiv' }} +
+
{{ $r->name }}
+ @if($r->description)
{{ $r->description }}
@endif +
+ 🪙 {{ $r->points_cost }} Münzen + @if($r->minutes){{ $r->minutes }} Min.@endif +
+
+ Bearbeiten +
+ @csrf @method('DELETE') + +
+
+
+@empty +

Noch keine Belohnungen.

+@endforelse +
+@endsection diff --git a/resources/views/admin/users/create.blade.php b/resources/views/admin/users/create.blade.php new file mode 100644 index 0000000..de7d631 --- /dev/null +++ b/resources/views/admin/users/create.blade.php @@ -0,0 +1,29 @@ +@extends('layouts.admin') +@section('title','Neues Kind') +@section('content') +
+ ← Zurück +
+

Neues Kind-Konto

+
+ @csrf +
+ + + @error('name')

{{ $message }}

@enderror +
+
+ + + @error('email')

{{ $message }}

@enderror +
+
+ + + @error('password')

{{ $message }}

@enderror +
+ +
+
+
+@endsection diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php new file mode 100644 index 0000000..2e6a9cc --- /dev/null +++ b/resources/views/admin/users/edit.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.admin') +@section('title','Kind bearbeiten') +@section('content') +
+ ← Zurück +
+

{{ $user->name }} bearbeiten

+
+ @csrf @method('PUT') +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+@endsection diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php new file mode 100644 index 0000000..e3261d7 --- /dev/null +++ b/resources/views/admin/users/index.blade.php @@ -0,0 +1,40 @@ +@extends('layouts.admin') +@section('title','Kinder') +@section('content') +
+

Alle Kinder-Konten

+ + Neues Kind +
+
+ + + + + + + + + + + + @forelse($users as $u) + + + + + + + + @empty + + @endforelse + +
NameE-Mail🪙 MünzenAntworten
{{ $u->name }}{{ $u->email }}{{ $u->points }}{{ $u->attempts_count }} + Bearbeiten +
+ @csrf @method('DELETE') + +
+
Noch keine Kinder angelegt.
+
+@endsection diff --git a/resources/views/child/dashboard.blade.php b/resources/views/child/dashboard.blade.php new file mode 100644 index 0000000..eee2787 --- /dev/null +++ b/resources/views/child/dashboard.blade.php @@ -0,0 +1,72 @@ +@extends('layouts.child') +@section('content') +@php $level = auth()->user()->level() @endphp +
+
{{ $level['icon'] }}
+

Hallo, {{ auth()->user()->name }}! 👋

+

Du bist: {{ $level['label'] }}

+ @if($streak >= 3) +
+ 🔥 {{ $streak }}er-Serie! +5 Bonusmünzen pro Antwort +
+ @elseif($streak > 0) +
+ ✨ {{ $streak }} richtige Antworten in Folge! +
+ @endif +
+ +
+
+
{{ auth()->user()->points }}
+
🪙 Münzen
+ @if($level['next']) +
+
Nächstes Level in {{ $level['next'] - auth()->user()->points }} Münzen
+
+
+ @endif +
+ @if($pendingRedemptions > 0) + +
{{ $pendingRedemptions }}
+
🎁 Einlösung wartet
+
+ @else + +
🎁
+
Belohnungen
+
+ @endif +
+ +

📚 Fächer

+
+@foreach($subjects as $s) +@php $colors = ['green'=>'from-green-400 to-emerald-500','blue'=>'from-blue-400 to-indigo-500','orange'=>'from-orange-400 to-amber-500'] @endphp + +
{{ $s->icon }}
+
{{ $s->name }}
+
{{ $s->correct }} ✓
+
+@endforeach +
+ +@if($recentAttempts->count()) +

⏱️ Zuletzt gespielt

+
+@foreach($recentAttempts as $a) +
+
+ {{ $a->is_correct ? '✅' : '❌' }} +
+
{{ Str::limit($a->question->question_text, 40) }}
+
{{ $a->question->subject->name }}
+
+
+ @if($a->points_earned > 0)+{{ $a->points_earned }} 🪙@endif +
+@endforeach +
+@endif +@endsection diff --git a/resources/views/child/learn/quiz.blade.php b/resources/views/child/learn/quiz.blade.php new file mode 100644 index 0000000..bb802cf --- /dev/null +++ b/resources/views/child/learn/quiz.blade.php @@ -0,0 +1,34 @@ +@extends('layouts.child') +@section('content') +
+ ← Fächer + {{ $subject->icon }} + {{ $subject->name }} + {{ $question->difficultyStars() }} +
+
+
+ {{ ['','⭐ Leicht — 5 Münzen','⭐⭐ Mittel — 10 Münzen','⭐⭐⭐ Schwer — 20 Münzen'][$question->difficulty] }} +
+

{{ $question->question_text }}

+
+
+ @csrf + +
+ @foreach($question->answerOptions as $opt) + + @endforeach +
+ +
+@endsection diff --git a/resources/views/child/learn/result.blade.php b/resources/views/child/learn/result.blade.php new file mode 100644 index 0000000..c9e307a --- /dev/null +++ b/resources/views/child/learn/result.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.child') +@section('content') +
+ @if($isRight) +
🎉
+

Richtig!

+

Super gemacht!

+
+ +{{ $earned }} 🪙 +
+ @if($earned > $question->points_value) +

🔥 Serien-Bonus inklusive!

+ @endif + @if($newStreak >= 3) +
+ 🔥 {{ $newStreak }}er-Serie! Weiter so! +
+ @endif + @else +
😅
+

Leider falsch

+

Die richtige Antwort war:

+
+ {{ $correct->text }} +
+ @endif + + + +
+ Dein Kontostand: 🪙 {{ auth()->user()->fresh()->points }} +
+
+@endsection diff --git a/resources/views/child/learn/subjects.blade.php b/resources/views/child/learn/subjects.blade.php new file mode 100644 index 0000000..4808a32 --- /dev/null +++ b/resources/views/child/learn/subjects.blade.php @@ -0,0 +1,19 @@ +@extends('layouts.child') +@section('content') +

📚 Welches Fach?

+
+@foreach($subjects as $s) +@php $colors = ['green'=>'from-green-400 to-emerald-500','blue'=>'from-blue-400 to-indigo-500','orange'=>'from-orange-400 to-amber-500'] @endphp + +
+ {{ $s->icon }} +
+
{{ $s->name }}
+
{{ $s->total }} Fragen · {{ $s->correct }} richtig beantwortet
+
+
+
+
+@endforeach +
+@endsection diff --git a/resources/views/child/rewards/index.blade.php b/resources/views/child/rewards/index.blade.php new file mode 100644 index 0000000..cd0d2b5 --- /dev/null +++ b/resources/views/child/rewards/index.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.child') +@section('content') +
+

🎁 Belohnungen

+
🪙 {{ auth()->user()->points }}
+
+ +
+@foreach($rewards as $r) +@php $canAfford = auth()->user()->points >= $r->points_cost @endphp +
+
+
+ {{ $r->icon }} +
+
{{ $r->name }}
+ @if($r->description)
{{ $r->description }}
@endif + @if($r->minutes)
⏱ {{ $r->minutes }} Minuten
@endif +
+
+
+
🪙 {{ $r->points_cost }}
+ @if($canAfford) +
+ @csrf + +
+ @else +
Noch {{ $r->points_cost - auth()->user()->points }} fehlen
+ @endif +
+
+
+@endforeach +
+ +@if($history->count()) +

📋 Meine Einlösungen

+
+@foreach($history as $red) +
+
+ {{ $red->reward->icon }} +
+
{{ $red->reward->name }}
+
{{ $red->created_at->format('d.m.Y') }}
+
+
+ + {{ $red->status==='approved' ? '✅ Freigegeben' : ($red->status==='rejected' ? '❌ Abgelehnt' : '⏳ Warten...') }} + +
+@endforeach +
+@endif +@endsection diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php new file mode 100644 index 0000000..7c730e2 --- /dev/null +++ b/resources/views/layouts/admin.blade.php @@ -0,0 +1,58 @@ + + + + +Lernapp Admin +@vite(['resources/css/app.css','resources/js/app.js']) + + +
+ + + +
+
+

@yield('title','Dashboard')

+ {{ auth()->user()->name }} +
+
+ @if(session('success'))
{{ session('success') }}
@endif + @if(session('error'))
{{ session('error') }}
@endif + @yield('content') +
+
+
+ + diff --git a/resources/views/layouts/child.blade.php b/resources/views/layouts/child.blade.php new file mode 100644 index 0000000..2393ba3 --- /dev/null +++ b/resources/views/layouts/child.blade.php @@ -0,0 +1,33 @@ + + + + +Lernapp +@vite(['resources/css/app.css','resources/js/app.js']) + + + +
+ @if(session('success'))
✅ {{ session('success') }}
@endif + @if(session('error'))
❌ {{ session('error') }}
@endif + @if(session('info'))
ℹ️ {{ session('info') }}
@endif + @yield('content') +
+ + diff --git a/routes/web.php b/routes/web.php index 74bb7ca..73fff3e 100755 --- a/routes/web.php +++ b/routes/web.php @@ -1,20 +1,35 @@ redirect()->route('dashboard')); + +// Role-aware dashboard redirect +Route::middleware(['auth','verified'])->get('/dashboard', function () { + return auth()->user()->isAdmin() + ? redirect()->route('admin.dashboard') + : app(\App\Http\Controllers\Child\DashboardController::class)->index(); +})->name('dashboard'); + +// ── ADMIN ────────────────────────────────────────────────── +Route::middleware(['auth','admin'])->prefix('admin')->name('admin.')->group(function () { + Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard'); + Route::resource('users', Admin\UserController::class); + Route::resource('questions',Admin\QuestionController::class); + Route::resource('rewards', Admin\RewardController::class)->except('show'); + Route::get ('redemptions', [Admin\RedemptionController::class,'index']) ->name('redemptions.index'); + Route::patch ('redemptions/{redemption}/approve', [Admin\RedemptionController::class,'approve'])->name('redemptions.approve'); + Route::patch ('redemptions/{redemption}/reject', [Admin\RedemptionController::class,'reject']) ->name('redemptions.reject'); }); -Route::get('/dashboard', function () { - return view('dashboard'); -})->middleware(['auth', 'verified'])->name('dashboard'); - -Route::middleware('auth')->group(function () { - Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); - Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); - Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); +// ── CHILD ────────────────────────────────────────────────── +Route::middleware(['auth','child'])->group(function () { + Route::get ('lernen', [\App\Http\Controllers\Child\LearnController::class,'subjects'])->name('learn.subjects'); + Route::get ('lernen/{subject:slug}', [\App\Http\Controllers\Child\LearnController::class,'quiz']) ->name('learn.quiz'); + Route::post('lernen/{subject:slug}/antwort', [\App\Http\Controllers\Child\LearnController::class,'answer']) ->name('learn.answer'); + Route::get ('belohnungen', [\App\Http\Controllers\Child\RewardController::class,'index']) ->name('rewards.index'); + Route::post('belohnungen/{reward}/einloesen', [\App\Http\Controllers\Child\RewardController::class,'redeem']) ->name('rewards.redeem'); }); require __DIR__.'/auth.php';