diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ccda790..41dc01a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,7 +29,8 @@ "mcp__playwright__browser_wait_for", "WebFetch(domain:www.bakertilly.nl)", "mcp__playwright__browser_type", - "mcp__playwright__browser_hover" + "mcp__playwright__browser_hover", + "mcp__playwright__browser_evaluate" ] } } diff --git a/app/Http/Controllers/SessionController.php b/app/Http/Controllers/SessionController.php index 1d15ec0..06d984c 100644 --- a/app/Http/Controllers/SessionController.php +++ b/app/Http/Controllers/SessionController.php @@ -11,6 +11,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Arr; +use Illuminate\Validation\ValidationException; use Inertia\Inertia; use Inertia\Response; @@ -109,6 +110,8 @@ private function saveAnswers(Session $session, array $answers): void */ private function completeSession(Session $session): RedirectResponse { + $this->validateSessionCompletion($session); + $scoringService = new ScoringService; $score = $scoringService->calculateScore($session); $result = $scoringService->determineResult($score); @@ -125,6 +128,76 @@ private function completeSession(Session $session): RedirectResponse 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 !== '') { + if (! $answer || empty(trim(Arr::get($answer->toArray(), 'text_value', '')))) { + $errors[] = "Question '{$question->text}' requires a text response."; + } + } + } + /** * Display the final session result. */ diff --git a/resources/js/Components/QuestionCard.vue b/resources/js/Components/QuestionCard.vue index a27a889..0e412fb 100644 --- a/resources/js/Components/QuestionCard.vue +++ b/resources/js/Components/QuestionCard.vue @@ -11,6 +11,10 @@ const props = defineProps({ type: Object, default: () => ({ value: null, text_value: '' }), }, + error: { + type: String, + default: null, + }, }) const emit = defineEmits(['update:modelValue']) @@ -58,7 +62,10 @@ const updateTextValue = (event) => { diff --git a/resources/js/Pages/Session/Show.vue b/resources/js/Pages/Session/Show.vue index 49bb786..bf97df5 100644 --- a/resources/js/Pages/Session/Show.vue +++ b/resources/js/Pages/Session/Show.vue @@ -1,5 +1,5 @@