216 lines
7.3 KiB
PHP
216 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Http\Requests\Session\UpdateSessionRequest;
|
|
use App\Models\Session;
|
|
use App\Services\ActivityLogger;
|
|
use App\Services\ScoringService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
final class SessionController extends Controller
|
|
{
|
|
/**
|
|
* Create a new session for the selected category.
|
|
*/
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$session = Session::create([
|
|
'user_id' => auth()->id(),
|
|
'category_id' => $request->input('category_id'),
|
|
'screening_id' => $request->input('screening_id'),
|
|
'status' => 'in_progress',
|
|
]);
|
|
|
|
ActivityLogger::log('session_started', auth()->id(), sessionId: $session->id, categoryId: (int) $request->input('category_id'), metadata: ['category_id' => $request->input('category_id')]);
|
|
|
|
return redirect()->route('sessions.show', $session);
|
|
}
|
|
|
|
/**
|
|
* Display the session questionnaire with category, question groups, questions, and existing answers.
|
|
*/
|
|
public function show(Session $session): Response
|
|
{
|
|
$session->load('category', 'user');
|
|
|
|
$questionGroups = $session->category
|
|
->questionGroups()
|
|
->with(['questions' => fn ($q) => $q->orderBy('sort_order')])
|
|
->orderBy('sort_order')
|
|
->get();
|
|
|
|
ActivityLogger::log('step_viewed', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: ['question_group_id' => $questionGroups->first()?->id]);
|
|
|
|
$answers = $session->answers()->get()->keyBy('question_id');
|
|
|
|
$scoringService = new ScoringService;
|
|
$score = $scoringService->calculateScore($session);
|
|
|
|
return Inertia::render('Session/Show', [
|
|
'session' => $session,
|
|
'questionGroups' => $questionGroups,
|
|
'answers' => $answers,
|
|
'score' => $score,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Save session basic info, answers, and additional comments.
|
|
*/
|
|
public function update(UpdateSessionRequest $request, Session $session): RedirectResponse
|
|
{
|
|
$validated = $request->validated();
|
|
|
|
if (Arr::has($validated, 'answers')) {
|
|
$this->saveAnswers($session, Arr::get($validated, 'answers'));
|
|
}
|
|
|
|
if (Arr::has($validated, 'additional_comments')) {
|
|
$session->update(['additional_comments' => Arr::get($validated, 'additional_comments')]);
|
|
}
|
|
|
|
if ($request->boolean('complete')) {
|
|
return $this->completeSession($session);
|
|
}
|
|
|
|
return back();
|
|
}
|
|
|
|
/**
|
|
* Save or update answers for the session using composite key upsert.
|
|
*/
|
|
private function saveAnswers(Session $session, array $answers): void
|
|
{
|
|
foreach ($answers as $questionId => $answer) {
|
|
$session->answers()->updateOrCreate(
|
|
['question_id' => (int) $questionId],
|
|
[
|
|
'value' => Arr::get($answer, 'value'),
|
|
'text_value' => Arr::get($answer, 'text_value'),
|
|
]
|
|
);
|
|
|
|
ActivityLogger::log('answer_saved', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: [
|
|
'question_id' => (int) $questionId,
|
|
'value' => Arr::get($answer, 'value'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Complete the session by calculating final score and result.
|
|
*/
|
|
private function completeSession(Session $session): RedirectResponse
|
|
{
|
|
$this->validateSessionCompletion($session);
|
|
|
|
$scoringService = new ScoringService;
|
|
$score = $scoringService->calculateScore($session);
|
|
$result = $scoringService->determineResult($score);
|
|
|
|
$session->update([
|
|
'score' => $score,
|
|
'result' => $result,
|
|
'status' => 'completed',
|
|
'completed_at' => now(),
|
|
]);
|
|
|
|
ActivityLogger::log('session_completed', auth()->id(), sessionId: $session->id, categoryId: $session->category_id, metadata: ['category_id' => $session->category_id, 'score' => $score, 'result' => $result]);
|
|
|
|
return redirect()->route('sessions.result', $session);
|
|
}
|
|
|
|
/**
|
|
* Validate that all required fields are answered before session completion.
|
|
*/
|
|
private function validateSessionCompletion(Session $session): void
|
|
{
|
|
$session->load(['category.questionGroups.questions', 'answers']);
|
|
|
|
$errors = [];
|
|
|
|
foreach ($session->category->questionGroups as $questionGroup) {
|
|
foreach ($questionGroup->questions as $question) {
|
|
$answer = $session->answers->firstWhere('question_id', $question->id);
|
|
|
|
$this->validateRadioAnswer($question, $answer, $errors);
|
|
$this->validateDetailsAnswer($question, $answer, $errors);
|
|
}
|
|
}
|
|
|
|
if (Arr::exists($errors, 0)) {
|
|
throw ValidationException::withMessages([
|
|
'complete' => $errors,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate that radio button questions have an answer selected.
|
|
*/
|
|
private function validateRadioAnswer($question, $answer, array &$errors): void
|
|
{
|
|
$hasRadioButtons = $question->has_yes || $question->has_no || $question->has_na;
|
|
|
|
if ($hasRadioButtons && (! $answer || $answer->value === null)) {
|
|
$errors[] = "Question '{$question->text}' requires an answer.";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate that questions with required details have text values provided.
|
|
*/
|
|
private function validateDetailsAnswer($question, $answer, array &$errors): void
|
|
{
|
|
$details = $question->details;
|
|
$hasRadioButtons = $question->has_yes || $question->has_no || $question->has_na;
|
|
|
|
if ($details === 'required') {
|
|
if (! $answer || empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
|
$errors[] = "Question '{$question->text}' requires details to be provided.";
|
|
}
|
|
}
|
|
|
|
if ($details === 'req_on_yes' && $answer && $answer->value === 'yes') {
|
|
if (empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
|
$errors[] = "Question '{$question->text}' requires details when answered 'Yes'.";
|
|
}
|
|
}
|
|
|
|
if ($details === 'req_on_no' && $answer && $answer->value === 'no') {
|
|
if (empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
|
$errors[] = "Question '{$question->text}' requires details when answered 'No'.";
|
|
}
|
|
}
|
|
|
|
if (! $hasRadioButtons && $details !== null && $details !== '' && $details !== 'optional') {
|
|
if (! $answer || empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) {
|
|
$errors[] = "Question '{$question->text}' requires a text response.";
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display the final session result.
|
|
*/
|
|
public function result(Session $session): Response
|
|
{
|
|
$session->load('category');
|
|
|
|
return Inertia::render('Session/Result', [
|
|
'session' => $session,
|
|
'score' => $session->score,
|
|
'result' => $session->result,
|
|
'categoryName' => $session->category->name,
|
|
]);
|
|
}
|
|
}
|