diff --git a/.dockerignore b/.dockerignore index b572ec2..70f101b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,9 +29,9 @@ public/build .phpunit.cache phpunit.xml -# Docker files (tidak perlu dalam app container) +# Docker Compose files (tidak perlu dalam app container) docker-compose*.yml -docker/ +# docker/ TIDAK diexclude — Dockerfile perlukan docker/entrypoint.sh dan docker/php/php.ini # Logs & cache storage/logs/* diff --git a/app/Http/Controllers/Admin/ParticipantController.php b/app/Http/Controllers/Admin/ParticipantController.php index 14edc40..c2f3ba5 100644 --- a/app/Http/Controllers/Admin/ParticipantController.php +++ b/app/Http/Controllers/Admin/ParticipantController.php @@ -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')); diff --git a/app/Http/Controllers/Admin/ProfileController.php b/app/Http/Controllers/Admin/ProfileController.php new file mode 100644 index 0000000..c68b425 --- /dev/null +++ b/app/Http/Controllers/Admin/ProfileController.php @@ -0,0 +1,60 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Admin/ProgramController.php b/app/Http/Controllers/Admin/ProgramController.php index f023cae..f5acba6 100644 --- a/app/Http/Controllers/Admin/ProgramController.php +++ b/app/Http/Controllers/Admin/ProgramController.php @@ -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')); diff --git a/app/Http/Controllers/Admin/ProgramQuestionnaireController.php b/app/Http/Controllers/Admin/ProgramQuestionnaireController.php index 7f78b1a..c24c2b2 100644 --- a/app/Http/Controllers/Admin/ProgramQuestionnaireController.php +++ b/app/Http/Controllers/Admin/ProgramQuestionnaireController.php @@ -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; diff --git a/app/Http/Controllers/Admin/QuestionController.php b/app/Http/Controllers/Admin/QuestionController.php index c4b339c..cdf215c 100644 --- a/app/Http/Controllers/Admin/QuestionController.php +++ b/app/Http/Controllers/Admin/QuestionController.php @@ -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) { diff --git a/app/Http/Controllers/Admin/QuestionnaireSetController.php b/app/Http/Controllers/Admin/QuestionnaireSetController.php index 5703c39..0baa321 100644 --- a/app/Http/Controllers/Admin/QuestionnaireSetController.php +++ b/app/Http/Controllers/Admin/QuestionnaireSetController.php @@ -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 diff --git a/app/Http/Controllers/Admin/StatisticsController.php b/app/Http/Controllers/Admin/StatisticsController.php index 606f07b..8ab841f 100644 --- a/app/Http/Controllers/Admin/StatisticsController.php +++ b/app/Http/Controllers/Admin/StatisticsController.php @@ -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( diff --git a/app/Http/Controllers/Public/QuestionnaireController.php b/app/Http/Controllers/Public/QuestionnaireController.php index eb0d5e1..21650af 100644 --- a/app/Http/Controllers/Public/QuestionnaireController.php +++ b/app/Http/Controllers/Public/QuestionnaireController.php @@ -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; + } } diff --git a/app/Models/QuestionnaireQuestion.php b/app/Models/QuestionnaireQuestion.php index 49c8f40..77f6b6c 100644 --- a/app/Models/QuestionnaireQuestion.php +++ b/app/Models/QuestionnaireQuestion.php @@ -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); diff --git a/database/migrations/2026_05_18_161355_add_parent_and_labels_to_questionnaire_questions.php b/database/migrations/2026_05_18_161355_add_parent_and_labels_to_questionnaire_questions.php new file mode 100644 index 0000000..2745c72 --- /dev/null +++ b/database/migrations/2026_05_18_161355_add_parent_and_labels_to_questionnaire_questions.php @@ -0,0 +1,29 @@ +foreignId('parent_id') + ->nullable() + ->after('questionnaire_set_id') + ->constrained('questionnaire_questions') + ->cascadeOnDelete(); + + $table->json('rating_labels')->nullable()->after('options_json'); + }); + } + + public function down(): void + { + Schema::table('questionnaire_questions', function (Blueprint $table) { + $table->dropForeign(['parent_id']); + $table->dropColumn(['parent_id', 'rating_labels']); + }); + } +}; diff --git a/database/migrations/2026_05_18_163447_add_tajuk_to_question_type_enum.php b/database/migrations/2026_05_18_163447_add_tajuk_to_question_type_enum.php new file mode 100644 index 0000000..a172e75 --- /dev/null +++ b/database/migrations/2026_05_18_163447_add_tajuk_to_question_type_enum.php @@ -0,0 +1,17 @@ + 'admin@mbip.gov.my'], + ['email' => 'saufi@mbip.gov.my'], [ 'name' => 'Admin eCert MBIP', - 'password' => Hash::make('Admin@MBIP2025!'), + 'password' => Hash::make('YongTauFu26'), 'role' => 'super_admin', ] ); - $this->command->info('Admin account created: admin@mbip.gov.my / Admin@MBIP2025!'); + $this->command->info('Admin account created: saufi@mbip.gov.my / YongTauFu26'); } } diff --git a/resources/views/admin/profile/show.blade.php b/resources/views/admin/profile/show.blade.php new file mode 100644 index 0000000..9184b48 --- /dev/null +++ b/resources/views/admin/profile/show.blade.php @@ -0,0 +1,137 @@ +@extends('layouts.admin') + +@section('title', 'Profil Saya') +@section('header', 'Profil Saya') + +@section('breadcrumb') +
{{ $pq->questionnaireSet->description }}
+ @endif ++ Sila jawab semua soalan sebelum memuat turun sijil anda, PESERTA CONTOH. +
+