6c6dd26823
- Quizzes table with questions, answer options, attempts, answers - Question types: multiple_choice, exclusion, true_false, free_text - Progressive scoring: [1,1,2,2,3,3,4,6,8,10] = max 40 per quiz - Alpine.js countdown timer per question with auto-submit on timeout - Admin: CRUD for quizzes + per-question editor, JSON export/import - Child: quiz overview with best scores, question view, result breakdown - Nav: Quiz link in child header and admin sidebar
97 lines
4.2 KiB
PHP
97 lines
4.2 KiB
PHP
<?php
|
|
namespace App\Http\Controllers\Child;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\{Quiz, QuizAttempt, QuizAttemptAnswer, QuizQuestion, Subject};
|
|
use Illuminate\Http\Request;
|
|
|
|
class QuizController extends Controller {
|
|
|
|
public function index() {
|
|
$subjects = Subject::all()->keyBy('id');
|
|
$quizzes = Quiz::with('subject')->where('active',true)
|
|
->withCount('questions')->get()->groupBy('subject_id');
|
|
$bestScores = QuizAttempt::where('user_id',auth()->id())
|
|
->where('status','completed')
|
|
->selectRaw('quiz_id, MAX(score) as best')
|
|
->groupBy('quiz_id')->pluck('best','quiz_id');
|
|
return view('child.quiz.index', compact('subjects','quizzes','bestScores'));
|
|
}
|
|
|
|
public function start(Quiz $quiz) {
|
|
abort_unless($quiz->active, 403);
|
|
$attempt = QuizAttempt::firstOrCreate(
|
|
['user_id'=>auth()->id(),'quiz_id'=>$quiz->id,'status'=>'in_progress'],
|
|
['score'=>0,'current_question'=>0,'started_at'=>now()]
|
|
);
|
|
return redirect()->route('quiz.question', $attempt);
|
|
}
|
|
|
|
public function question(QuizAttempt $attempt) {
|
|
abort_unless($attempt->user_id === auth()->id(), 403);
|
|
if ($attempt->status === 'completed') return redirect()->route('quiz.result', $attempt);
|
|
$question = $attempt->quiz->questions()->skip($attempt->current_question)->first();
|
|
$pointsIfCorrect = QuizAttempt::POINTS[$attempt->current_question];
|
|
return view('child.quiz.question', compact('attempt','question','pointsIfCorrect'));
|
|
}
|
|
|
|
public function answer(Request $r, QuizAttempt $attempt) {
|
|
abort_unless($attempt->user_id === auth()->id(), 403);
|
|
abort_if($attempt->status === 'completed', 400);
|
|
|
|
$question = $attempt->quiz->questions()->skip($attempt->current_question)->first();
|
|
$possible = QuizAttempt::POINTS[$attempt->current_question];
|
|
$timedOut = $r->input('timeout') === '1';
|
|
$answerGiven = $r->input('answer','');
|
|
$isCorrect = false;
|
|
$pointsEarned = 0;
|
|
|
|
if (!$timedOut && $answerGiven !== '') {
|
|
$isCorrect = $this->check($question, $answerGiven);
|
|
$pointsEarned = $isCorrect ? $possible : 0;
|
|
}
|
|
|
|
QuizAttemptAnswer::create([
|
|
'quiz_attempt_id' => $attempt->id,
|
|
'quiz_question_id' => $question->id,
|
|
'answer_given' => $timedOut ? '__timeout__' : $answerGiven,
|
|
'is_correct' => $isCorrect,
|
|
'points_earned' => $pointsEarned,
|
|
]);
|
|
|
|
session()->flash('last_answer', [
|
|
'correct' => $isCorrect,
|
|
'points' => $pointsEarned,
|
|
'possible' => $possible,
|
|
'timeout' => $timedOut,
|
|
]);
|
|
|
|
$next = $attempt->current_question + 1;
|
|
$totalQuestions = $attempt->quiz->questions()->count();
|
|
|
|
if ($next >= $totalQuestions || $next >= 10) {
|
|
$total = QuizAttemptAnswer::where('quiz_attempt_id',$attempt->id)->sum('points_earned');
|
|
$attempt->update(['score'=>$total,'current_question'=>$next,'status'=>'completed','completed_at'=>now()]);
|
|
auth()->user()->increment('points', $total);
|
|
return redirect()->route('quiz.result', $attempt);
|
|
}
|
|
$attempt->update(['current_question'=>$next]);
|
|
return redirect()->route('quiz.question', $attempt);
|
|
}
|
|
|
|
public function result(QuizAttempt $attempt) {
|
|
abort_unless($attempt->user_id === auth()->id(), 403);
|
|
abort_unless($attempt->status === 'completed', 400);
|
|
$answers = $attempt->answers()->with('question.answerOptions')->get();
|
|
return view('child.quiz.result', compact('attempt','answers'));
|
|
}
|
|
|
|
private function check(QuizQuestion $q, string $answer): bool {
|
|
return match($q->type) {
|
|
'multiple_choice','exclusion' => ($opt = $q->answerOptions()->where('is_correct',true)->first())
|
|
&& (string)$opt->id === $answer,
|
|
'true_false','free_text' => strtolower(trim($answer)) === strtolower(trim((string)$q->correct_answer)),
|
|
default => false,
|
|
};
|
|
}
|
|
}
|