190 lines
5.7 KiB
PHP
190 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
use App\Models\Attempt;
|
|
use App\Models\Answer;
|
|
use App\Models\Quiz;
|
|
use App\Models\Option;
|
|
use App\Models\AdminLog;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Inertia;
|
|
|
|
class AttemptController extends Controller
|
|
{
|
|
public function destroy(Attempt $attempt)
|
|
{
|
|
$this->authorizeAdmin();
|
|
|
|
$candidateName = $attempt->candidate->user->name;
|
|
// Bypass tenant scope: admin may delete attempts for cross-tenant quizzes
|
|
$quiz = Quiz::withoutGlobalScopes()->find($attempt->quiz_id);
|
|
$quizTitle = $quiz?->title ?? "Quiz #{$attempt->quiz_id}";
|
|
|
|
DB::transaction(function () use ($attempt, $candidateName, $quizTitle) {
|
|
// Log the action
|
|
AdminLog::create([
|
|
'user_id' => auth()->id(),
|
|
'action' => 'DELETE_ATTEMPT',
|
|
'description' => "Suppression du test '{$quizTitle}' pour le candidat '{$candidateName}'."
|
|
]);
|
|
|
|
// Delete attempt (cascades to answers)
|
|
$attempt->delete();
|
|
|
|
// Re-evaluate candidate status if needed
|
|
$candidate = $attempt->candidate;
|
|
if ($candidate->attempts()->count() === 0) {
|
|
$candidate->update(['status' => 'en_attente']);
|
|
}
|
|
});
|
|
|
|
return back()->with('success', 'Tentative de test supprimée et action journalisée.');
|
|
}
|
|
|
|
private function authorizeAdmin()
|
|
{
|
|
if (!auth()->user()->isAdmin()) {
|
|
abort(403);
|
|
}
|
|
}
|
|
public function show(Quiz $quiz)
|
|
{
|
|
$candidate = auth()->user()->candidate;
|
|
|
|
if (!$candidate) {
|
|
abort(403, 'Seuls les candidats peuvent passer des quiz.');
|
|
}
|
|
|
|
$attempt = Attempt::where('candidate_id', $candidate->id)
|
|
->where('quiz_id', $quiz->id)
|
|
->first();
|
|
|
|
if ($attempt && $attempt->finished_at) {
|
|
return Inertia::render('Candidate/Thanks');
|
|
}
|
|
|
|
if (!$attempt) {
|
|
$attempt = Attempt::create([
|
|
'candidate_id' => $candidate->id,
|
|
'quiz_id' => $quiz->id,
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$candidate->update(['status' => 'en_cours']);
|
|
}
|
|
|
|
// Reload quiz with questions FRESHLY (avoid any cached state from model binding)
|
|
$quizData = Quiz::with(['questions' => function($q) {
|
|
$q->orderBy('id')->with('options');
|
|
}])
|
|
->find($quiz->id);
|
|
|
|
return Inertia::render('Candidate/QuizInterface', [
|
|
'quiz' => $quizData,
|
|
'attempt' => $attempt->load('answers'),
|
|
]);
|
|
}
|
|
|
|
public function saveAnswer(Request $request, Attempt $attempt)
|
|
{
|
|
// Security: Verify the authenticated user owns this attempt
|
|
$candidate = auth()->user()->candidate;
|
|
if (!$candidate || $attempt->candidate_id !== $candidate->id) {
|
|
abort(403, 'You are not authorized to submit answers for this attempt.');
|
|
}
|
|
|
|
$request->validate([
|
|
'question_id' => 'required|exists:questions,id',
|
|
'option_id' => 'nullable|exists:options,id',
|
|
'text_content' => 'nullable|string',
|
|
]);
|
|
|
|
// Extra guard: prevent answering a finished attempt
|
|
if ($attempt->finished_at) {
|
|
return response()->json(['error' => 'This attempt is already finished.'], 403);
|
|
}
|
|
|
|
Answer::updateOrCreate(
|
|
[
|
|
'attempt_id' => $attempt->id,
|
|
'question_id' => $request->question_id,
|
|
],
|
|
[
|
|
'option_id' => $request->option_id,
|
|
'text_content' => $request->text_content,
|
|
]
|
|
);
|
|
|
|
return response()->json(['status' => 'success']);
|
|
}
|
|
|
|
public function finish(Attempt $attempt)
|
|
{
|
|
// Security: Verify the authenticated user owns this attempt
|
|
$candidate = auth()->user()->candidate;
|
|
if (!$candidate || $attempt->candidate_id !== $candidate->id) {
|
|
abort(403, 'You are not authorized to finish this attempt.');
|
|
}
|
|
|
|
if ($attempt->finished_at) {
|
|
return redirect()->route('dashboard');
|
|
}
|
|
|
|
$this->recalculateScore($attempt);
|
|
|
|
$attempt->update([
|
|
'finished_at' => now(),
|
|
]);
|
|
|
|
$attempt->candidate->update(['status' => 'termine']);
|
|
|
|
return redirect()->route('dashboard');
|
|
}
|
|
|
|
public function updateAnswerScore(Request $request, Answer $answer)
|
|
{
|
|
$this->authorizeAdmin();
|
|
|
|
$request->validate([
|
|
'score' => 'required|numeric|min:0'
|
|
]);
|
|
|
|
$answer->update(['score' => $request->score]);
|
|
|
|
$this->recalculateScore($answer->attempt);
|
|
|
|
return back()->with('success', 'Note mise à jour et score total recalculé.');
|
|
}
|
|
|
|
private function recalculateScore(Attempt $attempt)
|
|
{
|
|
$attempt->load(['quiz.questions.options', 'answers.option']);
|
|
|
|
$score = 0;
|
|
$maxScore = 0;
|
|
|
|
foreach ($attempt->quiz->questions as $question) {
|
|
$maxScore += $question->points;
|
|
$userAnswer = $attempt->answers->where('question_id', $question->id)->first();
|
|
|
|
if ($userAnswer) {
|
|
if ($question->type === 'qcm') {
|
|
if ($userAnswer->option && $userAnswer->option->is_correct) {
|
|
$score += $question->points;
|
|
}
|
|
} else if ($question->type === 'open') {
|
|
$score += (float) $userAnswer->score;
|
|
}
|
|
}
|
|
}
|
|
|
|
$attempt->update([
|
|
'score' => $score,
|
|
'max_score' => $maxScore,
|
|
]);
|
|
}
|
|
}
|