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(),
'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(),
'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')

View File

@@ -3,6 +3,8 @@
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;
@@ -40,6 +42,20 @@ class ParticipantController extends Controller
$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')
->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")
@@ -52,7 +68,7 @@ class ParticipantController extends Controller
'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

View File

@@ -82,7 +82,7 @@ class AttendanceCheckController extends Controller
// Masukkan queue hantar e-sijil jika sijil sudah dijana dan belum dihantar
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'))

View File

@@ -30,9 +30,15 @@ class SendCertificateEmailJob implements ShouldQueue
$email = $cert->participant->email;
if (! $email) {
EmailLog::where('certificate_id', $cert->id)->where('status', 'pending')->delete();
return;
}
$log = EmailLog::where('certificate_id', $cert->id)
->where('status', 'pending')
->latest()
->first();
try {
Mail::to($email)->send(new CertificateReadyMail($cert));
@@ -41,6 +47,9 @@ class SendCertificateEmailJob implements ShouldQueue
'emailed_at' => now(),
]);
if ($log) {
$log->update(['status' => 'sent', 'sent_at' => now()]);
} else {
EmailLog::create([
'program_id' => $cert->program_id,
'participant_id' => $cert->participant_id,
@@ -51,7 +60,11 @@ class SendCertificateEmailJob implements ShouldQueue
'status' => 'sent',
'sent_at' => now(),
]);
}
} catch (\Throwable $e) {
if ($log) {
$log->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
} else {
EmailLog::create([
'program_id' => $cert->program_id,
'participant_id' => $cert->participant_id,
@@ -62,20 +75,42 @@ class SendCertificateEmailJob implements ShouldQueue
'status' => 'failed',
'error_message' => $e->getMessage(),
]);
}
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
{
$program->certificates()
->whereIn('status', ['generated'])
->whereNull('emailed_at')
->with('participant')
->with(['participant', 'program'])
->each(function (Certificate $cert) {
if ($cert->participant->email) {
static::dispatch($cert);
static::dispatchForCert($cert);
}
});
}

View File

@@ -61,8 +61,8 @@
<div>
<div class="text-muted small">Soalselidik Dijawab</div>
<div class="fs-3 fw-bold">{{ $stats['total_responses'] }}</div>
@if($stats['pending_emails'] > 0)
<div class="text-warning small"><i class="bi bi-envelope-fill me-1"></i>{{ $stats['pending_emails'] }} emel tertunda</div>
@if($stats['emails_pending'] > 0)
<div class="text-warning small"><i class="bi bi-envelope-fill me-1"></i>{{ $stats['emails_pending'] }} emel tertunda</div>
@else
<div class="text-muted small">Tiada emel tertunda</div>
@endif
@@ -72,6 +72,69 @@
</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 --}}
<div class="row g-3">
<div class="col-lg-8">

View File

@@ -100,12 +100,17 @@
<th>Sesi</th>
<th>Sumber</th>
<th>Status</th>
<th>E-Sijil</th>
<th class="text-end">Tindakan</th>
</tr>
</thead>
<tbody>
@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>
<td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td>
<td>
@@ -143,6 +148,54 @@
<span class="badge bg-light text-dark border">Berdaftar</span>
@endif
</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">
@if($pp->status !== 'checked_in')
<form method="POST"