feat: Lernapp mit Mathe/Deutsch/Englisch, Münzsystem und Belohnungen
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\QuestionAttempt;
|
||||
use App\Models\RewardRedemption;
|
||||
class DashboardController extends Controller {
|
||||
public function index() {
|
||||
$children = User::where('role','child')->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'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Question;
|
||||
use App\Models\Subject;
|
||||
use App\Models\AnswerOption;
|
||||
use Illuminate\Http\Request;
|
||||
class QuestionController extends Controller {
|
||||
public function index(Request $r) {
|
||||
$subjects = Subject::all();
|
||||
$query = Question::with('subject')->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\RewardRedemption;
|
||||
use Illuminate\Http\Request;
|
||||
class RedemptionController extends Controller {
|
||||
public function index() {
|
||||
$pending = RewardRedemption::with(['user','reward'])->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Reward;
|
||||
use Illuminate\Http\Request;
|
||||
class RewardController extends Controller {
|
||||
public function index() {
|
||||
$rewards = Reward::orderBy('points_cost')->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
class UserController extends Controller {
|
||||
public function index() {
|
||||
$users = User::where('role','child')->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Child;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Subject;
|
||||
use App\Models\QuestionAttempt;
|
||||
class DashboardController extends Controller {
|
||||
public function index() {
|
||||
$user = auth()->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Child;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Subject;
|
||||
use App\Models\Question;
|
||||
use App\Models\QuestionAttempt;
|
||||
use Illuminate\Http\Request;
|
||||
class LearnController extends Controller {
|
||||
public function subjects() {
|
||||
$user = auth()->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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Child;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Reward;
|
||||
use App\Models\RewardRedemption;
|
||||
use Illuminate\Http\Request;
|
||||
class RewardController extends Controller {
|
||||
public function index() {
|
||||
$user = auth()->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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Http\Middleware;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
class EnsureAdmin {
|
||||
public function handle(Request $request, Closure $next) {
|
||||
if (!$request->user() || !$request->user()->isAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace App\Http\Middleware;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
class EnsureChild {
|
||||
public function handle(Request $request, Closure $next) {
|
||||
if (!$request->user() || !$request->user()->isChild()) {
|
||||
abort(403);
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user