diff --git a/app/Http/Controllers/Admin/ProgramController.php b/app/Http/Controllers/Admin/ProgramController.php index 065818c..e64a8b3 100644 --- a/app/Http/Controllers/Admin/ProgramController.php +++ b/app/Http/Controllers/Admin/ProgramController.php @@ -3,63 +3,135 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\StoreProgramRequest; +use App\Http\Requests\Admin\UpdateProgramRequest; +use App\Models\Program; +use App\Services\AuditLogService; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\View\View; class ProgramController extends Controller { - /** - * Display a listing of the resource. - */ - public function index() + public function index(Request $request): View { - // + $query = Program::with('creator') + ->withCount(['attendances', 'programParticipants']) + ->latest(); + + if ($request->filled('search')) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('organizer', 'like', '%' . $request->search . '%') + ->orWhere('location', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $programs = $query->paginate(15)->withQueryString(); + + return view('admin.programs.index', compact('programs')); } - /** - * Show the form for creating a new resource. - */ - public function create() + public function create(): View { - // + return view('admin.programs.create'); } - /** - * Store a newly created resource in storage. - */ - public function store(Request $request) + public function store(StoreProgramRequest $request): RedirectResponse { - // + $program = Program::create([ + ...$request->validated(), + 'created_by' => auth()->id(), + ]); + + AuditLogService::log('program.created', $program); + + return redirect() + ->route('admin.programs.show', $program) + ->with('success', 'Program "' . $program->title . '" berjaya ditambah.'); } - /** - * Display the specified resource. - */ - public function show(string $id) + public function show(Program $program): View { - // + $program->load([ + 'qrCode', + 'certificateTemplate', + 'questionnaire.questionnaireSet.questions', + ]); + + $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(), + ]; + + return view('admin.programs.show', compact('program', 'stats')); } - /** - * Show the form for editing the specified resource. - */ - public function edit(string $id) + public function edit(Program $program): View { - // + return view('admin.programs.edit', compact('program')); } - /** - * Update the specified resource in storage. - */ - public function update(Request $request, string $id) + public function update(UpdateProgramRequest $request, Program $program): RedirectResponse { - // + $old = $program->only([ + 'title', 'status', 'checkin_start_at', 'checkin_end_at', + 'ecert_download_start_at', 'ecert_download_end_at', + ]); + + $program->update($request->validated()); + + AuditLogService::log('program.updated', $program, $old); + + return redirect() + ->route('admin.programs.show', $program) + ->with('success', 'Maklumat program berjaya dikemas kini.'); } - /** - * Remove the specified resource from storage. - */ - public function destroy(string $id) + public function destroy(Program $program): RedirectResponse { - // + if ($program->attendances()->exists()) { + return back()->with('error', 'Program tidak boleh dipadam kerana sudah ada rekod kehadiran.'); + } + + $title = $program->title; + AuditLogService::log('program.deleted', $program); + $program->delete(); + + return redirect() + ->route('admin.programs.index') + ->with('success', 'Program "' . $title . '" berjaya dipadam.'); + } + + public function publish(Program $program): RedirectResponse + { + if ($program->status !== 'draft') { + return back()->with('error', 'Hanya program berstatus Draf boleh diterbitkan.'); + } + + $program->update(['status' => 'published']); + AuditLogService::log('program.published', $program); + + return back()->with('success', 'Program berjaya diterbitkan.'); + } + + public function close(Program $program): RedirectResponse + { + if ($program->status !== 'published') { + return back()->with('error', 'Hanya program berstatus Diterbitkan boleh ditutup.'); + } + + $program->update(['status' => 'closed']); + AuditLogService::log('program.closed', $program); + + return back()->with('success', 'Program berjaya ditutup.'); } } diff --git a/app/Http/Requests/Admin/StoreProgramRequest.php b/app/Http/Requests/Admin/StoreProgramRequest.php new file mode 100644 index 0000000..4b75d92 --- /dev/null +++ b/app/Http/Requests/Admin/StoreProgramRequest.php @@ -0,0 +1,56 @@ +check(); + } + + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string', 'max:5000'], + 'organizer' => ['required', 'string', 'max:255'], + 'location' => ['required', 'string', 'max:500'], + 'start_date' => ['required', 'date'], + 'end_date' => ['required', 'date', 'gte:start_date'], + 'checkin_start_at' => ['nullable', 'date'], + 'checkin_end_at' => ['nullable', 'date', 'after_or_equal:checkin_start_at'], + 'ecert_download_start_at' => ['nullable', 'date'], + 'ecert_download_end_at' => ['nullable', 'date', 'after_or_equal:ecert_download_start_at'], + 'allow_walk_in' => ['boolean'], + 'default_staff_session' => ['nullable', 'in:pagi,petang,full_day'], + 'default_external_session' => ['nullable', 'in:pagi,petang,full_day'], + ]; + } + + public function attributes(): array + { + return [ + 'title' => 'nama program', + 'organizer' => 'penganjur', + 'location' => 'lokasi', + 'start_date' => 'tarikh mula', + 'end_date' => 'tarikh tamat', + 'checkin_start_at' => 'masa mula check-in', + 'checkin_end_at' => 'masa tamat check-in', + 'ecert_download_start_at' => 'masa mula download sijil', + 'ecert_download_end_at' => 'masa tamat download sijil', + 'default_staff_session' => 'sesi default kakitangan', + 'default_external_session' => 'sesi default peserta luar', + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'allow_walk_in' => $this->boolean('allow_walk_in'), + ]); + } +} diff --git a/app/Http/Requests/Admin/UpdateProgramRequest.php b/app/Http/Requests/Admin/UpdateProgramRequest.php new file mode 100644 index 0000000..6cfca8a --- /dev/null +++ b/app/Http/Requests/Admin/UpdateProgramRequest.php @@ -0,0 +1,9 @@ + Limit::perMinute(60)->by($request->ip()) diff --git a/app/Services/AuditLogService.php b/app/Services/AuditLogService.php new file mode 100644 index 0000000..f2855c6 --- /dev/null +++ b/app/Services/AuditLogService.php @@ -0,0 +1,39 @@ + auth()->id(), + 'action' => $action, + 'auditable_type' => $model ? get_class($model) : null, + 'auditable_id' => $model?->getKey(), + 'old_values' => self::redact($oldValues), + 'new_values' => self::redact($newValues), + 'ip_address' => request()->ip(), + 'user_agent' => substr(request()->userAgent() ?? '', 0, 500), + ]); + } catch (\Throwable) { + // Audit log failure must not break the main flow. + } + } + + private static function redact(array $values): array + { + // Never log these sensitive fields. + $sensitive = ['no_kp', 'password', 'token', 'remember_token']; + + return array_diff_key($values, array_flip($sensitive)); + } +} diff --git a/resources/views/admin/programs/_form.blade.php b/resources/views/admin/programs/_form.blade.php new file mode 100644 index 0000000..6fe660f --- /dev/null +++ b/resources/views/admin/programs/_form.blade.php @@ -0,0 +1,174 @@ +{{-- + Shared form partial. + Variables expected: + $program — Program model (or null for create) + $action — form action URL + $method — PUT for edit, omit for create (POST) +--}} +
diff --git a/resources/views/admin/programs/create.blade.php b/resources/views/admin/programs/create.blade.php new file mode 100644 index 0000000..3466f8e --- /dev/null +++ b/resources/views/admin/programs/create.blade.php @@ -0,0 +1,15 @@ +@extends('layouts.admin') + +@section('title', 'Tambah Program Baru') +@section('header', 'Tambah Program Baru') + +@section('breadcrumb') +| Nama Program | +Tarikh | +Kehadiran | +Peserta | +Status | +Tindakan | +
|---|---|---|---|---|---|
|
+ {{ $program->title }}
+
+ {{ Str::limit($program->location, 40) }}
+
+ |
+
+ {{ $program->start_date->format('d M Y') }}
+ @if($program->start_date->ne($program->end_date))
+ - {{ $program->end_date->format('d M Y') }}
+ @endif
+ |
+ + + {{ $program->attendances_count }} + + | ++ + {{ $program->program_participants_count }} + + | ++ @include('admin.partials.program-status-badge', ['status' => $program->status]) + | ++ + | +
{{ route('public.checkin.show', $program->qrCode->token) }}
+ QR Code belum dijana untuk program ini.
+ +Belum ada template sijil untuk program ini.
+ + Upload Template + +{{ $qs->questions->count() }} soalan · {{ $qs->description }}
+ @else +Belum ada soalselidik dilampirkan untuk program ini.
+ + Lampir Soalselidik + +