- QuestionnaireSetController: full CRUD + publish/archive - QuestionController: store, update, destroy, reorder - ProgramQuestionnaireController: attach, confirm, detach - Public/QuestionnaireController: show form, submit responses, double-submit guard - Views: admin questionnaire CRUD, program questionnaire assign, public form + thankyou/already Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
4.3 KiB
PHP
122 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Public;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Participant;
|
|
use App\Models\ProgramQrCode;
|
|
use App\Models\QuestionnaireResponse;
|
|
use App\Models\QuestionnaireAnswer;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class QuestionnaireController extends Controller
|
|
{
|
|
public function show(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();
|
|
|
|
// Verify participant belongs to this program
|
|
$pp = $program->programParticipants()->where('participant_id', $participant->id)->first();
|
|
abort_if(! $pp, 404);
|
|
|
|
$pq = $program->questionnaire()->with('questionnaireSet.questions')->first();
|
|
|
|
if (! $pq || ! $pq->is_confirmed) {
|
|
// No questionnaire — go straight to semak page
|
|
return redirect()->route('public.semak.show', $qr_token);
|
|
}
|
|
|
|
// Check already submitted
|
|
$alreadySubmitted = QuestionnaireResponse::where('program_id', $program->id)
|
|
->where('participant_id', $participant->id)
|
|
->exists();
|
|
|
|
if ($alreadySubmitted) {
|
|
return view('public.questionnaire.already', compact('program', 'participant', 'qrCode'));
|
|
}
|
|
|
|
$questions = $pq->questionnaireSet->questions;
|
|
|
|
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.questions')->first();
|
|
abort_if(! $pq || ! $pq->is_confirmed, 404);
|
|
|
|
// Prevent double-submit
|
|
$existing = QuestionnaireResponse::where('program_id', $program->id)
|
|
->where('participant_id', $participant->id)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
return view('public.questionnaire.already', compact('program', 'participant', 'qrCode'));
|
|
}
|
|
|
|
$questions = $pq->questionnaireSet->questions;
|
|
|
|
// Validate required questions
|
|
$rules = [];
|
|
foreach ($questions as $q) {
|
|
if ($q->is_required) {
|
|
$rules['q_' . $q->id] = 'required';
|
|
} else {
|
|
$rules['q_' . $q->id] = 'nullable';
|
|
}
|
|
if ($q->question_type === 'multiple_choice') {
|
|
$rules['q_' . $q->id] = ($q->is_required ? 'required|' : 'nullable|') . 'array';
|
|
}
|
|
}
|
|
|
|
$validated = $request->validate($rules, [
|
|
'q_*.required' => 'Soalan ini wajib dijawab.',
|
|
]);
|
|
|
|
// Save response
|
|
$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 ($questions 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'));
|
|
}
|
|
}
|