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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class AnswerOption extends Model {
|
||||||
|
public $timestamps = false;
|
||||||
|
protected $fillable = ['question_id','text','is_correct','sort_order'];
|
||||||
|
protected $casts = ['is_correct' => 'boolean'];
|
||||||
|
public function question() { return $this->belongsTo(Question::class); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class Question extends Model {
|
||||||
|
protected $fillable = ['subject_id','question_text','type','difficulty','points_value','active'];
|
||||||
|
protected $casts = ['active' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class QuestionAttempt extends Model {
|
||||||
|
public $timestamps = false;
|
||||||
|
protected $fillable = ['user_id','question_id','is_correct','points_earned'];
|
||||||
|
protected $casts = ['is_correct' => 'boolean', 'created_at' => 'datetime'];
|
||||||
|
public function user() { return $this->belongsTo(User::class); }
|
||||||
|
public function question() { return $this->belongsTo(Question::class); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class Reward extends Model {
|
||||||
|
protected $fillable = ['name','description','icon','points_cost','minutes','active'];
|
||||||
|
protected $casts = ['active' => 'boolean'];
|
||||||
|
public function redemptions() { return $this->hasMany(RewardRedemption::class); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class RewardRedemption extends Model {
|
||||||
|
protected $fillable = ['user_id','reward_id','status','points_spent','note','resolved_at','resolved_by'];
|
||||||
|
protected $casts = ['resolved_at' => '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'); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
class Subject extends Model {
|
||||||
|
protected $fillable = ['name','slug','icon','color'];
|
||||||
|
public function questions() { return $this->hasMany(Question::class); }
|
||||||
|
public function activeQuestions() { return $this->questions()->where('active', true); }
|
||||||
|
}
|
||||||
+26
-23
@@ -1,32 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Database\Factories\UserFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
class User extends Authenticatable {
|
||||||
#[Fillable(['name', 'email', 'password'])]
|
|
||||||
#[Hidden(['password', 'remember_token'])]
|
|
||||||
class User extends Authenticatable
|
|
||||||
{
|
|
||||||
/** @use HasFactory<UserFactory> */
|
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
|
protected $fillable = ['name','email','password','role','points'];
|
||||||
|
protected $hidden = ['password','remember_token'];
|
||||||
|
protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed'];
|
||||||
|
|
||||||
/**
|
public function isAdmin(): bool { return $this->role === 'admin'; }
|
||||||
* Get the attributes that should be cast.
|
public function isChild(): bool { return $this->role === 'child'; }
|
||||||
*
|
|
||||||
* @return array<string, string>
|
public function attempts() { return $this->hasMany(QuestionAttempt::class); }
|
||||||
*/
|
public function redemptions() { return $this->hasMany(RewardRedemption::class); }
|
||||||
protected function casts(): array
|
|
||||||
{
|
public function level(): array {
|
||||||
return [
|
$p = $this->points;
|
||||||
'email_verified_at' => 'datetime',
|
if ($p >= 600) return ['label' => 'Meister', 'icon' => '👑', 'color' => 'text-yellow-500', 'next' => null, 'progress' => 100];
|
||||||
'password' => 'hashed',
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -1,5 +1,4 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
@@ -10,9 +9,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->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 {
|
->withExceptions(function (Exceptions $exceptions) {})
|
||||||
//
|
->create();
|
||||||
})->create();
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->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']));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('subjects', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('questions', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('answer_options', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('question_attempts', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('rewards', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void {
|
||||||
|
Schema::create('reward_redemptions', function (Blueprint $table) {
|
||||||
|
$table->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'); }
|
||||||
|
};
|
||||||
@@ -1,25 +1,117 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use App\Models\Subject;
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\AnswerOption;
|
||||||
|
use App\Models\Reward;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder {
|
||||||
{
|
public function run(): void {
|
||||||
use WithoutModelEvents;
|
User::create([
|
||||||
|
'name' => 'Admin',
|
||||||
|
'email' => 'admin@lernapp.de',
|
||||||
|
'password' => Hash::make('Admin1234!'),
|
||||||
|
'role' => 'admin',
|
||||||
|
'points' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
$subjects = [
|
||||||
* Seed the application's database.
|
['name'=>'Mathe', 'slug'=>'mathe', 'icon'=>'🔢', 'color'=>'green'],
|
||||||
*/
|
['name'=>'Deutsch', 'slug'=>'deutsch', 'icon'=>'📖', 'color'=>'blue'],
|
||||||
public function run(): void
|
['name'=>'Englisch','slug'=>'englisch', 'icon'=>'🌍', 'color'=>'orange'],
|
||||||
{
|
];
|
||||||
// User::factory(10)->create();
|
foreach ($subjects as $s) {
|
||||||
|
Subject::create($s);
|
||||||
|
}
|
||||||
|
|
||||||
User::factory()->create([
|
$questions = [
|
||||||
'name' => 'Test User',
|
// Mathe
|
||||||
'email' => 'test@example.com',
|
['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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Dashboard')
|
||||||
|
@section('content')
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
|
||||||
|
<div class="text-2xl font-bold text-slate-800">{{ $children }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1">👦 Kinder</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
|
||||||
|
<div class="text-2xl font-bold text-slate-800">{{ $questions }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1">❓ Fragen</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200">
|
||||||
|
<div class="text-2xl font-bold text-slate-800">{{ $attempts_today }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1">📝 Antworten heute</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200 {{ $pending > 0 ? 'border-red-300 bg-red-50' : '' }}">
|
||||||
|
<div class="text-2xl font-bold {{ $pending > 0 ? 'text-red-600' : 'text-slate-800' }}">{{ $pending }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1">🎁 Offene Einlösungen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-2 gap-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-4">🏆 Bestenliste</h2>
|
||||||
|
@forelse($topKids as $i => $kid)
|
||||||
|
<div class="flex items-center justify-between py-2 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-lg">{{ ['🥇','🥈','🥉','4.','5.'][$i] }}</span>
|
||||||
|
<span class="font-medium text-slate-700">{{ $kid->name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-amber-600 font-bold">🪙 {{ $kid->points }}</span>
|
||||||
|
</div>
|
||||||
|
@empty<p class="text-slate-400 text-sm">Noch keine Kinder angelegt.</p>@endforelse
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-4">📝 Letzte Aktivität</h2>
|
||||||
|
<div class="space-y-2">
|
||||||
|
@forelse($recentAttempts as $a)
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{{ $a->is_correct ? '✅' : '❌' }}</span>
|
||||||
|
<span class="text-slate-600">{{ $a->user->name }}</span>
|
||||||
|
<span class="text-slate-400">– {{ $a->question->subject->name }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-slate-400">{{ $a->created_at->diffForHumans() }}</span>
|
||||||
|
</div>
|
||||||
|
@empty<p class="text-slate-400 text-sm">Noch keine Aktivität.</p>@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Neue Frage')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-2xl">
|
||||||
|
<a href="{{ route('admin.questions.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6" x-data="{type:'multiple_choice'}">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">Neue Frage erstellen</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.questions.store') }}" class="space-y-5">
|
||||||
|
@csrf
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Fach</label>
|
||||||
|
<select name="subject_id" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
@foreach($subjects as $s)<option value="{{ $s->id }}">{{ $s->icon }} {{ $s->name }}</option>@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Schwierigkeit</label>
|
||||||
|
<select name="difficulty" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
<option value="1">⭐ Leicht (5 Münzen)</option>
|
||||||
|
<option value="2">⭐⭐ Mittel (10 Münzen)</option>
|
||||||
|
<option value="3">⭐⭐⭐ Schwer (20 Münzen)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Typ</label>
|
||||||
|
<select name="type" x-model="type" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
<option value="multiple_choice">Multiple Choice</option>
|
||||||
|
<option value="number_input">Zahlenantwort</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Frage</label>
|
||||||
|
<textarea name="question_text" required rows="2" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">{{ old('question_text') }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div x-show="type==='multiple_choice'">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-2">Antwortmöglichkeiten <span class="text-slate-400">(Richtige markieren)</span></label>
|
||||||
|
@for($i=0;$i<4;$i++)
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<input type="radio" name="correct" value="{{ $i }}" {{ $i===0?'checked':'' }} class="accent-violet-600">
|
||||||
|
<input type="text" name="options[]" placeholder="Antwort {{ $i+1 }}" class="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
<div x-show="type==='number_input'">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Richtige Antwort (Zahl)</label>
|
||||||
|
<input type="text" name="number_answer" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Frage speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Frage bearbeiten')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-2xl">
|
||||||
|
<a href="{{ route('admin.questions.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">Frage bearbeiten</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.questions.update',$question) }}" class="space-y-5">
|
||||||
|
@csrf @method('PUT')
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Fach</label>
|
||||||
|
<select name="subject_id" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
@foreach($subjects as $s)<option value="{{ $s->id }}" {{ $s->id==$question->subject_id?'selected':'' }}>{{ $s->icon }} {{ $s->name }}</option>@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Schwierigkeit</label>
|
||||||
|
<select name="difficulty" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
@foreach([1=>'⭐ Leicht (5 Münzen)',2=>'⭐⭐ Mittel (10 Münzen)',3=>'⭐⭐⭐ Schwer (20 Münzen)'] as $v=>$l)
|
||||||
|
<option value="{{ $v }}" {{ $question->difficulty==$v?'selected':'' }}>{{ $l }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Frage</label>
|
||||||
|
<textarea name="question_text" required rows="2" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">{{ old('question_text',$question->question_text) }}</textarea>
|
||||||
|
</div>
|
||||||
|
@if($question->type==='multiple_choice')
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-2">Antwortmöglichkeiten</label>
|
||||||
|
@foreach($question->answerOptions as $i=>$opt)
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<input type="radio" name="correct" value="{{ $i }}" {{ $opt->is_correct?'checked':'' }} class="accent-violet-600">
|
||||||
|
<input type="text" name="options[]" value="{{ $opt->text }}" class="flex-1 border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input type="checkbox" name="active" id="active" value="1" {{ $question->active?'checked':'' }} class="accent-violet-600">
|
||||||
|
<label for="active" class="text-sm font-medium text-slate-700">Frage aktiv</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Fragen')
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-wrap gap-3 items-center justify-between mb-6">
|
||||||
|
<form method="GET" class="flex gap-2">
|
||||||
|
<select name="subject" class="border border-slate-300 rounded-lg px-3 py-2 text-sm">
|
||||||
|
<option value="">Alle Fächer</option>
|
||||||
|
@foreach($subjects as $s)<option value="{{ $s->id }}" {{ request('subject')==$s->id?'selected':'' }}>{{ $s->icon }} {{ $s->name }}</option>@endforeach
|
||||||
|
</select>
|
||||||
|
<button class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Filtern</button>
|
||||||
|
</form>
|
||||||
|
<a href="{{ route('admin.questions.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Frage</a>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">Frage</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">Fach</th>
|
||||||
|
<th class="text-center px-4 py-3 font-medium text-slate-600">Schwierigkeit</th>
|
||||||
|
<th class="text-center px-4 py-3 font-medium text-slate-600">Aktiv</th>
|
||||||
|
<th class="px-4 py-3"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
@forelse($questions as $q)
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 text-slate-700 max-w-xs truncate">{{ $q->question_text }}</td>
|
||||||
|
<td class="px-4 py-3"><span class="text-base">{{ $q->subject->icon }}</span> {{ $q->subject->name }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">{{ $q->difficultyStars() }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">{{ $q->active ? '✅' : '⏸️' }}</td>
|
||||||
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||||
|
<a href="{{ route('admin.questions.edit',$q) }}" class="text-violet-600 hover:underline mr-3">Bearbeiten</a>
|
||||||
|
<form method="POST" action="{{ route('admin.questions.destroy',$q) }}" class="inline" onsubmit="return confirm('Löschen?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button class="text-red-500 hover:underline">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-400">Keine Fragen vorhanden.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">{{ $questions->links() }}</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Einlösungen')
|
||||||
|
@section('content')
|
||||||
|
@if($pending->count())
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-4">⏳ Warten auf Freigabe</h2>
|
||||||
|
<div class="space-y-3 mb-8">
|
||||||
|
@foreach($pending as $red)
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-amber-200 p-5 flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-3xl">{{ $red->reward->icon }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-slate-800">{{ $red->user->name }} möchte: {{ $red->reward->name }}</div>
|
||||||
|
<div class="text-sm text-slate-500">🪙 {{ $red->points_spent }} Münzen · {{ $red->created_at->diffForHumans() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 shrink-0">
|
||||||
|
<form method="POST" action="{{ route('admin.redemptions.approve',$red) }}">
|
||||||
|
@csrf @method('PATCH')
|
||||||
|
<button class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium">✅ Freigeben</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="{{ route('admin.redemptions.reject',$red) }}" x-data x-on:submit.prevent="if(confirm('Ablehnen?')) $el.submit()">
|
||||||
|
@csrf @method('PATCH')
|
||||||
|
<button class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium">❌ Ablehnen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="bg-green-50 border border-green-200 rounded-xl p-5 mb-8 text-green-700 font-medium">✅ Keine offenen Einlösungen.</div>
|
||||||
|
@endif
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-4">📋 Verlauf</h2>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">Kind</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">Belohnung</th>
|
||||||
|
<th class="text-center px-4 py-3 font-medium text-slate-600">Status</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-slate-600">Münzen</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-slate-600">Datum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
@forelse($resolved as $red)
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 font-medium text-slate-700">{{ $red->user->name }}</td>
|
||||||
|
<td class="px-4 py-3">{{ $red->reward->icon }} {{ $red->reward->name }}</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
<span class="{{ $red->status==='approved' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }} px-2 py-0.5 rounded-full text-xs font-medium">
|
||||||
|
{{ $red->status==='approved' ? 'Freigegeben' : 'Abgelehnt' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right text-amber-600 font-medium">🪙 {{ $red->points_spent }}</td>
|
||||||
|
<td class="px-4 py-3 text-right text-slate-400 text-xs">{{ $red->created_at->format('d.m.Y') }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="px-4 py-6 text-center text-slate-400">Noch keine Einlösungen.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Neue Belohnung')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-lg">
|
||||||
|
<a href="{{ route('admin.rewards.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">Neue Belohnung</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.rewards.store') }}" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Icon</label>
|
||||||
|
<input name="icon" value="{{ old('icon','🎁') }}" required maxlength="4" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-center text-xl focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||||
|
<input name="name" value="{{ old('name') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||||||
|
<input name="description" value="{{ old('description') }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzenpreis</label>
|
||||||
|
<input name="points_cost" type="number" min="1" value="{{ old('points_cost') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Minuten <span class="text-slate-400">(optional)</span></label>
|
||||||
|
<input name="minutes" type="number" min="1" value="{{ old('minutes') }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Belohnung erstellen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Belohnung bearbeiten')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-lg">
|
||||||
|
<a href="{{ route('admin.rewards.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">Belohnung bearbeiten</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.rewards.update',$reward) }}" class="space-y-4">
|
||||||
|
@csrf @method('PUT')
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Icon</label>
|
||||||
|
<input name="icon" value="{{ old('icon',$reward->icon) }}" required maxlength="4" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-center text-xl focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||||
|
<input name="name" value="{{ old('name',$reward->name) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||||||
|
<input name="description" value="{{ old('description',$reward->description) }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzenpreis</label>
|
||||||
|
<input name="points_cost" type="number" min="1" value="{{ old('points_cost',$reward->points_cost) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Minuten</label>
|
||||||
|
<input name="minutes" type="number" min="1" value="{{ old('minutes',$reward->minutes) }}" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input type="checkbox" name="active" id="active" value="1" {{ $reward->active?'checked':'' }} class="accent-violet-600">
|
||||||
|
<label for="active" class="text-sm font-medium text-slate-700">Belohnung aktiv</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Belohnungen')
|
||||||
|
@section('content')
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-700">Belohnungen</h2>
|
||||||
|
<a href="{{ route('admin.rewards.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Belohnung</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
@forelse($rewards as $r)
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<span class="text-3xl">{{ $r->icon }}</span>
|
||||||
|
<span class="{{ $r->active ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500' }} text-xs px-2 py-0.5 rounded-full font-medium">{{ $r->active ? 'Aktiv' : 'Inaktiv' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-slate-800">{{ $r->name }}</div>
|
||||||
|
@if($r->description)<div class="text-xs text-slate-500 mt-0.5">{{ $r->description }}</div>@endif
|
||||||
|
<div class="mt-3 flex items-center justify-between">
|
||||||
|
<span class="font-bold text-amber-600">🪙 {{ $r->points_cost }} Münzen</span>
|
||||||
|
@if($r->minutes)<span class="text-xs text-slate-500">{{ $r->minutes }} Min.</span>@endif
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<a href="{{ route('admin.rewards.edit',$r) }}" class="flex-1 text-center bg-slate-100 hover:bg-slate-200 text-slate-700 py-1.5 rounded-lg text-xs font-medium">Bearbeiten</a>
|
||||||
|
<form method="POST" action="{{ route('admin.rewards.destroy',$r) }}" onsubmit="return confirm('Löschen?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button class="bg-red-50 hover:bg-red-100 text-red-600 py-1.5 px-3 rounded-lg text-xs font-medium">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-slate-400 col-span-3">Noch keine Belohnungen.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Neues Kind')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-lg">
|
||||||
|
<a href="{{ route('admin.users.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">Neues Kind-Konto</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.users.store') }}" class="space-y-4">
|
||||||
|
@csrf
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||||
|
<input name="name" value="{{ old('name') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
|
||||||
|
@error('name')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">E-Mail</label>
|
||||||
|
<input name="email" type="email" value="{{ old('email') }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
|
||||||
|
@error('email')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Passwort</label>
|
||||||
|
<input name="password" type="password" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 focus:border-transparent outline-none">
|
||||||
|
@error('password')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Konto erstellen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Kind bearbeiten')
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-lg">
|
||||||
|
<a href="{{ route('admin.users.index') }}" class="text-sm text-slate-500 hover:text-slate-700 mb-4 inline-block">← Zurück</a>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="font-semibold text-slate-800 mb-5">{{ $user->name }} bearbeiten</h2>
|
||||||
|
<form method="POST" action="{{ route('admin.users.update',$user) }}" class="space-y-4">
|
||||||
|
@csrf @method('PUT')
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||||
|
<input name="name" value="{{ old('name',$user->name) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">E-Mail</label>
|
||||||
|
<input name="email" type="email" value="{{ old('email',$user->email) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Neues Passwort <span class="text-slate-400">(leer = unverändert)</span></label>
|
||||||
|
<input name="password" type="password" class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzen</label>
|
||||||
|
<input name="points" type="number" min="0" value="{{ old('points',$user->points) }}" required class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-violet-500 outline-none">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Speichern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
@section('title','Kinder')
|
||||||
|
@section('content')
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-700">Alle Kinder-Konten</h2>
|
||||||
|
<a href="{{ route('admin.users.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neues Kind</a>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">Name</th>
|
||||||
|
<th class="text-left px-4 py-3 font-medium text-slate-600">E-Mail</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-slate-600">🪙 Münzen</th>
|
||||||
|
<th class="text-right px-4 py-3 font-medium text-slate-600">Antworten</th>
|
||||||
|
<th class="px-4 py-3"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
@forelse($users as $u)
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 font-medium text-slate-800">{{ $u->name }}</td>
|
||||||
|
<td class="px-4 py-3 text-slate-500">{{ $u->email }}</td>
|
||||||
|
<td class="px-4 py-3 text-right font-bold text-amber-600">{{ $u->points }}</td>
|
||||||
|
<td class="px-4 py-3 text-right text-slate-500">{{ $u->attempts_count }}</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<a href="{{ route('admin.users.edit',$u) }}" class="text-violet-600 hover:underline mr-3">Bearbeiten</a>
|
||||||
|
<form method="POST" action="{{ route('admin.users.destroy',$u) }}" class="inline" onsubmit="return confirm('Wirklich löschen?')">
|
||||||
|
@csrf @method('DELETE')
|
||||||
|
<button class="text-red-500 hover:underline">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="px-4 py-8 text-center text-slate-400">Noch keine Kinder angelegt.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
@extends('layouts.child')
|
||||||
|
@section('content')
|
||||||
|
@php $level = auth()->user()->level() @endphp
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="text-5xl mb-2">{{ $level['icon'] }}</div>
|
||||||
|
<h1 class="text-2xl font-bold text-indigo-700">Hallo, {{ auth()->user()->name }}! 👋</h1>
|
||||||
|
<p class="text-slate-500 mt-1">Du bist: <span class="{{ $level['color'] }} font-bold">{{ $level['label'] }}</span></p>
|
||||||
|
@if($streak >= 3)
|
||||||
|
<div class="mt-3 inline-flex items-center gap-2 bg-orange-100 text-orange-700 rounded-full px-4 py-1.5 text-sm font-bold">
|
||||||
|
🔥 {{ $streak }}er-Serie! +5 Bonusmünzen pro Antwort
|
||||||
|
</div>
|
||||||
|
@elseif($streak > 0)
|
||||||
|
<div class="mt-3 inline-flex items-center gap-2 bg-sky-100 text-sky-700 rounded-full px-4 py-1.5 text-sm font-medium">
|
||||||
|
✨ {{ $streak }} richtige Antworten in Folge!
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-8">
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-amber-200 p-5 text-center">
|
||||||
|
<div class="text-4xl font-black text-amber-500">{{ auth()->user()->points }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1 font-medium">🪙 Münzen</div>
|
||||||
|
@if($level['next'])
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-xs text-slate-400 mb-1">Nächstes Level in {{ $level['next'] - auth()->user()->points }} Münzen</div>
|
||||||
|
<div class="bg-slate-200 rounded-full h-2"><div class="bg-amber-400 h-2 rounded-full transition-all" style="width:{{ $level['progress'] }}%"></div></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($pendingRedemptions > 0)
|
||||||
|
<a href="{{ route('rewards.index') }}" class="bg-green-50 border border-green-300 rounded-2xl p-5 text-center hover:bg-green-100 transition">
|
||||||
|
<div class="text-4xl font-black text-green-600">{{ $pendingRedemptions }}</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1 font-medium">🎁 Einlösung wartet</div>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('rewards.index') }}" class="bg-white rounded-2xl shadow-sm border border-slate-200 p-5 text-center hover:border-indigo-300 transition">
|
||||||
|
<div class="text-4xl">🎁</div>
|
||||||
|
<div class="text-sm text-slate-500 mt-1 font-medium">Belohnungen</div>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="font-bold text-slate-700 text-lg mb-4">📚 Fächer</h2>
|
||||||
|
<div class="grid grid-cols-3 gap-4 mb-8">
|
||||||
|
@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
|
||||||
|
<a href="{{ route('learn.quiz', $s->slug) }}" class="bg-gradient-to-br {{ $colors[$s->color] ?? 'from-violet-400 to-purple-500' }} rounded-2xl p-5 text-white text-center hover:scale-105 transition-transform shadow-sm">
|
||||||
|
<div class="text-3xl mb-2">{{ $s->icon }}</div>
|
||||||
|
<div class="font-bold text-sm">{{ $s->name }}</div>
|
||||||
|
<div class="text-white/80 text-xs mt-1">{{ $s->correct }} ✓</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($recentAttempts->count())
|
||||||
|
<h2 class="font-bold text-slate-700 text-lg mb-4">⏱️ Zuletzt gespielt</h2>
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
@foreach($recentAttempts as $a)
|
||||||
|
<div class="flex items-center justify-between px-5 py-3 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-lg">{{ $a->is_correct ? '✅' : '❌' }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-slate-700 line-clamp-1">{{ Str::limit($a->question->question_text, 40) }}</div>
|
||||||
|
<div class="text-xs text-slate-400">{{ $a->question->subject->name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($a->points_earned > 0)<span class="text-xs font-bold text-amber-500">+{{ $a->points_earned }} 🪙</span>@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
@extends('layouts.child')
|
||||||
|
@section('content')
|
||||||
|
<div class="mb-4 flex items-center gap-3">
|
||||||
|
<a href="{{ route('learn.subjects') }}" class="text-slate-400 hover:text-slate-600 text-sm">← Fächer</a>
|
||||||
|
<span class="text-lg">{{ $subject->icon }}</span>
|
||||||
|
<span class="font-bold text-slate-700">{{ $subject->name }}</span>
|
||||||
|
<span class="ml-auto text-sm text-slate-400">{{ $question->difficultyStars() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-2xl shadow-md border border-slate-200 p-6 mb-6">
|
||||||
|
<div class="text-xs text-slate-400 mb-3 font-medium">
|
||||||
|
{{ ['','⭐ Leicht — 5 Münzen','⭐⭐ Mittel — 10 Münzen','⭐⭐⭐ Schwer — 20 Münzen'][$question->difficulty] }}
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold text-slate-800 leading-snug">{{ $question->question_text }}</p>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('learn.answer', $subject->slug) }}">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="question_id" value="{{ $question->id }}">
|
||||||
|
<div class="grid gap-3">
|
||||||
|
@foreach($question->answerOptions as $opt)
|
||||||
|
<label class="cursor-pointer">
|
||||||
|
<input type="radio" name="answer" value="{{ $opt->id }}" class="sr-only peer" required>
|
||||||
|
<div class="bg-white border-2 border-slate-200 rounded-2xl px-5 py-4 text-base font-medium text-slate-700
|
||||||
|
peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700
|
||||||
|
hover:border-indigo-300 hover:bg-slate-50 transition-all">
|
||||||
|
{{ $opt->text }}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="mt-6 w-full bg-indigo-600 hover:bg-indigo-700 text-white text-lg font-bold py-4 rounded-2xl shadow-md transition-colors">
|
||||||
|
Antworten ✓
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@extends('layouts.child')
|
||||||
|
@section('content')
|
||||||
|
<div class="text-center">
|
||||||
|
@if($isRight)
|
||||||
|
<div class="text-7xl mb-4 animate-bounce">🎉</div>
|
||||||
|
<h1 class="text-3xl font-black text-green-600 mb-2">Richtig!</h1>
|
||||||
|
<p class="text-slate-600 mb-4">Super gemacht!</p>
|
||||||
|
<div class="inline-flex items-center gap-2 bg-amber-100 text-amber-700 font-black text-2xl rounded-2xl px-6 py-3 mb-4">
|
||||||
|
+{{ $earned }} 🪙
|
||||||
|
</div>
|
||||||
|
@if($earned > $question->points_value)
|
||||||
|
<p class="text-orange-600 font-bold text-sm mb-4">🔥 Serien-Bonus inklusive!</p>
|
||||||
|
@endif
|
||||||
|
@if($newStreak >= 3)
|
||||||
|
<div class="bg-orange-50 border border-orange-200 rounded-xl px-4 py-2 text-orange-700 font-bold text-sm mb-4 inline-block">
|
||||||
|
🔥 {{ $newStreak }}er-Serie! Weiter so!
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<div class="text-7xl mb-4">😅</div>
|
||||||
|
<h1 class="text-3xl font-black text-red-500 mb-2">Leider falsch</h1>
|
||||||
|
<p class="text-slate-600 mb-4">Die richtige Antwort war:</p>
|
||||||
|
<div class="bg-green-100 border border-green-300 text-green-800 font-bold text-lg rounded-2xl px-6 py-3 mb-4 inline-block">
|
||||||
|
{{ $correct->text }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-6 flex gap-3 justify-center">
|
||||||
|
<a href="{{ route('learn.quiz', $subject->slug) }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-8 py-3 rounded-2xl text-base shadow-md transition-colors">
|
||||||
|
Nächste Frage ▶
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('learn.subjects') }}" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-bold px-6 py-3 rounded-2xl text-base transition-colors">
|
||||||
|
Fächer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 text-slate-500 text-sm">
|
||||||
|
Dein Kontostand: <span class="font-bold text-amber-600">🪙 {{ auth()->user()->fresh()->points }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@extends('layouts.child')
|
||||||
|
@section('content')
|
||||||
|
<h1 class="text-2xl font-bold text-indigo-700 mb-6 text-center">📚 Welches Fach?</h1>
|
||||||
|
<div class="grid gap-5">
|
||||||
|
@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
|
||||||
|
<a href="{{ route('learn.quiz', $s->slug) }}" class="bg-gradient-to-r {{ $colors[$s->color] ?? 'from-violet-400 to-purple-500' }} rounded-2xl p-6 text-white flex items-center justify-between hover:scale-[1.02] transition-transform shadow-md">
|
||||||
|
<div class="flex items-center gap-5">
|
||||||
|
<span class="text-5xl">{{ $s->icon }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="text-2xl font-black">{{ $s->name }}</div>
|
||||||
|
<div class="text-white/80 text-sm mt-0.5">{{ $s->total }} Fragen · {{ $s->correct }} richtig beantwortet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-4xl">▶</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@extends('layouts.child')
|
||||||
|
@section('content')
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-indigo-700">🎁 Belohnungen</h1>
|
||||||
|
<div class="bg-amber-100 text-amber-700 font-black rounded-full px-4 py-2">🪙 {{ auth()->user()->points }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 mb-8">
|
||||||
|
@foreach($rewards as $r)
|
||||||
|
@php $canAfford = auth()->user()->points >= $r->points_cost @endphp
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border {{ $canAfford ? 'border-slate-200 hover:border-indigo-300' : 'border-slate-100 opacity-60' }} p-5 transition-all">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="text-4xl">{{ $r->icon }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="font-bold text-slate-800 text-base">{{ $r->name }}</div>
|
||||||
|
@if($r->description)<div class="text-sm text-slate-500">{{ $r->description }}</div>@endif
|
||||||
|
@if($r->minutes)<div class="text-xs text-slate-400 mt-0.5">⏱ {{ $r->minutes }} Minuten</div>@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right shrink-0">
|
||||||
|
<div class="font-black text-amber-600 text-lg">🪙 {{ $r->points_cost }}</div>
|
||||||
|
@if($canAfford)
|
||||||
|
<form method="POST" action="{{ route('rewards.redeem', $r) }}" onsubmit="return confirm('Möchtest du {{ $r->name }} für {{ $r->points_cost }} Münzen einlösen?')">
|
||||||
|
@csrf
|
||||||
|
<button class="mt-2 bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-4 py-2 rounded-xl text-sm transition-colors">
|
||||||
|
Einlösen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<div class="mt-2 text-xs text-slate-400 font-medium">Noch {{ $r->points_cost - auth()->user()->points }} fehlen</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($history->count())
|
||||||
|
<h2 class="font-bold text-slate-700 text-lg mb-4">📋 Meine Einlösungen</h2>
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
@foreach($history as $red)
|
||||||
|
<div class="flex items-center justify-between px-5 py-3 {{ !$loop->last ? 'border-b border-slate-100' : '' }}">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-xl">{{ $red->reward->icon }}</span>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-slate-700">{{ $red->reward->name }}</div>
|
||||||
|
<div class="text-xs text-slate-400">{{ $red->created_at->format('d.m.Y') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-bold px-2.5 py-1 rounded-full
|
||||||
|
{{ $red->status==='approved' ? 'bg-green-100 text-green-700' :
|
||||||
|
($red->status==='rejected' ? 'bg-red-100 text-red-600' : 'bg-amber-100 text-amber-700') }}">
|
||||||
|
{{ $red->status==='approved' ? '✅ Freigegeben' : ($red->status==='rejected' ? '❌ Abgelehnt' : '⏳ Warten...') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Lernapp Admin</title>
|
||||||
|
@vite(['resources/css/app.css','resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="h-full bg-slate-100 font-sans" x-data="{open:false}">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="hidden md:flex flex-col w-60 bg-slate-900 text-slate-100 shrink-0">
|
||||||
|
<div class="flex items-center gap-3 px-6 py-5 border-b border-slate-700">
|
||||||
|
<span class="text-2xl">🎓</span>
|
||||||
|
<div><div class="font-bold text-white text-sm">Lernapp</div><div class="text-xs text-slate-400">Admin-Bereich</div></div>
|
||||||
|
</div>
|
||||||
|
<nav class="flex-1 px-3 py-4 space-y-1 text-sm">
|
||||||
|
<a href="{{ route('admin.dashboard') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.dashboard') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
|
||||||
|
<span>📊</span> Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.users.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.users.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
|
||||||
|
<span>👦</span> Kinder
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.questions.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.questions.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
|
||||||
|
<span>❓</span> Fragen
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.rewards.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.rewards.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
|
||||||
|
<span>🎁</span> Belohnungen
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.redemptions.index') }}" class="flex items-center gap-3 px-3 py-2 rounded-lg {{ request()->routeIs('admin.redemptions.*') ? 'bg-violet-600 text-white' : 'text-slate-300 hover:bg-slate-800' }}">
|
||||||
|
<span>✅</span> Einlösungen
|
||||||
|
@php $p = \App\Models\RewardRedemption::where('status','pending')->count() @endphp
|
||||||
|
@if($p > 0)<span class="ml-auto bg-red-500 text-white text-xs rounded-full px-1.5">{{ $p }}</span>@endif
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="px-3 py-4 border-t border-slate-700">
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
<button class="flex items-center gap-3 px-3 py-2 w-full text-sm text-slate-400 hover:text-white rounded-lg hover:bg-slate-800">
|
||||||
|
<span>🚪</span> Abmelden
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<!-- Main -->
|
||||||
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between">
|
||||||
|
<h1 class="font-semibold text-slate-800">@yield('title','Dashboard')</h1>
|
||||||
|
<span class="text-sm text-slate-500">{{ auth()->user()->name }}</span>
|
||||||
|
</header>
|
||||||
|
<main class="flex-1 overflow-y-auto p-6">
|
||||||
|
@if(session('success'))<div class="mb-4 bg-green-100 border border-green-300 text-green-800 rounded-lg px-4 py-3 text-sm">{{ session('success') }}</div>@endif
|
||||||
|
@if(session('error'))<div class="mb-4 bg-red-100 border border-red-300 text-red-800 rounded-lg px-4 py-3 text-sm">{{ session('error') }}</div>@endif
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Lernapp</title>
|
||||||
|
@vite(['resources/css/app.css','resources/js/app.js'])
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-gradient-to-br from-sky-100 to-indigo-100 font-sans">
|
||||||
|
<nav class="bg-white shadow-sm sticky top-0 z-10">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||||
|
<a href="{{ route('dashboard') }}" class="flex items-center gap-2 font-bold text-indigo-600 text-lg">
|
||||||
|
🎓 Lernapp
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<a href="{{ route('learn.subjects') }}" class="text-sm font-medium text-slate-600 hover:text-indigo-600">Lernen</a>
|
||||||
|
<a href="{{ route('rewards.index') }}" class="text-sm font-medium text-slate-600 hover:text-indigo-600">🪙 Belohnungen</a>
|
||||||
|
<div class="flex items-center gap-1 bg-amber-100 text-amber-700 font-bold rounded-full px-3 py-1 text-sm">
|
||||||
|
🪙 {{ auth()->user()->points }}
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('logout') }}">@csrf
|
||||||
|
<button class="text-xs text-slate-400 hover:text-slate-600">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main class="max-w-3xl mx-auto px-4 py-8">
|
||||||
|
@if(session('success'))<div class="mb-4 bg-green-100 border border-green-300 text-green-800 rounded-xl px-4 py-3 text-sm font-medium">✅ {{ session('success') }}</div>@endif
|
||||||
|
@if(session('error'))<div class="mb-4 bg-red-100 border border-red-300 text-red-800 rounded-xl px-4 py-3 text-sm font-medium">❌ {{ session('error') }}</div>@endif
|
||||||
|
@if(session('info'))<div class="mb-4 bg-blue-100 border border-blue-300 text-blue-800 rounded-xl px-4 py-3 text-sm font-medium">ℹ️ {{ session('info') }}</div>@endif
|
||||||
|
@yield('content')
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+27
-12
@@ -1,20 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
|
use App\Http\Controllers\Admin;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\Child;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', fn() => redirect()->route('dashboard'));
|
||||||
return view('welcome');
|
|
||||||
|
// 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 () {
|
// ── CHILD ──────────────────────────────────────────────────
|
||||||
return view('dashboard');
|
Route::middleware(['auth','child'])->group(function () {
|
||||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
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::middleware('auth')->group(function () {
|
Route::post('lernen/{subject:slug}/antwort', [\App\Http\Controllers\Child\LearnController::class,'answer']) ->name('learn.answer');
|
||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get ('belohnungen', [\App\Http\Controllers\Child\RewardController::class,'index']) ->name('rewards.index');
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::post('belohnungen/{reward}/einloesen', [\App\Http\Controllers\Child\RewardController::class,'redeem']) ->name('rewards.redeem');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user