status emel

This commit is contained in:
Saufi
2026-05-20 09:11:51 +08:00
parent 6b2769d506
commit 899507070c
6 changed files with 198 additions and 28 deletions

View File

@@ -22,8 +22,11 @@ class DashboardController extends Controller
'total_certificates' => Certificate::count(), 'total_certificates' => Certificate::count(),
'generated_certs' => Certificate::whereIn('status', ['generated', 'emailed', 'downloaded'])->count(), 'generated_certs' => Certificate::whereIn('status', ['generated', 'emailed', 'downloaded'])->count(),
'downloaded_certs' => Certificate::where('status', 'downloaded')->count(), 'downloaded_certs' => Certificate::where('status', 'downloaded')->count(),
'total_download_count'=> (int) Certificate::sum('download_count'),
'total_responses' => QuestionnaireResponse::count(), 'total_responses' => QuestionnaireResponse::count(),
'pending_emails' => EmailLog::where('status', 'pending')->count(), 'emails_pending' => EmailLog::where('status', 'pending')->count(),
'emails_sent' => EmailLog::where('status', 'sent')->count(),
'emails_failed' => EmailLog::where('status', 'failed')->count(),
]; ];
$recentPrograms = Program::with('creator') $recentPrograms = Program::with('creator')

View File

@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Certificate;
use App\Models\EmailLog;
use App\Models\Participant; use App\Models\Participant;
use App\Models\Program; use App\Models\Program;
use App\Models\ProgramParticipant; use App\Models\ProgramParticipant;
@@ -40,6 +42,20 @@ class ParticipantController extends Controller
$programParticipants = $query->paginate(20)->withQueryString(); $programParticipants = $query->paginate(20)->withQueryString();
// Load certificates and latest email logs 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') $countRow = DB::table('program_participants')
->where('program_id', $program->id) ->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") ->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")
@@ -52,7 +68,7 @@ class ParticipantController extends Controller
'checked_in' => (int) ($countRow->checked_in ?? 0), 'checked_in' => (int) ($countRow->checked_in ?? 0),
]; ];
return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts')); return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts', 'certificates', 'emailLogs'));
} }
public function create(Program $program): View public function create(Program $program): View

View File

@@ -82,7 +82,7 @@ class AttendanceCheckController extends Controller
// Masukkan queue hantar e-sijil jika sijil sudah dijana dan belum dihantar // Masukkan queue hantar e-sijil jika sijil sudah dijana dan belum dihantar
if ($certificate && $certificate->status === 'generated' && ! $certificate->emailed_at) { if ($certificate && $certificate->status === 'generated' && ! $certificate->emailed_at) {
SendCertificateEmailJob::dispatch($certificate); SendCertificateEmailJob::dispatchForCert($certificate);
} }
return view('public.semak.result', compact('program', 'qrCode', 'participant', 'attendance', 'certificate')) return view('public.semak.result', compact('program', 'qrCode', 'participant', 'attendance', 'certificate'))

View File

