adds validation

This commit is contained in:
2026-02-16 13:41:25 +01:00
parent eb43b35873
commit c39b8085af
6 changed files with 203 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, reactive } from 'vue'
import { computed, reactive, ref, watch, nextTick } from 'vue'
import { Head, useForm, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
@@ -44,6 +44,11 @@ const initializeAnswers = () => {
}
initializeAnswers()
// Validation state
const validationErrors = ref({})
const showErrors = ref(false)
const questionRefs = ref({})
// Save a single answer with partial reload including score
let saveTimeout = null
const saveAnswer = (questionId) => {
@@ -82,9 +87,79 @@ const saveComments = () => {
}, 1000)
}
// Session completion
// Validation function
const validate = () => {
const errors = {}
props.questionGroups.forEach(group => {
group.questions.forEach(question => {
const answer = answerData[question.id]
const hasRadioButtons = question.has_yes || question.has_no || question.has_na
// Rule 1: Radio button questions must have a selection
if (hasRadioButtons && answer.value === null) {
errors[question.id] = 'Please select an answer'
return
}
// Rule 2: Required text fields based on details
if (question.details === 'required' && !answer.text_value?.trim()) {
errors[question.id] = 'Please provide details'
return
}
if (question.details === 'req_on_yes' && answer.value === 'yes' && !answer.text_value?.trim()) {
errors[question.id] = 'Please provide details'
return
}
if (question.details === 'req_on_no' && answer.value === 'no' && !answer.text_value?.trim()) {
errors[question.id] = 'Please provide details'
return
}
// Rule 3: Text-only questions (no radio buttons, has details)
if (!hasRadioButtons && question.details && !answer.text_value?.trim()) {
errors[question.id] = 'Please enter a response'
return
}
})
})
validationErrors.value = errors
return Object.keys(errors).length === 0
}
// Watch answerData for changes and revalidate when errors are showing
watch(answerData, () => {
if (showErrors.value) {
validate()
}
}, { deep: true })
// Error count for summary banner
const errorCount = computed(() => {
return Object.values(validationErrors.value).filter(err => err !== null).length
})
// Session completion with validation
let completing = false
const completeSession = () => {
const completeSession = async () => {
showErrors.value = true
if (!validate()) {
// Scroll to first error
await nextTick()
const firstErrorQuestionId = Object.keys(validationErrors.value)[0]
const firstErrorElement = questionRefs.value[firstErrorQuestionId]
if (firstErrorElement) {
firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
return
}
completing = true
router.put(`/sessions/${props.session.id}`, {
complete: true,
@@ -141,13 +216,19 @@ const hasScoredAnswers = computed(() => {
<p v-if="group.scoring_instructions" class="text-amber-400 text-sm italic mb-4">{{ group.scoring_instructions }}</p>
<div class="divide-y divide-white/[0.06]">
<QuestionCard
<div
v-for="question in group.questions"
:key="question.id"
:question="question"
:modelValue="answerData[question.id]"
@update:modelValue="updateAnswer(question.id, $event)"
/>
:ref="el => { if (el) questionRefs[question.id] = el }"
:data-question-id="question.id"
>
<QuestionCard
:question="question"
:modelValue="answerData[question.id]"
:error="showErrors ? validationErrors[question.id] : null"
@update:modelValue="updateAnswer(question.id, $event)"
/>
</div>
</div>
</div>
@@ -164,8 +245,24 @@ const hasScoredAnswers = computed(() => {
</div>
</div>
<!-- Complete button - now enabled -->
<!-- Complete button with validation summary -->
<div class="mt-12 pt-8 border-t border-white/[0.06]">
<!-- Validation summary banner -->
<Transition
enter-active-class="transition-all duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition-all duration-150 ease-in"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 -translate-y-2"
>
<div v-if="showErrors && errorCount > 0" class="bg-red-500/10 border border-red-400/20 rounded-lg px-5 py-4 mb-6">
<p class="text-red-400 text-sm font-medium">
Please complete all required fields before submitting. {{ errorCount }} {{ errorCount === 1 ? 'question requires' : 'questions require' }} your attention.
</p>
</div>
</Transition>
<div class="flex justify-end">
<AppButton size="lg" @click="completeSession" data-cy="complete-session">
Complete