From 12324091dd891bd9818bbb3b940617f4849d6239 Mon Sep 17 00:00:00 2001 From: Saufi Date: Sat, 16 May 2026 19:42:33 +0800 Subject: [PATCH] feat: qr code generation - QrCodeService: generate unique 48-char token, create QR PNG (400x400, error-correction H) - QrCodeController: show, generate, download PNG, deactivate - Admin QR page: preview, copy URL, download, regenerate, deactivate - Existing active QR deactivated on regenerate - Token-based URL (not program ID) for PDPA compliance Co-Authored-By: Claude Sonnet 4.6 --- .../Controllers/Admin/QrCodeController.php | 54 +++++- app/Services/QrCodeService.php | 51 ++++++ resources/views/admin/programs/qr.blade.php | 171 ++++++++++++++++++ 3 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 app/Services/QrCodeService.php create mode 100644 resources/views/admin/programs/qr.blade.php diff --git a/app/Http/Controllers/Admin/QrCodeController.php b/app/Http/Controllers/Admin/QrCodeController.php index 6b102c2..dbb1d0b 100644 --- a/app/Http/Controllers/Admin/QrCodeController.php +++ b/app/Http/Controllers/Admin/QrCodeController.php @@ -3,9 +3,59 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; -use Illuminate\Http\Request; +use App\Models\Program; +use App\Services\AuditLogService; +use App\Services\QrCodeService; +use Illuminate\Support\Str; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Response; +use Illuminate\View\View; class QrCodeController extends Controller { - // + public function __construct(private QrCodeService $qrCodeService) {} + + public function show(Program $program): View + { + $qrCode = $program->qrCodes()->where('is_active', true)->latest()->first(); + + return view('admin.programs.qr', compact('program', 'qrCode')); + } + + public function generate(Program $program): RedirectResponse + { + $qrCode = $this->qrCodeService->generateForProgram($program); + + AuditLogService::log('qrcode.generated', $program); + + return redirect() + ->route('admin.programs.qr.show', $program) + ->with('success', 'QR Code berjaya dijana.'); + } + + public function download(Program $program): Response|RedirectResponse + { + $qrCode = $program->qrCodes()->where('is_active', true)->latest()->first(); + + if (! $qrCode) { + return back()->with('error', 'QR Code belum dijana.'); + } + + $png = $this->qrCodeService->getRawPng($qrCode); + $filename = 'QR_' . Str::slug($program->title) . '_' . now()->format('Ymd') . '.png'; + + return response($png, 200, [ + 'Content-Type' => 'image/png', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]); + } + + public function deactivate(Program $program): RedirectResponse + { + $program->qrCodes()->where('is_active', true)->update(['is_active' => false]); + + AuditLogService::log('qrcode.deactivated', $program); + + return back()->with('success', 'QR Code berjaya dinyahaktifkan.'); + } } diff --git a/app/Services/QrCodeService.php b/app/Services/QrCodeService.php new file mode 100644 index 0000000..876436d --- /dev/null +++ b/app/Services/QrCodeService.php @@ -0,0 +1,51 @@ +qrCodes()->where('is_active', true)->update(['is_active' => false]); + + $token = Str::random(48); + $url = route('public.checkin.show', $token); + $path = 'public/qrcodes/' . $token . '.png'; + $absPath = Storage::path($path); + + // Ensure directory exists + Storage::makeDirectory('public/qrcodes'); + + // Generate QR code PNG (400×400, with quiet zone) + $png = QrCode::format('png') + ->size(400) + ->margin(2) + ->errorCorrection('H') + ->generate($url); + + Storage::put($path, $png); + + return $program->qrCodes()->create([ + 'token' => $token, + 'qr_image_path' => $path, + 'is_active' => true, + ]); + } + + public function getPublicUrl(ProgramQrCode $qrCode): string + { + return Storage::url($qrCode->qr_image_path); + } + + public function getRawPng(ProgramQrCode $qrCode): string + { + return Storage::get($qrCode->qr_image_path); + } +} diff --git a/resources/views/admin/programs/qr.blade.php b/resources/views/admin/programs/qr.blade.php new file mode 100644 index 0000000..f0821e2 --- /dev/null +++ b/resources/views/admin/programs/qr.blade.php @@ -0,0 +1,171 @@ +@extends('layouts.admin') + +@section('title', 'QR Code — ' . $program->title) +@section('header', 'QR Code Program') + +@section('breadcrumb') + + + +@endsection + +@section('content') + +
+
+
+
+ QR Code Check-In +
+
+ + @if($qrCode) + {{-- QR Code Image --}} +
+ QR Code {{ $program->title }} +
+ + {{-- Program Info --}} +
{{ $program->title }}
+

+ {{ $program->start_date->format('d M Y') }} + @if($program->start_date->ne($program->end_date)) + — {{ $program->end_date->format('d M Y') }} + @endif + · {{ $program->location }} +

+ + {{-- Check-in URL --}} +
+
URL Check-In:
+ {{ route('public.checkin.show', $qrCode->token) }} + +
+ + {{-- Action Buttons --}} +
+ + Muat Turun PNG + + +
+ @csrf + +
+ +
+ @csrf + +
+
+ + {{-- Status --}} +
+ Aktif + Dijana {{ $qrCode->created_at->diffForHumans() }} +
+ + @else + {{-- No QR Code yet --}} +
+ +
QR Code belum dijana
+

+ Jana QR Code untuk membolehkan peserta scan dan check-in ke program ini. +

+
+ @csrf + +
+
+ @endif + +
+
+
+ + {{-- How to Use --}} +
+
+
+ Cara Penggunaan +
+
+
    +
  1. Jana QR Code untuk program ini.
  2. +
  3. Print atau papar QR Code di tempat program.
  4. +
  5. Peserta scan menggunakan kamera telefon.
  6. +
  7. Peserta isi maklumat check-in.
  8. +
  9. Sistem rekod kehadiran secara automatik.
  10. +
+
+
+ + QR Code mengandungi token unik — bukan ID program. +
+
+ + Jana semula akan membatalkan QR Code lama. +
+
+
+ +
+
+ Status Program +
+
+
+ Status + @include('admin.partials.program-status-badge', ['status' => $program->status]) +
+ @if($program->checkin_start_at) +
+ Check-In Dibuka + {{ $program->checkin_start_at->format('d/m H:i') }} +
+
+ Check-In Ditutup + {{ $program->checkin_end_at?->format('d/m H:i') ?? '—' }} +
+ @endif + @if($program->ecert_download_start_at) +
+ Download Sijil + {{ $program->ecert_download_start_at->format('d/m H:i') }} +
+ @endif +
+
+
+
+ +@endsection + +@push('scripts') + +@endpush