@@ -30,9 +30,15 @@ class SendCertificateEmailJob implements ShouldQueue
$email = $cert->participant->email; $email = $cert->participant->email;
if (! $email) { if (! $email) {
EmailLog::where('certificate_id', $cert->id)->where('status', 'pending')->delete();
return; return;
} }
$log = EmailLog::where('certificate_id', $cert->id)
->where('status', 'pending')
->latest()
->first();
try { try {
Mail::to($email)->send(new CertificateReadyMail($cert)); Mail::to($email)->send(new CertificateReadyMail($cert));
@@ -41,6 +47,9 @@ class SendCertificateEmailJob implements ShouldQueue
'emailed_at' => now(), 'emailed_at' => now(),
]); ]);
if ($log) {
$log->update(['status' => 'sent', 'sent_at' => now()]);
} else {
EmailLog::create([ EmailLog::create([
'program_id' => $cert->program_id, 'program_id' => $cert->program_id,
'participant_id' => $cert->participant_id, 'participant_id' => $cert->participant_id,
@@ -51,7 +60,11 @@ class SendCertificateEmailJob implements ShouldQueue
'status' => 'sent', 'status' => 'sent',
'sent_at' => now(), 'sent_at' => now(),
]); ]);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($log) {
$log->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
} else {
EmailLog::create([ EmailLog::create([
'program_id' => $cert->program_id, 'program_id' => $cert->program_id,
'participant_id' => $cert->participant_id, 'participant_id' => $cert->participant_id,
@@ -62,20 +75,42 @@ class SendCertificateEmailJob implements ShouldQueue
'status' => 'failed', 'status' => 'failed',
'error_message' => $e->getMessage(), 'error_message' => $e->getMessage(),
]); ]);
}
throw $e; throw $e;
} }
} }
/**
* Cipta pending EmailLog dahulu, kemudian dispatch job.
* Ini membolehkan dashboard tunjuk status "dalam antrian" sebelum job diproses.
*/
public static function dispatchForCert(Certificate $cert): void
{
$cert->loadMissing(['participant', 'program']);
EmailLog::create([
'program_id' => $cert->program_id,
'participant_id' => $cert->participant_id,
'certificate_id' => $cert->id,
'recipient_email' => $cert->participant->email,
'subject' => 'Sijil Digital Program — ' . $cert->program->title,
'email_type' => 'certificate_ready',
'status' => 'pending',
]);
static::dispatch($cert);
}
public static function dispatchBatch(Program $program): void public static function dispatchBatch(Program $program): void
{ {
$program->certificates() $program->certificates()
->whereIn('status', ['generated']) ->whereIn('status', ['generated'])
->whereNull('emailed_at') ->whereNull('emailed_at')
->with('participant') ->with(['participant', 'program'])
->each(function (Certificate $cert) { ->each(function (Certificate $cert) {
if ($cert->participant->email) { if ($cert->participant->email) {
static::dispatch($cert); static::dispatchForCert($cert);
} }
}); });
} }

View File

@@ -61,8 +61,8 @@
<div> <div>
<div class="text-muted small">Soalselidik Dijawab</div> <div class="text-muted small">Soalselidik Dijawab</div>
<div class="fs-3 fw-bold">{{ $stats['total_responses'] }}</div> <div class="fs-3 fw-bold">{{ $stats['total_responses'] }}</div>
@if($stats['pending_emails'] > 0) @if($stats['emails_pending'] > 0)
<div class="text-warning small"><i class="bi bi-envelope-fill me-1"></i>{{ $stats['pending_emails'] }} emel tertunda</div> <div class="text-warning small"><i class="bi bi-envelope-fill me-1"></i>{{ $stats['emails_pending'] }} emel tertunda</div>
@else @else
<div class="text-muted small">Tiada emel tertunda</div> <div class="text-muted small">Tiada emel tertunda</div>
@endif @endif
@@ -72,6 +72,69 @@
</div> </div>
</div> </div>
{{-- Email & Download Status --}}
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-envelope-fill me-2 text-primary"></i>Status Penghantaran E-Sijil</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-6 col-md-3">
<div class="d-flex align-items-center gap-3 p-3 rounded-3 bg-secondary bg-opacity-10">
<div class="flex-shrink-0">
<i class="bi bi-hourglass-split text-secondary fs-3"></i>
</div>
<div>
<div class="text-muted small">Dalam Antrian</div>
<div class="fs-3 fw-bold lh-1">{{ $stats['emails_pending'] }}</div>
<div class="text-muted" style="font-size:.7rem;">menunggu / sedang cuba</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center gap-3 p-3 rounded-3 bg-success bg-opacity-10">
<div class="flex-shrink-0">
<i class="bi bi-envelope-check-fill text-success fs-3"></i>
</div>
<div>
<div class="text-muted small">Berjaya Dihantar</div>
<div class="fs-3 fw-bold lh-1">{{ $stats['emails_sent'] }}</div>
<div class="text-muted" style="font-size:.7rem;">emel e-sijil</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center gap-3 p-3 rounded-3 bg-danger bg-opacity-10">
<div class="flex-shrink-0">
<i class="bi bi-envelope-x-fill text-danger fs-3"></i>
</div>
<div>
<div class="text-muted small">Gagal Dihantar</div>
<div class="fs-3 fw-bold lh-1">{{ $stats['emails_failed'] }}</div>
<div class="text-muted" style="font-size:.7rem;">semua percubaan gagal</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="d-flex align-items-center gap-3 p-3 rounded-3 bg-warning bg-opacity-10">
<div class="flex-shrink-0">
<i class="bi bi-download text-warning fs-3"></i>
</div>
<div>
<div class="text-muted small">Sijil Dimuat Turun</div>
<div class="fs-3 fw-bold lh-1">{{ $stats['downloaded_certs'] }}</div>
<div class="text-muted" style="font-size:.7rem;">{{ $stats['total_download_count'] }} kali klik pautan</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- Recent Programs --}} {{-- Recent Programs --}}
<div class="row g-3"> <div class="row g-3">
<div class="col-lg-8"> <div class="col-lg-8">

