diff --git a/app/Http/Controllers/Admin/ProgramQuestionnaireController.php b/app/Http/Controllers/Admin/ProgramQuestionnaireController.php index 3b7ed44..1dcaafb 100644 --- a/app/Http/Controllers/Admin/ProgramQuestionnaireController.php +++ b/app/Http/Controllers/Admin/ProgramQuestionnaireController.php @@ -3,9 +3,86 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\Program; +use App\Models\ProgramQuestionnaire; +use App\Models\QuestionnaireSet; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\View\View; class ProgramQuestionnaireController extends Controller { - // + public function show(Program $program): View + { + $pq = $program->questionnaire()->with('questionnaireSet.questions')->first(); + + $availableSets = QuestionnaireSet::where('status', 'published') + ->withCount('questions') + ->orderBy('title') + ->get(); + + return view('admin.programs.questionnaire.show', compact('program', 'pq', 'availableSets')); + } + + public function attach(Request $request, Program $program): RedirectResponse + { + $data = $request->validate([ + 'questionnaire_set_id' => 'required|exists:questionnaire_sets,id', + ]); + + if ($program->questionnaire()->exists()) { + return back()->with('error', 'Program ini sudah ada soalselidik dilampirkan. Tanggalkan dahulu sebelum lampir yang baru.'); + } + + $set = QuestionnaireSet::findOrFail($data['questionnaire_set_id']); + + if ($set->status !== 'published') { + return back()->with('error', 'Hanya soalselidik yang diterbitkan boleh dilampirkan.'); + } + + ProgramQuestionnaire::create([ + 'program_id' => $program->id, + 'questionnaire_set_id' => $set->id, + 'is_confirmed' => false, + ]); + + return back()->with('success', 'Soalselidik berjaya dilampirkan. Sila sahkan sebelum program bermula.'); + } + + public function confirm(Request $request, Program $program): RedirectResponse + { + $pq = $program->questionnaire; + + if (! $pq) { + return back()->with('error', 'Tiada soalselidik untuk disahkan.'); + } + + $pq->update([ + 'is_confirmed' => true, + 'confirmed_at' => now(), + 'confirmed_by' => auth()->id(), + ]); + + return back()->with('success', 'Soalselidik telah disahkan untuk program ini.'); + } + + public function detach(Program $program): RedirectResponse + { + $pq = $program->questionnaire; + + if (! $pq) { + return back()->with('error', 'Tiada soalselidik untuk ditanggalkan.'); + } + + if ($pq->is_confirmed) { + $hasResponses = \App\Models\QuestionnaireResponse::where('program_id', $program->id)->exists(); + if ($hasResponses) { + return back()->with('error', 'Soalselidik tidak boleh ditanggalkan kerana sudah ada respons diterima.'); + } + } + + $pq->delete(); + + return back()->with('success', 'Soalselidik berjaya ditanggalkan.'); + } } diff --git a/app/Http/Controllers/Admin/QuestionController.php b/app/Http/Controllers/Admin/QuestionController.php index 1d7fa5b..c4b339c 100644 --- a/app/Http/Controllers/Admin/QuestionController.php +++ b/app/Http/Controllers/Admin/QuestionController.php @@ -3,9 +3,87 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\QuestionnaireQuestion; +use App\Models\QuestionnaireSet; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; class QuestionController extends Controller { - // + public function store(Request $request, QuestionnaireSet $set): RedirectResponse + { + $data = $request->validate([ + 'question_text' => 'required|string|max:1000', + 'question_type' => 'required|in:rating,single_choice,multiple_choice,short_text,long_text', + 'is_required' => 'boolean', + 'options' => 'nullable|array', + 'options.*' => 'required|string|max:255', + ]); + + $needsOptions = in_array($data['question_type'], ['single_choice', 'multiple_choice']); + if ($needsOptions && empty($data['options'])) { + return back()->withErrors(['options' => 'Pilihan jawapan diperlukan untuk jenis soalan ini.'])->withInput(); + } + + $maxOrder = $set->questions()->max('sort_order') ?? 0; + + $set->questions()->create([ + 'question_text' => $data['question_text'], + 'question_type' => $data['question_type'], + 'is_required' => $data['is_required'] ?? true, + 'options_json' => $needsOptions ? array_values(array_filter($data['options'] ?? [])) : null, + 'sort_order' => $maxOrder + 1, + ]); + + return redirect()->route('admin.questionnaires.show', $set) + ->with('success', 'Soalan berjaya ditambah.'); + } + + public function update(Request $request, QuestionnaireQuestion $question): RedirectResponse + { + $data = $request->validate([ + 'question_text' => 'required|string|max:1000', + 'question_type' => 'required|in:rating,single_choice,multiple_choice,short_text,long_text', + 'is_required' => 'boolean', + 'options' => 'nullable|array', + 'options.*' => 'required|string|max:255', + ]); + + $needsOptions = in_array($data['question_type'], ['single_choice', 'multiple_choice']); + + $question->update([ + 'question_text' => $data['question_text'], + 'question_type' => $data['question_type'], + 'is_required' => $data['is_required'] ?? true, + 'options_json' => $needsOptions ? array_values(array_filter($data['options'] ?? [])) : null, + ]); + + return redirect()->route('admin.questionnaires.show', $question->questionnaire_set_id) + ->with('success', 'Soalan berjaya dikemaskini.'); + } + + public function destroy(QuestionnaireQuestion $question): RedirectResponse + { + $setId = $question->questionnaire_set_id; + $question->delete(); + + return redirect()->route('admin.questionnaires.show', $setId) + ->with('success', 'Soalan berjaya dipadam.'); + } + + public function reorder(Request $request): JsonResponse + { + $data = $request->validate([ + 'order' => 'required|array', + 'order.*' => 'integer|exists:questionnaire_questions,id', + ]); + + foreach ($data['order'] as $sortOrder => $questionId) { + QuestionnaireQuestion::where('id', $questionId) + ->update(['sort_order' => $sortOrder + 1]); + } + + return response()->json(['ok' => true]); + } } diff --git a/app/Http/Controllers/Admin/QuestionnaireSetController.php b/app/Http/Controllers/Admin/QuestionnaireSetController.php index 3d1ea98..5703c39 100644 --- a/app/Http/Controllers/Admin/QuestionnaireSetController.php +++ b/app/Http/Controllers/Admin/QuestionnaireSetController.php @@ -3,63 +3,108 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\QuestionnaireSet; use Illuminate\Http\Request; +use Illuminate\Http\RedirectResponse; +use Illuminate\View\View; class QuestionnaireSetController extends Controller { - /** - * Display a listing of the resource. - */ - public function index() + public function index(Request $request): View { - // + $query = QuestionnaireSet::withCount('questions') + ->with('creator') + ->latest(); + + if ($request->filled('q')) { + $query->where('title', 'like', '%' . $request->q . '%'); + } + + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $sets = $query->paginate(20)->withQueryString(); + + return view('admin.questionnaires.index', compact('sets')); } - /** - * Show the form for creating a new resource. - */ - public function create() + public function create(): View { - // + return view('admin.questionnaires.create'); } - /** - * Store a newly created resource in storage. - */ - public function store(Request $request) + public function store(Request $request): RedirectResponse { - // + $data = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + ]); + + $data['created_by'] = auth()->id(); + $data['status'] = 'draft'; + + $set = QuestionnaireSet::create($data); + + return redirect()->route('admin.questionnaires.show', $set) + ->with('success', 'Set soalselidik berjaya dicipta.'); } - /** - * Display the specified resource. - */ - public function show(string $id) + public function show(QuestionnaireSet $set): View { - // + $set->load(['questions', 'creator']); + $usedInPrograms = $set->programs()->get(); + + return view('admin.questionnaires.show', compact('set', 'usedInPrograms')); } - /** - * Show the form for editing the specified resource. - */ - public function edit(string $id) + public function edit(QuestionnaireSet $set): View { - // + return view('admin.questionnaires.edit', compact('set')); } - /** - * Update the specified resource in storage. - */ - public function update(Request $request, string $id) + public function update(Request $request, QuestionnaireSet $set): RedirectResponse { - // + $data = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + ]); + + $set->update($data); + + return redirect()->route('admin.questionnaires.show', $set) + ->with('success', 'Set soalselidik berjaya dikemaskini.'); } - /** - * Remove the specified resource from storage. - */ - public function destroy(string $id) + public function destroy(QuestionnaireSet $set): RedirectResponse { - // + $confirmed = $set->programQuestionnaires()->where('is_confirmed', true)->exists(); + + if ($confirmed) { + return back()->with('error', 'Soalselidik ini tidak boleh dipadam kerana sudah disahkan untuk program.'); + } + + $set->delete(); + + return redirect()->route('admin.questionnaires.index') + ->with('success', 'Set soalselidik berjaya dipadam.'); + } + + public function publish(QuestionnaireSet $set): RedirectResponse + { + if ($set->questions()->count() === 0) { + return back()->with('error', 'Tambah sekurang-kurangnya satu soalan sebelum menerbitkan.'); + } + + $set->update(['status' => 'published']); + + return back()->with('success', 'Set soalselidik diterbitkan.'); + } + + public function archive(QuestionnaireSet $set): RedirectResponse + { + $set->update(['status' => 'archived']); + + return back()->with('success', 'Set soalselidik diarkibkan.'); } } diff --git a/app/Http/Controllers/Public/QuestionnaireController.php b/app/Http/Controllers/Public/QuestionnaireController.php index 40022e5..eb0d5e1 100644 --- a/app/Http/Controllers/Public/QuestionnaireController.php +++ b/app/Http/Controllers/Public/QuestionnaireController.php @@ -3,9 +3,119 @@ 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')); + } } diff --git a/resources/views/admin/programs/questionnaire/show.blade.php b/resources/views/admin/programs/questionnaire/show.blade.php new file mode 100644 index 0000000..6935632 --- /dev/null +++ b/resources/views/admin/programs/questionnaire/show.blade.php @@ -0,0 +1,171 @@ +@extends('layouts.admin') + +@section('title', 'Soalselidik — ' . $program->title) +@section('header', 'Urus Soalselidik Program') + +@section('breadcrumb') +
Belum ada soalselidik dilampirkan untuk program ini.
+| Tajuk | +Soalan | +Status | +Dicipta Oleh | +Tarikh Cipta | ++ |
|---|---|---|---|---|---|
|
+
+ {{ $set->title }}
+
+ @if($set->description)
+ {{ $set->description }}
+ @endif
+ |
+ + {{ $set->questions_count }} + | ++ @if($set->status === 'published') + Diterbitkan + @elseif($set->status === 'archived') + Diarkib + @else + Draf + @endif + | +{{ $set->creator?->name ?? '—' }} | +{{ $set->created_at->format('d/m/Y') }} | ++ + + + | +
| + + Tiada set soalselidik dijumpai. + | +|||||
+ Maklum balas anda untuk program ini sudah pernah dihantar sebelum ini. + Setiap peserta hanya boleh menghantar satu maklum balas. +
+ ++ Sila jawab semua soalan sebelum memuat turun sijil anda, {{ $participant->name }}. +
++ Maklum balas anda telah berjaya dihantar, {{ $participant->name }}. + Kami menghargai pandangan anda. +
+ +