where('is_active', true)->firstOrFail(); $program = $qrCode->program; $participant = Participant::where('uuid', $participant_uuid)->firstOrFail(); $pp = $program->programParticipants()->where('participant_id', $participant->id)->first(); abort_if(! $pp, 404); $pq = $program->questionnaire()->with('questionnaireSet')->first(); if (! $pq || ! $pq->is_confirmed) { return redirect()->route('public.semak.show', $qr_token); } if (QuestionnaireResponse::where('program_id', $program->id)->where('participant_id', $participant->id)->exists()) { return view('public.questionnaire.already', compact('program', 'participant', 'qrCode')); } $questions = $this->loadHierarchical($pq); return view('public.questionnaire.show', compact('program', 'participant', 'qrCode', 'pq', 'questions')); } public function submit(Request $request, string $qr_token, string $participant_uuid): View|RedirectResponse { $qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail(); $program = $qrCode->program; $participant = Participant::where('uuid', $participant_uuid)->firstOrFail(); $pp = $program->programParticipants()->where('participant_id', $participant->id)->first(); abort_if(! $pp, 404); $pq = $program->questionnaire()->with('questionnaireSet')->first(); abort_if(! $pq || ! $pq->is_confirmed, 404); if (QuestionnaireResponse::where('program_id', $program->id)->where('participant_id', $participant->id)->exists()) { return view('public.questionnaire.already', compact('program', 'participant', 'qrCode')); } $questions = $this->loadHierarchical($pq); $answerable = $this->flatten($questions); $rules = []; foreach ($answerable as $q) { if ($q->question_type === 'multiple_choice') { $rules['q_' . $q->id] = ($q->is_required ? 'required|' : 'nullable|') . 'array'; } else { $rules['q_' . $q->id] = $q->is_required ? 'required' : 'nullable'; } } $request->validate($rules, ['q_*.required' => 'Soalan ini wajib dijawab.']); $response = QuestionnaireResponse::create([ 'program_id' => $program->id, 'participant_id' => $participant->id, 'questionnaire_set_id' => $pq->questionnaire_set_id, 'submitted_at' => now(), 'ip_address' => $request->ip(), 'user_agent' => substr($request->userAgent() ?? '', 0, 500), ]); foreach ($answerable as $q) { $raw = $request->input('q_' . $q->id); if ($raw === null && ! $q->is_required) { continue; } $value = match ($q->question_type) { 'multiple_choice' => (array) $raw, 'rating' => (int) $raw, default => $raw, }; QuestionnaireAnswer::create([ 'questionnaire_response_id' => $response->id, 'questionnaire_question_id' => $q->id, 'answer_value' => $value, ]); } return view('public.questionnaire.thankyou', compact('program', 'participant', 'qrCode')); } // ── Helpers ────────────────────────────────────────────────────────────── private function loadHierarchical($pq): Collection { return $pq->questionnaireSet->questions() ->whereNull('parent_id') ->with(['children' => fn($q) => $q->orderBy('sort_order')]) ->orderBy('sort_order') ->get(); } /** Return only answerable (non-tajuk) questions as a flat collection. */ private function flatten(Collection $topLevel): Collection { $out = collect(); foreach ($topLevel as $q) { if ($q->question_type === 'tajuk') { foreach ($q->children as $child) { $out->push($child); } } else { $out->push($q); } } return $out; } }