View File

@@ -100,12 +100,17 @@
<th>Sesi</th> <th>Sesi</th>
<th>Sumber</th> <th>Sumber</th>
<th>Status</th> <th>Status</th>
<th>E-Sijil</th>
<th class="text-end">Tindakan</th> <th class="text-end">Tindakan</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach($programParticipants as $i => $pp) @foreach($programParticipants as $i => $pp)
@php $p = $pp->participant; @endphp @php
$p = $pp->participant;
$cert = $certificates[$pp->participant_id] ?? null;
$emailLog = $cert ? ($emailLogs[$cert->id] ?? null) : null;
@endphp
<tr> <tr>
<td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td> <td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td>
<td> <td>
@@ -143,6 +148,54 @@
<span class="badge bg-light text-dark border">Berdaftar</span> <span class="badge bg-light text-dark border">Berdaftar</span>
@endif @endif
</td> </td>
<td style="min-width:130px;">
@if(! $cert)
<span class="text-muted small"></span>
@else
{{-- Sijil --}}
@if(in_array($cert->status, ['generated','emailed','downloaded']))
<span class="badge bg-success-subtle text-success border border-success-subtle">
<i class="bi bi-award-fill me-1"></i>Jana
</span>
@elseif($cert->status === 'pending')
<span class="badge bg-warning-subtle text-warning border border-warning-subtle">
<i class="bi bi-hourglass-split me-1"></i>Menjana...
</span>
@elseif($cert->status === 'failed')
<span class="badge bg-danger-subtle text-danger border border-danger-subtle">
<i class="bi bi-x-circle me-1"></i>Gagal Jana
</span>
@endif
{{-- Emel --}}
@if($emailLog)
@if($emailLog->status === 'sent')
<div class="text-success mt-1" style="font-size:.7rem;">
<i class="bi bi-envelope-check me-1"></i>Emel Dihantar
</div>
@elseif($emailLog->status === 'failed')
<div class="text-danger mt-1" style="font-size:.7rem;">
<i class="bi bi-envelope-x me-1"></i>Emel Gagal
</div>
@elseif($emailLog->status === 'pending')
<div class="text-warning mt-1" style="font-size:.7rem;">
<i class="bi bi-hourglass-split me-1"></i>Dalam Antrian
</div>
@endif
@elseif($cert->isGenerated())
<div class="text-muted mt-1" style="font-size:.7rem;">
<i class="bi bi-envelope me-1"></i>Belum Dihantar
</div>
@endif
{{-- Muat turun --}}
@if($cert->download_count > 0)
<div class="text-primary mt-1" style="font-size:.7rem;">
<i class="bi bi-download me-1"></i>{{ $cert->download_count }}× Muat Turun
</div>
@endif
@endif
</td>
<td class="text-end"> <td class="text-end">
@if($pp->status !== 'checked_in') @if($pp->status !== 'checked_in')
<form method="POST" <form method="POST"