first
This commit is contained in:
@@ -40,11 +40,16 @@ class ParticipantController extends Controller
|
||||
|
||||
$programParticipants = $query->paginate(20)->withQueryString();
|
||||
|
||||
$countRow = DB::table('program_participants')
|
||||
->where('program_id', $program->id)
|
||||
->selectRaw("COUNT(*) as total, SUM(is_pre_registered) as pre_registered, SUM(registration_source = 'walk_in') as walk_in, SUM(status = 'checked_in') as checked_in")
|
||||
->first();
|
||||
|
||||
$counts = [
|
||||
'total' => $program->programParticipants()->count(),
|
||||
'pre_registered' => $program->programParticipants()->where('is_pre_registered', true)->count(),
|
||||
'walk_in' => $program->programParticipants()->where('registration_source', 'walk_in')->count(),
|
||||
'checked_in' => $program->programParticipants()->where('status', 'checked_in')->count(),
|
||||
'total' => (int) ($countRow->total ?? 0),
|
||||
'pre_registered' => (int) ($countRow->pre_registered ?? 0),
|
||||
'walk_in' => (int) ($countRow->walk_in ?? 0),
|
||||
'checked_in' => (int) ($countRow->checked_in ?? 0),
|
||||
];
|
||||
|
||||
return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts'));
|
||||
|
||||
60
app/Http/Controllers/Admin/ProfileController.php
Normal file
60
app/Http/Controllers/Admin/ProfileController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(): View
|
||||
{
|
||||
return view('admin.profile.show', ['user' => auth()->user()]);
|
||||
}
|
||||
|
||||
public function updateEmail(Request $request): RedirectResponse
|
||||
{
|
||||
$validator = \Validator::make($request->all(), [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email,' . auth()->id()],
|
||||
], [
|
||||
'current_password.current_password' => 'Kata laluan semasa tidak betul.',
|
||||
'email.unique' => 'Alamat emel ini sudah digunakan.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return back()->withErrors($validator, 'email')->withInput();
|
||||
}
|
||||
|
||||
auth()->user()->update(['email' => $request->email]);
|
||||
|
||||
return back()->with('email_success', 'Alamat emel berjaya dikemaskini.');
|
||||
}
|
||||
|
||||
public function updatePassword(Request $request): RedirectResponse
|
||||
{
|
||||
$validator = \Validator::make($request->all(), [
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Password::min(8)],
|
||||
], [
|
||||
'current_password.current_password' => 'Kata laluan semasa tidak betul.',
|
||||
'password.min' => 'Kata laluan baru mestilah sekurang-kurangnya 8 aksara.',
|
||||
'password.confirmed' => 'Pengesahan kata laluan tidak sepadan.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return back()->withErrors($validator, 'password')->withInput();
|
||||
}
|
||||
|
||||
auth()->user()->update(['password' => Hash::make($request->password)]);
|
||||
|
||||
Auth::login(auth()->user());
|
||||
|
||||
return back()->with('password_success', 'Kata laluan berjaya ditukar.');
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,24 @@ class ProgramController extends Controller
|
||||
'questionnaire.questionnaireSet.questions',
|
||||
]);
|
||||
|
||||
// Consolidate into 2 queries instead of 6 separate COUNTs
|
||||
$ppStats = \DB::table('program_participants')
|
||||
->where('program_id', $program->id)
|
||||
->selectRaw("COUNT(*) as total, SUM(is_pre_registered) as pre_registered, SUM(registration_source = 'walk_in') as walk_in")
|
||||
->first();
|
||||
|
||||
$certStats = \DB::table('certificates')
|
||||
->where('program_id', $program->id)
|
||||
->selectRaw("COUNT(*) as total, SUM(status IN ('generated','emailed','downloaded')) as cert_generated")
|
||||
->first();
|
||||
|
||||
$stats = [
|
||||
'total_participants' => $program->programParticipants()->count(),
|
||||
'pre_registered' => $program->programParticipants()->where('is_pre_registered', true)->count(),
|
||||
'walk_in' => $program->programParticipants()->where('registration_source', 'walk_in')->count(),
|
||||
'total_attendances' => $program->attendances()->count(),
|
||||
'total_certificates' => $program->certificates()->count(),
|
||||
'generated_certificates'=> $program->certificates()->whereIn('status', ['generated', 'emailed', 'downloaded'])->count(),
|
||||
'total_participants' => (int) ($ppStats->total ?? 0),
|
||||
'pre_registered' => (int) ($ppStats->pre_registered ?? 0),
|
||||
'walk_in' => (int) ($ppStats->walk_in ?? 0),
|
||||
'total_attendances' => $program->attendances()->count(),
|
||||
'total_certificates' => (int) ($certStats->total ?? 0),
|
||||
'generated_certificates' => (int) ($certStats->cert_generated ?? 0),
|
||||
];
|
||||
|
||||
return view('admin.programs.show', compact('program', 'stats'));
|
||||
|
||||
@@ -69,6 +69,23 @@ class ProgramQuestionnaireController extends Controller
|
||||
return back()->with('success', 'Soalselidik telah disahkan untuk program ini.');
|
||||
}
|
||||
|
||||
public function preview(Program $program): View|\Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$pq = $program->questionnaire()->with('questionnaireSet')->first();
|
||||
|
||||
if (! $pq || ! $pq->questionnaireSet) {
|
||||
return back()->with('error', 'Tiada soalselidik untuk dipratonton.');
|
||||
}
|
||||
|
||||
$questions = $pq->questionnaireSet->questions()
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => fn($q) => $q->orderBy('sort_order')])
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
return view('admin.programs.questionnaire.preview', compact('program', 'pq', 'questions'));
|
||||
}
|
||||
|
||||
public function detach(Program $program): RedirectResponse
|
||||
{
|
||||
$pq = $program->questionnaire;
|
||||
|
||||
@@ -13,26 +13,58 @@ class QuestionController extends Controller
|
||||
{
|
||||
public function store(Request $request, QuestionnaireSet $set): RedirectResponse
|
||||
{
|
||||
if ($request->has('options')) {
|
||||
$request->merge(['options' => array_values(array_filter($request->input('options', [])))]);
|
||||
}
|
||||
|
||||
$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',
|
||||
'question_text' => 'required|string|max:1000',
|
||||
'question_type' => 'required|in:tajuk,rating,single_choice,multiple_choice,short_text,long_text',
|
||||
'is_required' => 'boolean',
|
||||
'parent_id' => 'nullable|integer|exists:questionnaire_questions,id',
|
||||
'options' => 'nullable|array',
|
||||
'options.*' => 'required|string|max:255',
|
||||
'rating_labels' => 'nullable|array',
|
||||
'rating_labels.*' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
if ($data['question_type'] === 'rating') {
|
||||
if (empty($data['parent_id'])) {
|
||||
return back()->withErrors(['parent_id' => 'Soalan rating mesti diletakkan di bawah tajuk.'])->withInput();
|
||||
}
|
||||
$parent = QuestionnaireQuestion::find($data['parent_id']);
|
||||
if (! $parent || $parent->question_type !== 'tajuk') {
|
||||
return back()->withErrors(['parent_id' => 'Parent mesti jenis Tajuk.'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
$parentId = $data['question_type'] === 'rating' ? ($data['parent_id'] ?? null) : null;
|
||||
|
||||
$maxOrder = $set->questions()
|
||||
->when($parentId,
|
||||
fn($q) => $q->where('parent_id', $parentId),
|
||||
fn($q) => $q->whereNull('parent_id')
|
||||
)
|
||||
->max('sort_order') ?? 0;
|
||||
|
||||
$ratingLabels = null;
|
||||
if ($data['question_type'] === 'tajuk') {
|
||||
$filtered = array_filter($data['rating_labels'] ?? [], fn($v) => $v !== null && $v !== '');
|
||||
$ratingLabels = ! empty($filtered) ? $filtered : null;
|
||||
}
|
||||
|
||||
$set->questions()->create([
|
||||
'question_text' => $data['question_text'],
|
||||
'question_type' => $data['question_type'],
|
||||
'is_required' => $data['is_required'] ?? true,
|
||||
'parent_id' => $parentId,
|
||||
'is_required' => $data['question_type'] === 'tajuk' ? false : ($data['is_required'] ?? true),
|
||||
'options_json' => $needsOptions ? array_values(array_filter($data['options'] ?? [])) : null,
|
||||
'rating_labels' => $ratingLabels,
|
||||
'sort_order' => $maxOrder + 1,
|
||||
]);
|
||||
|
||||
@@ -42,21 +74,48 @@ class QuestionController extends Controller
|
||||
|
||||
public function update(Request $request, QuestionnaireQuestion $question): RedirectResponse
|
||||
{
|
||||
if ($request->has('options')) {
|
||||
$request->merge(['options' => array_values(array_filter($request->input('options', [])))]);
|
||||
}
|
||||
|
||||
$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',
|
||||
'question_text' => 'required|string|max:1000',
|
||||
'question_type' => 'required|in:tajuk,rating,single_choice,multiple_choice,short_text,long_text',
|
||||
'is_required' => 'boolean',
|
||||
'parent_id' => 'nullable|integer|exists:questionnaire_questions,id',
|
||||
'options' => 'nullable|array',
|
||||
'options.*' => 'required|string|max:255',
|
||||
'rating_labels' => 'nullable|array',
|
||||
'rating_labels.*' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
if ($data['question_type'] === 'rating') {
|
||||
if (empty($data['parent_id'])) {
|
||||
return back()->withErrors(['parent_id' => 'Soalan rating mesti diletakkan di bawah tajuk.'])->withInput();
|
||||
}
|
||||
$parent = QuestionnaireQuestion::find($data['parent_id']);
|
||||
if (! $parent || $parent->question_type !== 'tajuk') {
|
||||
return back()->withErrors(['parent_id' => 'Parent mesti jenis Tajuk.'])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$needsOptions = in_array($data['question_type'], ['single_choice', 'multiple_choice']);
|
||||
|
||||
$parentId = $data['question_type'] === 'rating' ? ($data['parent_id'] ?? null) : null;
|
||||
|
||||
$ratingLabels = null;
|
||||
if ($data['question_type'] === 'tajuk') {
|
||||
$filtered = array_filter($data['rating_labels'] ?? [], fn($v) => $v !== null && $v !== '');
|
||||
$ratingLabels = ! empty($filtered) ? $filtered : null;
|
||||
}
|
||||
|
||||
$question->update([
|
||||
'question_text' => $data['question_text'],
|
||||
'question_type' => $data['question_type'],
|
||||
'is_required' => $data['is_required'] ?? true,
|
||||
'parent_id' => $parentId,
|
||||
'is_required' => $data['question_type'] === 'tajuk' ? false : ($data['is_required'] ?? true),
|
||||
'options_json' => $needsOptions ? array_values(array_filter($data['options'] ?? [])) : null,
|
||||
'rating_labels' => $ratingLabels,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.questionnaires.show', $question->questionnaire_set_id)
|
||||
@@ -66,6 +125,12 @@ class QuestionController extends Controller
|
||||
public function destroy(QuestionnaireQuestion $question): RedirectResponse
|
||||
{
|
||||
$setId = $question->questionnaire_set_id;
|
||||
|
||||
// Cascade-delete children if this is a tajuk (DB cascade handles it too, but be explicit)
|
||||
if ($question->question_type === 'tajuk') {
|
||||
$question->children()->delete();
|
||||
}
|
||||
|
||||
$question->delete();
|
||||
|
||||
return redirect()->route('admin.questionnaires.show', $setId)
|
||||
@@ -75,8 +140,9 @@ class QuestionController extends Controller
|
||||
public function reorder(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'order' => 'required|array',
|
||||
'order.*' => 'integer|exists:questionnaire_questions,id',
|
||||
'order' => 'required|array',
|
||||
'order.*' => 'integer|exists:questionnaire_questions,id',
|
||||
'parent_id' => 'nullable|integer|exists:questionnaire_questions,id',
|
||||
]);
|
||||
|
||||
foreach ($data['order'] as $sortOrder => $questionId) {
|
||||
|
||||
@@ -52,10 +52,18 @@ class QuestionnaireSetController extends Controller
|
||||
|
||||
public function show(QuestionnaireSet $set): View
|
||||
{
|
||||
$set->load(['questions', 'creator']);
|
||||
$set->load('creator');
|
||||
|
||||
$topLevel = $set->questions()
|
||||
->whereNull('parent_id')
|
||||
->with(['children' => fn($q) => $q->orderBy('sort_order')])
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$totalCount = $set->questions()->count();
|
||||
$usedInPrograms = $set->programs()->get();
|
||||
|
||||
return view('admin.questionnaires.show', compact('set', 'usedInPrograms'));
|
||||
return view('admin.questionnaires.show', compact('set', 'topLevel', 'totalCount', 'usedInPrograms'));
|
||||
}
|
||||
|
||||
public function edit(QuestionnaireSet $set): View
|
||||
|
||||
@@ -17,7 +17,7 @@ class StatisticsController extends Controller
|
||||
{
|
||||
public function show(Program $program): View
|
||||
{
|
||||
$program->load(['attendances.participant', 'questionnaire.questionnaireSet.questions']);
|
||||
$program->load(['questionnaire.questionnaireSet.questions']);
|
||||
|
||||
// Attendance by session
|
||||
$bySession = $program->attendances()
|
||||
@@ -40,23 +40,29 @@ class StatisticsController extends Controller
|
||||
->pluck('total', 'status')
|
||||
->toArray();
|
||||
|
||||
// Response rate
|
||||
// Response rate + question stats
|
||||
$pq = $program->questionnaire;
|
||||
$responseRate = null;
|
||||
$questionStats = [];
|
||||
$totalResponses = 0;
|
||||
|
||||
if ($pq && $pq->is_confirmed) {
|
||||
$totalAttended = $program->attendances()->count();
|
||||
$totalAttended = array_sum($bySession); // reuse already-fetched data
|
||||
$totalResponses = QuestionnaireResponse::where('program_id', $program->id)->count();
|
||||
$responseRate = $totalAttended > 0 ? round($totalResponses / $totalAttended * 100) : 0;
|
||||
|
||||
// Rating question averages
|
||||
$questions = $pq->questionnaireSet->questions ?? collect();
|
||||
|
||||
// Load ALL answers in one query, group by question — avoids N+1
|
||||
$allAnswers = QuestionnaireAnswer::whereIn('questionnaire_question_id', $questions->pluck('id'))
|
||||
->get()
|
||||
->groupBy('questionnaire_question_id');
|
||||
|
||||
foreach ($questions as $q) {
|
||||
$answers = $allAnswers->get($q->id, collect());
|
||||
|
||||
if ($q->question_type === 'rating') {
|
||||
$answers = QuestionnaireAnswer::where('questionnaire_question_id', $q->id)
|
||||
->pluck('answer_value');
|
||||
$values = $answers->map(fn($v) => is_array($v) ? (int)($v[0] ?? 0) : (int)$v);
|
||||
$values = $answers->map(fn($a) => is_array($a->answer_value) ? (int) ($a->answer_value[0] ?? 0) : (int) $a->answer_value);
|
||||
$questionStats[] = [
|
||||
'id' => $q->id,
|
||||
'text' => $q->question_text,
|
||||
@@ -65,11 +71,9 @@ class StatisticsController extends Controller
|
||||
'count' => $values->count(),
|
||||
];
|
||||
} elseif (in_array($q->question_type, ['single_choice', 'multiple_choice'])) {
|
||||
$answers = QuestionnaireAnswer::where('questionnaire_question_id', $q->id)
|
||||
->pluck('answer_value');
|
||||
$counts = [];
|
||||
foreach ($answers as $val) {
|
||||
$items = is_array($val) ? $val : [$val];
|
||||
foreach ($answers as $row) {
|
||||
$items = is_array($row->answer_value) ? $row->answer_value : [$row->answer_value];
|
||||
foreach ($items as $item) {
|
||||
$counts[$item] = ($counts[$item] ?? 0) + 1;
|
||||
}
|
||||
@@ -86,14 +90,15 @@ class StatisticsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse data already computed above — no extra queries
|
||||
$summary = [
|
||||
'total_attendances' => $program->attendances()->count(),
|
||||
'pre_registered' => $program->attendances()->where('attendance_source', 'pre_registered_staff')->count(),
|
||||
'walk_in' => $program->attendances()->where('attendance_source', 'walk_in_external')->count(),
|
||||
'total_certificates' => $program->certificates()->count(),
|
||||
'generated_certs' => $program->certificates()->whereIn('status', ['generated', 'emailed', 'downloaded'])->count(),
|
||||
'downloaded_certs' => $program->certificates()->where('status', 'downloaded')->count(),
|
||||
'total_responses' => QuestionnaireResponse::where('program_id', $program->id)->count(),
|
||||
'total_attendances' => array_sum($bySession),
|
||||
'pre_registered' => $bySource['pre_registered_staff'] ?? 0,
|
||||
'walk_in' => $bySource['walk_in_external'] ?? 0,
|
||||
'total_certificates' => array_sum($certStats),
|
||||
'generated_certs' => ($certStats['generated'] ?? 0) + ($certStats['emailed'] ?? 0) + ($certStats['downloaded'] ?? 0),
|
||||
'downloaded_certs' => $certStats['downloaded'] ?? 0,
|
||||
'total_responses' => $totalResponses,
|
||||
];
|
||||
|
||||
return view('admin.programs.statistics.show', compact(
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\QuestionnaireResponse;
|
||||
use App\Models\QuestionnaireAnswer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class QuestionnaireController extends Controller
|
||||
@@ -20,27 +21,20 @@ class QuestionnaireController extends Controller
|
||||
|
||||
$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();
|
||||
$pq = $program->questionnaire()->with('questionnaireSet')->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) {
|
||||
if (QuestionnaireResponse::where('program_id', $program->id)->where('participant_id', $participant->id)->exists()) {
|
||||
return view('public.questionnaire.already', compact('program', 'participant', 'qrCode'));
|
||||
}
|
||||
|
||||
$questions = $pq->questionnaireSet->questions;
|
||||
$questions = $this->loadHierarchical($pq);
|
||||
|
||||
return view('public.questionnaire.show', compact('program', 'participant', 'qrCode', 'pq', 'questions'));
|
||||
}
|
||||
@@ -55,38 +49,27 @@ class QuestionnaireController extends Controller
|
||||
$pp = $program->programParticipants()->where('participant_id', $participant->id)->first();
|
||||
abort_if(! $pp, 404);
|
||||
|
||||
$pq = $program->questionnaire()->with('questionnaireSet.questions')->first();
|
||||
$pq = $program->questionnaire()->with('questionnaireSet')->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) {
|
||||
if (QuestionnaireResponse::where('program_id', $program->id)->where('participant_id', $participant->id)->exists()) {
|
||||
return view('public.questionnaire.already', compact('program', 'participant', 'qrCode'));
|
||||
}
|
||||
|
||||
$questions = $pq->questionnaireSet->questions;
|
||||
$questions = $this->loadHierarchical($pq);
|
||||
$answerable = $this->flatten($questions);
|
||||
|
||||
// Validate required questions
|
||||
$rules = [];
|
||||
foreach ($questions as $q) {
|
||||
if ($q->is_required) {
|
||||
$rules['q_' . $q->id] = 'required';
|
||||
} else {
|
||||
$rules['q_' . $q->id] = 'nullable';
|
||||
}
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
$validated = $request->validate($rules, [
|
||||
'q_*.required' => 'Soalan ini wajib dijawab.',
|
||||
]);
|
||||
$request->validate($rules, ['q_*.required' => 'Soalan ini wajib dijawab.']);
|
||||
|
||||
// Save response
|
||||
$response = QuestionnaireResponse::create([
|
||||
'program_id' => $program->id,
|
||||
'participant_id' => $participant->id,
|
||||
@@ -96,7 +79,7 @@ class QuestionnaireController extends Controller
|
||||
'user_agent' => substr($request->userAgent() ?? '', 0, 500),
|
||||
]);
|
||||
|
||||
foreach ($questions as $q) {
|
||||
foreach ($answerable as $q) {
|
||||
$raw = $request->input('q_' . $q->id);
|
||||
|
||||
if ($raw === null && ! $q->is_required) {
|
||||
@@ -110,12 +93,39 @@ class QuestionnaireController extends Controller
|
||||
};
|
||||
|
||||
QuestionnaireAnswer::create([
|
||||
'questionnaire_response_id' => $response->id,
|
||||
'questionnaire_question_id' => $q->id,
|
||||
'answer_value' => $value,
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class QuestionnaireQuestion extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'questionnaire_set_id', 'question_text', 'question_type',
|
||||
'options_json', 'is_required', 'sort_order',
|
||||
'questionnaire_set_id', 'parent_id', 'question_text', 'question_type',
|
||||
'options_json', 'rating_labels', 'is_required', 'sort_order',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'options_json' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
'options_json' => 'array',
|
||||
'rating_labels' => 'array',
|
||||
'is_required' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -25,6 +26,16 @@ class QuestionnaireQuestion extends Model
|
||||
return $this->belongsTo(QuestionnaireSet::class);
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(QuestionnaireQuestion::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(QuestionnaireQuestion::class, 'parent_id')->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function answers()
|
||||
{
|
||||
return $this->hasMany(QuestionnaireAnswer::class);
|
||||
|
||||
Reference in New Issue
Block a user