From 7027651dd7ceaafd704caf48ed75f215a0d6ed73 Mon Sep 17 00:00:00 2001 From: Saufi Date: Wed, 20 May 2026 10:20:59 +0800 Subject: [PATCH] fix status hantar emel dan jana sijil --- .../Admin/CertificateController.php | 27 ++++++ .../Controllers/Admin/DashboardController.php | 34 ++++--- .../Admin/ParticipantController.php | 12 +-- .../Controllers/Admin/ProgramController.php | 24 +++-- src/app/Jobs/SendCertificateEmailJob.php | 30 +++++-- src/app/Models/ProgramParticipant.php | 2 +- ...atus_sent_emel_to_program_participants.php | 26 ++++++ src/resources/views/admin/dashboard.blade.php | 89 ++++--------------- .../programs/certificates/index.blade.php | 4 +- .../programs/participants/index.blade.php | 63 +++++++------ .../views/admin/programs/show.blade.php | 63 ++++++++++++- src/routes/web.php | 7 +- 12 files changed, 240 insertions(+), 141 deletions(-) create mode 100644 src/database/migrations/2026_05_20_000001_add_status_sent_emel_to_program_participants.php diff --git a/src/app/Http/Controllers/Admin/CertificateController.php b/src/app/Http/Controllers/Admin/CertificateController.php index 283a678..fa209d7 100644 --- a/src/app/Http/Controllers/Admin/CertificateController.php +++ b/src/app/Http/Controllers/Admin/CertificateController.php @@ -8,6 +8,8 @@ use App\Models\Certificate; use App\Models\Program; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; class CertificateController extends Controller @@ -97,6 +99,31 @@ class CertificateController extends Controller return back()->with('success', "Penghantaran emel sijil dijadualkan untuk {$toEmail} peserta."); } + public function download(Program $program, Certificate $certificate): Response|RedirectResponse + { + if ($certificate->program_id !== $program->id) { + abort(404); + } + + if (! $certificate->isGenerated()) { + return back()->with('error', 'Sijil belum sedia untuk dimuat turun.'); + } + + if (! $certificate->file_path || ! Storage::disk('local')->exists($certificate->file_path)) { + return back()->with('error', 'Fail sijil tidak dijumpai.'); + } + + $certificate->loadMissing('participant'); + $content = Storage::disk('local')->get($certificate->file_path); + $filename = 'Sijil-' . str($certificate->participant->name)->slug() . '.jpg'; + + return response($content, 200, [ + 'Content-Type' => 'image/jpeg', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Length' => strlen($content), + ]); + } + private function buildCertNo(Program $program, int $seq): string { $year = now()->format('Y'); diff --git a/src/app/Http/Controllers/Admin/DashboardController.php b/src/app/Http/Controllers/Admin/DashboardController.php index f1c83c1..5a5098c 100644 --- a/src/app/Http/Controllers/Admin/DashboardController.php +++ b/src/app/Http/Controllers/Admin/DashboardController.php @@ -5,28 +5,36 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Attendance; use App\Models\Certificate; -use App\Models\EmailLog; use App\Models\Participant; use App\Models\Program; use App\Models\QuestionnaireResponse; +use Illuminate\Support\Facades\DB; class DashboardController extends Controller { public function index() { + // Sijil dijana tapi belum dihantar emel (ada emel peserta) + $emailsPending = DB::table('certificates') + ->join('participants', 'participants.id', '=', 'certificates.participant_id') + ->where('certificates.status', 'generated') + ->whereNull('certificates.emailed_at') + ->whereNotNull('participants.email') + ->count(); + $stats = [ - 'total_programs' => Program::count(), - 'active_programs' => Program::where('status', 'published')->count(), - 'total_participants' => Participant::count(), - 'total_attendances' => Attendance::count(), - 'total_certificates' => Certificate::count(), - 'generated_certs' => Certificate::whereIn('status', ['generated', 'emailed', 'downloaded'])->count(), - 'downloaded_certs' => Certificate::where('status', 'downloaded')->count(), - 'total_download_count'=> (int) Certificate::sum('download_count'), - 'total_responses' => QuestionnaireResponse::count(), - 'emails_pending' => EmailLog::where('status', 'pending')->count(), - 'emails_sent' => EmailLog::where('status', 'sent')->count(), - 'emails_failed' => EmailLog::where('status', 'failed')->count(), + 'total_programs' => Program::count(), + 'active_programs' => Program::where('status', 'published')->count(), + 'total_participants' => Participant::count(), + 'total_attendances' => Attendance::count(), + 'total_certificates' => Certificate::count(), + 'generated_certs' => Certificate::whereIn('status', ['generated', 'emailed', 'downloaded'])->count(), + 'downloaded_certs' => Certificate::where('status', 'downloaded')->count(), + 'total_download_count' => (int) Certificate::sum('download_count'), + 'total_responses' => QuestionnaireResponse::count(), + 'emails_pending' => $emailsPending, + 'emails_sent' => Certificate::whereNotNull('emailed_at')->count(), + 'emails_failed' => DB::table('program_participants')->where('status_sent_emel', 'failed')->count(), ]; $recentPrograms = Program::with('creator') diff --git a/src/app/Http/Controllers/Admin/ParticipantController.php b/src/app/Http/Controllers/Admin/ParticipantController.php index 17602d4..fe07bec 100644 --- a/src/app/Http/Controllers/Admin/ParticipantController.php +++ b/src/app/Http/Controllers/Admin/ParticipantController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Certificate; -use App\Models\EmailLog; use App\Models\Participant; use App\Models\Program; use App\Models\ProgramParticipant; @@ -42,20 +41,13 @@ class ParticipantController extends Controller $programParticipants = $query->paginate(20)->withQueryString(); - // Load certificates and latest email logs for displayed participants + // Load certificates for displayed participants $participantIds = $programParticipants->pluck('participant_id'); $certificates = Certificate::where('program_id', $program->id) ->whereIn('participant_id', $participantIds) ->get() ->keyBy('participant_id'); - $certIds = $certificates->pluck('id'); - $emailLogs = EmailLog::whereIn('certificate_id', $certIds) - ->orderByDesc('id') - ->get() - ->groupBy('certificate_id') - ->map->first(); - $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") @@ -68,7 +60,7 @@ class ParticipantController extends Controller 'checked_in' => (int) ($countRow->checked_in ?? 0), ]; - return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts', 'certificates', 'emailLogs')); + return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts', 'certificates')); } public function create(Program $program): View diff --git a/src/app/Http/Controllers/Admin/ProgramController.php b/src/app/Http/Controllers/Admin/ProgramController.php index f5acba6..b4cd8b0 100644 --- a/src/app/Http/Controllers/Admin/ProgramController.php +++ b/src/app/Http/Controllers/Admin/ProgramController.php @@ -78,16 +78,30 @@ class ProgramController extends Controller $certStats = \DB::table('certificates') ->where('program_id', $program->id) - ->selectRaw("COUNT(*) as total, SUM(status IN ('generated','emailed','downloaded')) as cert_generated") + ->selectRaw("COUNT(*) as total, SUM(status IN ('generated','emailed','downloaded')) as cert_generated, SUM(download_count) as total_downloads, SUM(status = 'downloaded') as downloaded") ->first(); + // Sijil dijana tapi belum dihantar emel (ada emel peserta) + $emailsPending = \DB::table('certificates') + ->join('participants', 'participants.id', '=', 'certificates.participant_id') + ->where('certificates.program_id', $program->id) + ->where('certificates.status', 'generated') + ->whereNull('certificates.emailed_at') + ->whereNotNull('participants.email') + ->count(); + $stats = [ - 'total_participants' => (int) ($ppStats->total ?? 0), - 'pre_registered' => (int) ($ppStats->pre_registered ?? 0), - 'walk_in' => (int) ($ppStats->walk_in ?? 0), + '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), + 'total_certificates' => (int) ($certStats->total ?? 0), 'generated_certificates' => (int) ($certStats->cert_generated ?? 0), + 'downloaded_certificates'=> (int) ($certStats->downloaded ?? 0), + 'total_downloads' => (int) ($certStats->total_downloads ?? 0), + 'emails_pending' => $emailsPending, + 'emails_sent' => (int) \DB::table('certificates')->where('program_id', $program->id)->whereNotNull('emailed_at')->count(), + 'emails_failed' => (int) \DB::table('program_participants')->where('program_id', $program->id)->where('status_sent_emel', 'failed')->count(), ]; return view('admin.programs.show', compact('program', 'stats')); diff --git a/src/app/Jobs/SendCertificateEmailJob.php b/src/app/Jobs/SendCertificateEmailJob.php index fde625a..195ac4a 100644 --- a/src/app/Jobs/SendCertificateEmailJob.php +++ b/src/app/Jobs/SendCertificateEmailJob.php @@ -6,6 +6,7 @@ use App\Mail\CertificateReadyMail; use App\Models\Certificate; use App\Models\EmailLog; use App\Models\Program; +use App\Models\ProgramParticipant; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -30,6 +31,7 @@ class SendCertificateEmailJob implements ShouldQueue $email = $cert->participant->email; if (! $email) { + $this->updatePpStatus($cert, null); EmailLog::where('certificate_id', $cert->id)->where('status', 'pending')->delete(); return; } @@ -42,10 +44,9 @@ class SendCertificateEmailJob implements ShouldQueue try { Mail::to($email)->send(new CertificateReadyMail($cert)); - $cert->update([ - 'status' => 'emailed', - 'emailed_at' => now(), - ]); + $cert->update(['status' => 'emailed', 'emailed_at' => now()]); + + $this->updatePpStatus($cert, 'sent'); if ($log) { $log->update(['status' => 'sent', 'sent_at' => now()]); @@ -62,6 +63,8 @@ class SendCertificateEmailJob implements ShouldQueue ]); } } catch (\Throwable $e) { + $this->updatePpStatus($cert, 'failed'); + if ($log) { $log->update(['status' => 'failed', 'error_message' => $e->getMessage()]); } else { @@ -82,13 +85,14 @@ class SendCertificateEmailJob implements ShouldQueue } /** - * Cipta pending EmailLog dahulu, kemudian dispatch job. - * Ini membolehkan dashboard tunjuk status "dalam antrian" sebelum job diproses. + * Cipta pending EmailLog dan set status_sent_emel = pending, kemudian dispatch job. */ public static function dispatchForCert(Certificate $cert): void { $cert->loadMissing(['participant', 'program']); + self::updatePpStatusStatic($cert, 'pending'); + EmailLog::create([ 'program_id' => $cert->program_id, 'participant_id' => $cert->participant_id, @@ -114,4 +118,18 @@ class SendCertificateEmailJob implements ShouldQueue } }); } + + private function updatePpStatus(Certificate $cert, ?string $status): void + { + ProgramParticipant::where('program_id', $cert->program_id) + ->where('participant_id', $cert->participant_id) + ->update(['status_sent_emel' => $status]); + } + + private static function updatePpStatusStatic(Certificate $cert, ?string $status): void + { + ProgramParticipant::where('program_id', $cert->program_id) + ->where('participant_id', $cert->participant_id) + ->update(['status_sent_emel' => $status]); + } } diff --git a/src/app/Models/ProgramParticipant.php b/src/app/Models/ProgramParticipant.php index a7db038..3f0de9b 100644 --- a/src/app/Models/ProgramParticipant.php +++ b/src/app/Models/ProgramParticipant.php @@ -9,7 +9,7 @@ class ProgramParticipant extends Model protected $fillable = [ 'program_id', 'participant_id', 'registration_source', 'is_pre_registered', 'pre_registered_session', - 'status', 'registered_at', + 'status', 'status_sent_emel', 'registered_at', ]; protected function casts(): array diff --git a/src/database/migrations/2026_05_20_000001_add_status_sent_emel_to_program_participants.php b/src/database/migrations/2026_05_20_000001_add_status_sent_emel_to_program_participants.php new file mode 100644 index 0000000..e60798a --- /dev/null +++ b/src/database/migrations/2026_05_20_000001_add_status_sent_emel_to_program_participants.php @@ -0,0 +1,26 @@ +enum('status_sent_emel', ['pending', 'sent', 'failed']) + ->nullable() + ->default(null) + ->after('status') + ->index(); + }); + } + + public function down(): void + { + Schema::table('program_participants', function (Blueprint $table) { + $table->dropColumn('status_sent_emel'); + }); + } +}; diff --git a/src/resources/views/admin/dashboard.blade.php b/src/resources/views/admin/dashboard.blade.php index 6118134..36610da 100644 --- a/src/resources/views/admin/dashboard.blade.php +++ b/src/resources/views/admin/dashboard.blade.php @@ -5,7 +5,7 @@ @section('content') -{{-- Stats Row 1 --}} +{{-- Stats Row --}}
@@ -56,78 +56,25 @@
- +
-
Soalselidik Dijawab
-
{{ $stats['total_responses'] }}
- @if($stats['emails_pending'] > 0) -
{{ $stats['emails_pending'] }} emel tertunda
- @else -
Tiada emel tertunda
- @endif -
-
-
-
-
- -{{-- Email & Download Status --}} -
-
-
-
- Status Penghantaran E-Sijil -
-
-
-
-
-
- -
-
-
Dalam Antrian
-
{{ $stats['emails_pending'] }}
-
menunggu / sedang cuba
-
-
-
-
-
-
- -
-
-
Berjaya Dihantar
-
{{ $stats['emails_sent'] }}
-
emel e-sijil
-
-
-
-
-
-
- -
-
-
Gagal Dihantar
-
{{ $stats['emails_failed'] }}
-
semua percubaan gagal
-
-
-
-
-
-
- -
-
-
Sijil Dimuat Turun
-
{{ $stats['downloaded_certs'] }}
-
{{ $stats['total_download_count'] }} kali klik pautan
-
-
+
E-Sijil Dihantar
+
{{ $stats['emails_sent'] }}
+
+ @if($stats['emails_pending'] > 0) + + {{ $stats['emails_pending'] }} belum dihantar + + @endif + @if($stats['emails_failed'] > 0) + + {{ $stats['emails_failed'] }} gagal + + @endif + @if($stats['emails_pending'] === 0 && $stats['emails_failed'] === 0) + Semua selesai + @endif
diff --git a/src/resources/views/admin/programs/certificates/index.blade.php b/src/resources/views/admin/programs/certificates/index.blade.php index 4f7af65..1ea34b9 100644 --- a/src/resources/views/admin/programs/certificates/index.blade.php +++ b/src/resources/views/admin/programs/certificates/index.blade.php @@ -45,7 +45,7 @@ @csrf @@ -117,7 +117,7 @@ - Belum ada sijil dijana. Klik "Jana Semua Sijil" untuk mula. + Belum ada sijil dijana. Klik "Jana E-Sijil" untuk mula. @endforelse diff --git a/src/resources/views/admin/programs/participants/index.blade.php b/src/resources/views/admin/programs/participants/index.blade.php index 0e4115e..20d05aa 100644 --- a/src/resources/views/admin/programs/participants/index.blade.php +++ b/src/resources/views/admin/programs/participants/index.blade.php @@ -107,9 +107,8 @@ @foreach($programParticipants as $i => $pp) @php - $p = $pp->participant; - $cert = $certificates[$pp->participant_id] ?? null; - $emailLog = $cert ? ($emailLogs[$cert->id] ?? null) : null; + $p = $pp->participant; + $cert = $certificates[$pp->participant_id] ?? null; @endphp {{ $programParticipants->firstItem() + $i }} @@ -168,23 +167,21 @@ @endif {{-- Emel --}} - @if($emailLog) - @if($emailLog->status === 'sent') -
- Emel Dihantar -
- @elseif($emailLog->status === 'failed') -
- Emel Gagal -
- @elseif($emailLog->status === 'pending') -
- Dalam Antrian -
- @endif - @elseif($cert->isGenerated()) + @if($pp->status_sent_emel === 'sent') +
+ Emel Dihantar +
+ @elseif($pp->status_sent_emel === 'failed') +
+ Emel Gagal +
+ @elseif($pp->status_sent_emel === 'pending') +
+ Dalam Antrian +
+ @elseif($cert && $cert->isGenerated() && ! $cert->emailed_at)
- Belum Dihantar + Belum Dihantar
@endif @@ -197,16 +194,24 @@ @endif - @if($pp->status !== 'checked_in') -
- @csrf @method('DELETE') - -
- @endif +
+ @if($cert && $cert->isGenerated()) + + + + @endif + @if($pp->status !== 'checked_in') +
+ @csrf @method('DELETE') + +
+ @endif +
@endforeach diff --git a/src/resources/views/admin/programs/show.blade.php b/src/resources/views/admin/programs/show.blade.php index 9e012d7..cd436dc 100644 --- a/src/resources/views/admin/programs/show.blade.php +++ b/src/resources/views/admin/programs/show.blade.php @@ -107,6 +107,60 @@
+{{-- Status Penghantaran E-Sijil --}} +
+
+ Status Penghantaran E-Sijil + @if($stats['emails_sent'] + $stats['emails_pending'] + $stats['emails_failed'] === 0) + Belum Dihantar + @endif +
+
+
+
+
+ +
+
Belum Dihantar
+
{{ $stats['emails_pending'] }}
+
sijil sedia, emel belum hantar
+
+
+
+
+
+ +
+
Berjaya Dihantar
+
{{ $stats['emails_sent'] }}
+
emel e-sijil
+
+
+
+
+
+ +
+
Gagal Dihantar
+
{{ $stats['emails_failed'] }}
+
semua percubaan gagal
+
+
+
+
+
+ +
+
Sijil Dimuat Turun
+
{{ $stats['downloaded_certificates'] }}
+
{{ $stats['total_downloads'] }} kali klik pautan
+
+
+
+
+
+
+ {{-- Tab Navigation --}}
-
+
Import CSV +
+ @csrf + +
Tambah Peserta diff --git a/src/routes/web.php b/src/routes/web.php index d6523d2..54ca317 100644 --- a/src/routes/web.php +++ b/src/routes/web.php @@ -86,9 +86,10 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun // Certificates (Admin) Route::prefix('programs/{program:uuid}/certificates')->name('programs.certificates.')->group(function () { - Route::get('/', [AdminCertificateController::class, 'index'])->name('index'); - Route::post('/generate-all', [AdminCertificateController::class, 'generateAll'])->name('generate-all'); - Route::post('/email-all', [AdminCertificateController::class, 'emailAll'])->name('email-all'); + Route::get('/', [AdminCertificateController::class, 'index'])->name('index'); + Route::post('/generate-all', [AdminCertificateController::class, 'generateAll'])->name('generate-all'); + Route::post('/email-all', [AdminCertificateController::class, 'emailAll'])->name('email-all'); + Route::get('/{certificate}/download', [AdminCertificateController::class, 'download'])->name('download'); }); // User Management (Super Admin only)