Add grade/Klasse system: assign class levels to users, questions, and quizzes
- users.grade: set per child in admin (Klasse 1–10) - quizzes.grade, questions.grade: optional target class (null = all) - Children only see content matching their grade or without grade set - Admin views show grade badge in user list, quiz list, questions list - Quiz create/edit and user create/edit have Klasse dropdown
This commit is contained in:
@@ -18,8 +18,8 @@ class QuizController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $r) {
|
public function store(Request $r) {
|
||||||
$r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500']);
|
$r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500','grade'=>'nullable|integer|min:1|max:13']);
|
||||||
$quiz = Quiz::create($r->only('title','subject_id','description') + ['active'=>true]);
|
$quiz = Quiz::create($r->only('title','subject_id','description') + ['active'=>true,'grade'=>$r->grade ?: null]);
|
||||||
return redirect()->route('admin.quizzes.edit', $quiz)->with('success','Quiz erstellt – jetzt Fragen hinzufügen.');
|
return redirect()->route('admin.quizzes.edit', $quiz)->with('success','Quiz erstellt – jetzt Fragen hinzufügen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +30,8 @@ class QuizController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $r, Quiz $quiz) {
|
public function update(Request $r, Quiz $quiz) {
|
||||||
$r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500']);
|
$r->validate(['title'=>'required|string|max:120','subject_id'=>'required|exists:subjects,id','description'=>'nullable|string|max:500','grade'=>'nullable|integer|min:1|max:13']);
|
||||||
$quiz->update($r->only('title','subject_id','description') + ['active'=>$r->boolean('active')]);
|
$quiz->update($r->only('title','subject_id','description') + ['active'=>$r->boolean('active'),'grade'=>$r->grade ?: null]);
|
||||||
return back()->with('success','Quiz gespeichert.');
|
return back()->with('success','Quiz gespeichert.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class UserController extends Controller {
|
|||||||
'name' => 'required|string|max:60',
|
'name' => 'required|string|max:60',
|
||||||
'email' => 'required|email|unique:users',
|
'email' => 'required|email|unique:users',
|
||||||
'password' => 'required|min:6',
|
'password' => 'required|min:6',
|
||||||
|
'grade' => 'nullable|integer|min:1|max:13',
|
||||||
]);
|
]);
|
||||||
User::create([
|
User::create([
|
||||||
'name' => $r->name,
|
'name' => $r->name,
|
||||||
@@ -22,6 +23,7 @@ class UserController extends Controller {
|
|||||||
'password' => Hash::make($r->password),
|
'password' => Hash::make($r->password),
|
||||||
'role' => 'child',
|
'role' => 'child',
|
||||||
'points' => 0,
|
'points' => 0,
|
||||||
|
'grade' => $r->grade ?: null,
|
||||||
]);
|
]);
|
||||||
return redirect()->route('admin.users.index')->with('success','Kind-Konto erstellt.');
|
return redirect()->route('admin.users.index')->with('success','Kind-Konto erstellt.');
|
||||||
}
|
}
|
||||||
@@ -32,8 +34,9 @@ class UserController extends Controller {
|
|||||||
'email' => 'required|email|unique:users,email,'.$user->id,
|
'email' => 'required|email|unique:users,email,'.$user->id,
|
||||||
'password' => 'nullable|min:6',
|
'password' => 'nullable|min:6',
|
||||||
'points' => 'required|integer|min:0',
|
'points' => 'required|integer|min:0',
|
||||||
|
'grade' => 'nullable|integer|min:1|max:13',
|
||||||
]);
|
]);
|
||||||
$user->fill(['name'=>$r->name,'email'=>$r->email,'points'=>$r->points]);
|
$user->fill(['name'=>$r->name,'email'=>$r->email,'points'=>$r->points,'grade'=>$r->grade ?: null]);
|
||||||
if ($r->filled('password')) $user->password = Hash::make($r->password);
|
if ($r->filled('password')) $user->password = Hash::make($r->password);
|
||||||
$user->save();
|
$user->save();
|
||||||
return redirect()->route('admin.users.index')->with('success','Gespeichert.');
|
return redirect()->route('admin.users.index')->with('success','Gespeichert.');
|
||||||
|
|||||||
@@ -22,14 +22,24 @@ class LearnController extends Controller {
|
|||||||
$answeredToday = QuestionAttempt::where('user_id',$user->id)
|
$answeredToday = QuestionAttempt::where('user_id',$user->id)
|
||||||
->whereDate('created_at', today())
|
->whereDate('created_at', today())
|
||||||
->pluck('question_id');
|
->pluck('question_id');
|
||||||
|
$grade = $user->grade;
|
||||||
$question = $subject->activeQuestions()
|
$question = $subject->activeQuestions()
|
||||||
->whereNotIn('id', $answeredToday)
|
->whereNotIn('id', $answeredToday)
|
||||||
|
->where(function($q) use ($grade) {
|
||||||
|
$q->whereNull('grade');
|
||||||
|
if ($grade) $q->orWhere('grade', $grade);
|
||||||
|
})
|
||||||
->with('answerOptions')
|
->with('answerOptions')
|
||||||
->inRandomOrder()
|
->inRandomOrder()
|
||||||
->first();
|
->first();
|
||||||
// Falls alle beantwortet: ganz random
|
// Falls alle beantwortet: ganz random
|
||||||
if (!$question) {
|
if (!$question) {
|
||||||
$question = $subject->activeQuestions()->with('answerOptions')->inRandomOrder()->first();
|
$question = $subject->activeQuestions()
|
||||||
|
->where(function($q) use ($grade) {
|
||||||
|
$q->whereNull('grade');
|
||||||
|
if ($grade) $q->orWhere('grade', $grade);
|
||||||
|
})
|
||||||
|
->with('answerOptions')->inRandomOrder()->first();
|
||||||
}
|
}
|
||||||
if (!$question) {
|
if (!$question) {
|
||||||
return redirect()->route('learn.subjects')->with('info','Noch keine Fragen für dieses Fach.');
|
return redirect()->route('learn.subjects')->with('info','Noch keine Fragen für dieses Fach.');
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ class QuizController extends Controller {
|
|||||||
|
|
||||||
public function index() {
|
public function index() {
|
||||||
$subjects = Subject::all()->keyBy('id');
|
$subjects = Subject::all()->keyBy('id');
|
||||||
|
$grade = auth()->user()->grade;
|
||||||
$quizzes = Quiz::with('subject')->where('active',true)
|
$quizzes = Quiz::with('subject')->where('active',true)
|
||||||
|
->where(function($q) use ($grade) {
|
||||||
|
$q->whereNull('grade');
|
||||||
|
if ($grade) $q->orWhere('grade', $grade);
|
||||||
|
})
|
||||||
->withCount('questions')->get()->groupBy('subject_id');
|
->withCount('questions')->get()->groupBy('subject_id');
|
||||||
$bestScores = QuizAttempt::where('user_id',auth()->id())
|
$bestScores = QuizAttempt::where('user_id',auth()->id())
|
||||||
->where('status','completed')
|
->where('status','completed')
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
|||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
class User extends Authenticatable {
|
class User extends Authenticatable {
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
protected $fillable = ['name','email','password','role','points'];
|
protected $fillable = ['name','email','password','role','points','grade'];
|
||||||
protected $hidden = ['password','remember_token'];
|
protected $hidden = ['password','remember_token'];
|
||||||
protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed'];
|
protected $casts = ['email_verified_at' => 'datetime', 'password' => 'hashed'];
|
||||||
|
|
||||||
|
|||||||
@@ -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::table('users', fn(Blueprint $t) => $t->unsignedTinyInteger('grade')->nullable()->after('role'));
|
||||||
|
Schema::table('quizzes', fn(Blueprint $t) => $t->unsignedTinyInteger('grade')->nullable()->after('active'));
|
||||||
|
Schema::table('questions', fn(Blueprint $t) => $t->unsignedTinyInteger('grade')->nullable()->after('active'));
|
||||||
|
}
|
||||||
|
public function down(): void {
|
||||||
|
Schema::table('users', fn(Blueprint $t) => $t->dropColumn('grade'));
|
||||||
|
Schema::table('quizzes', fn(Blueprint $t) => $t->dropColumn('grade'));
|
||||||
|
Schema::table('questions', fn(Blueprint $t) => $t->dropColumn('grade'));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
<thead class="bg-slate-50 border-b border-slate-200">
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
<tr>
|
<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">Frage</th>
|
||||||
|
<th class="text-center px-4 py-3 font-medium text-slate-600">Klasse</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">Schwierigkeit</th>
|
||||||
<th class="text-center px-4 py-3 font-medium text-slate-600 hidden sm:table-cell">Aktiv</th>
|
<th class="text-center px-4 py-3 font-medium text-slate-600 hidden sm:table-cell">Aktiv</th>
|
||||||
<th class="px-4 py-3"></th>
|
<th class="px-4 py-3"></th>
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
<td class="px-4 py-3 text-slate-700 max-w-xs">
|
<td class="px-4 py-3 text-slate-700 max-w-xs">
|
||||||
<span class="line-clamp-2">{{ $q->question_text }}</span>
|
<span class="line-clamp-2">{{ $q->question_text }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center text-xs text-slate-500">{{ $q->grade ? "Kl.".$q->grade : "–" }}</td>
|
||||||
<td class="px-4 py-3 text-center">{{ $q->difficultyStars() }}</td>
|
<td class="px-4 py-3 text-center">{{ $q->difficultyStars() }}</td>
|
||||||
<td class="px-4 py-3 text-center hidden sm:table-cell">{{ $q->active ? '✅' : '⏸️' }}</td>
|
<td class="px-4 py-3 text-center hidden sm:table-cell">{{ $q->active ? '✅' : '⏸️' }}</td>
|
||||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||||
|
|||||||
@@ -21,6 +21,15 @@
|
|||||||
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung (optional)</label>
|
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung (optional)</label>
|
||||||
<textarea name="description" 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('description') }}</textarea>
|
<textarea name="description" 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('description') }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Zielklasse <span class="text-slate-400 font-normal">(leer = für alle Kinder)</span></label>
|
||||||
|
<select name="grade" 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="">Alle Klassen</option>
|
||||||
|
@for($g=1;$g<=10;$g++)
|
||||||
|
<option value="{{ $g }}" {{ old('grade','')==$g?'selected':'' }}>Klasse {{ $g }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Erstellen & Fragen hinzufügen →</button>
|
<button type="submit" class="w-full bg-violet-600 hover:bg-violet-700 text-white py-2 rounded-lg font-medium text-sm">Erstellen & Fragen hinzufügen →</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,15 @@
|
|||||||
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
<label class="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||||||
<textarea name="description" 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">{{ $quiz->description }}</textarea>
|
<textarea name="description" 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">{{ $quiz->description }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Zielklasse <span class="text-slate-400 font-normal">(leer = für alle Kinder)</span></label>
|
||||||
|
<select name="grade" 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="">Alle Klassen</option>
|
||||||
|
@for($g=1;$g<=10;$g++)
|
||||||
|
<option value="{{ $g }}" {{ old('grade',$quiz->grade)==$g?'selected':'' }}>Klasse {{ $g }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" name="active" id="active" value="1" {{ $quiz->active?'checked':'' }} class="w-4 h-4 text-violet-600">
|
<input type="checkbox" name="active" id="active" value="1" {{ $quiz->active?'checked':'' }} class="w-4 h-4 text-violet-600">
|
||||||
<label for="active" class="text-sm text-slate-700">Quiz aktiv (für Kinder sichtbar)</label>
|
<label for="active" class="text-sm text-slate-700">Quiz aktiv (für Kinder sichtbar)</label>
|
||||||
|
|||||||
@@ -36,7 +36,9 @@
|
|||||||
@forelse($quizzes as $quiz)
|
@forelse($quizzes as $quiz)
|
||||||
<tr class="hover:bg-slate-50">
|
<tr class="hover:bg-slate-50">
|
||||||
<td class="px-4 py-3 text-slate-600">{{ $quiz->subject->icon }} {{ $quiz->subject->name }}</td>
|
<td class="px-4 py-3 text-slate-600">{{ $quiz->subject->icon }} {{ $quiz->subject->name }}</td>
|
||||||
<td class="px-4 py-3 font-medium text-slate-800">{{ $quiz->title }}</td>
|
<td class="px-4 py-3 font-medium text-slate-800">{{ $quiz->title }}
|
||||||
|
@if($quiz->grade)<span class="ml-2 text-xs bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded-full">Kl.{{ $quiz->grade }}</span>@endif
|
||||||
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span class="{{ $quiz->questions_count < 10 ? 'text-amber-600' : 'text-green-600' }} font-medium">
|
<span class="{{ $quiz->questions_count < 10 ? 'text-amber-600' : 'text-green-600' }} font-medium">
|
||||||
{{ $quiz->questions_count }}/10
|
{{ $quiz->questions_count }}/10
|
||||||
|
|||||||
@@ -22,6 +22,15 @@
|
|||||||
<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">
|
<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
|
@error('password')<p class="text-red-500 text-xs mt-1">{{ $message }}</p>@enderror
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Klasse</label>
|
||||||
|
<select name="grade" 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="">– Keine Klasse –</option>
|
||||||
|
@for($g=1;$g<=10;$g++)
|
||||||
|
<option value="{{ $g }}" {{ old('grade','')==$g?'selected':'' }}>Klasse {{ $g }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
</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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,15 @@
|
|||||||
<label class="block text-sm font-medium text-slate-700 mb-1">🪙 Münzen</label>
|
<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">
|
<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>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Klasse</label>
|
||||||
|
<select name="grade" 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="">– Keine Klasse –</option>
|
||||||
|
@for($g=1;$g<=10;$g++)
|
||||||
|
<option value="{{ $g }}" {{ old('grade',$user->grade)==$g?'selected':'' }}>Klasse {{ $g }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
</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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<tr>
|
<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">Name</th>
|
||||||
<th class="text-left px-4 py-3 font-medium text-slate-600">E-Mail</th>
|
<th class="text-left px-4 py-3 font-medium text-slate-600">E-Mail</th>
|
||||||
|
<th class="text-center px-4 py-3 font-medium text-slate-600">Klasse</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">🪙 Münzen</th>
|
||||||
<th class="text-right px-4 py-3 font-medium text-slate-600">Antworten</th>
|
<th class="text-right px-4 py-3 font-medium text-slate-600">Antworten</th>
|
||||||
<th class="px-4 py-3"></th>
|
<th class="px-4 py-3"></th>
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
<tr class="hover:bg-slate-50">
|
<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 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-slate-500">{{ $u->email }}</td>
|
||||||
|
<td class="px-4 py-3 text-center text-sm text-slate-500">{{ $u->grade ? "Klasse ".$u->grade : "–" }}</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 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 text-slate-500">{{ $u->attempts_count }}</td>
|
||||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||||
|
|||||||
Reference in New Issue
Block a user