fix status hantar emel dan jana sijil
This commit is contained in:
@@ -8,6 +8,8 @@ use App\Models\Certificate;
|
|||||||
use App\Models\Program;
|
use App\Models\Program;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class CertificateController extends Controller
|
class CertificateController extends Controller
|
||||||
@@ -97,6 +99,31 @@ class CertificateController extends Controller
|
|||||||
return back()->with('success', "Penghantaran emel sijil dijadualkan untuk {$toEmail} peserta.");
|
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
|
private function buildCertNo(Program $program, int $seq): string
|
||||||
{
|
{
|
||||||
$year = now()->format('Y');
|
$year = now()->format('Y');
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Models\Certificate;
|
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\QuestionnaireResponse;
|
use App\Models\QuestionnaireResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
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 = [
|
$stats = [
|
||||||
'total_programs' => Program::count(),
|
'total_programs' => Program::count(),
|
||||||
'active_programs' => Program::where('status', 'published')->count(),
|
'active_programs' => Program::where('status', 'published')->count(),
|
||||||
@@ -22,11 +30,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_download_count' => (int) Certificate::sum('download_count'),
|
||||||
'total_responses' => QuestionnaireResponse::count(),
|
'total_responses' => QuestionnaireResponse::count(),
|
||||||
'emails_pending' => EmailLog::where('status', 'pending')->count(),
|
'emails_pending' => $emailsPending,
|
||||||
'emails_sent' => EmailLog::where('status', 'sent')->count(),
|
'emails_sent' => Certificate::whereNotNull('emailed_at')->count(),
|
||||||
'emails_failed' => EmailLog::where('status', 'failed')->count(),
|
'emails_failed' => DB::table('program_participants')->where('status_sent_emel', 'failed')->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$recentPrograms = Program::with('creator')
|
$recentPrograms = Program::with('creator')
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Certificate;
|
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;
|
||||||
@@ -42,20 +41,13 @@ class ParticipantController extends Controller
|
|||||||
|
|
||||||
$programParticipants = $query->paginate(20)->withQueryString();
|
$programParticipants = $query->paginate(20)->withQueryString();
|
||||||
|
|
||||||
// Load certificates and latest email logs for displayed participants
|
// Load certificates for displayed participants
|
||||||
$participantIds = $programParticipants->pluck('participant_id');
|
$participantIds = $programParticipants->pluck('participant_id');
|
||||||
$certificates = Certificate::where('program_id', $program->id)
|
$certificates = Certificate::where('program_id', $program->id)
|
||||||
->whereIn('participant_id', $participantIds)
|
->whereIn('participant_id', $participantIds)
|
||||||
->get()
|
->get()
|
||||||
->keyBy('participant_id');
|
->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")
|
||||||
@@ -68,7 +60,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', 'certificates', 'emailLogs'));
|
return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts', 'certificates'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(Program $program): View
|
public function create(Program $program): View
|
||||||
|
|||||||
@@ -78,9 +78,18 @@ class ProgramController extends Controller
|
|||||||
|
|
||||||
$certStats = \DB::table('certificates')
|
$certStats = \DB::table('certificates')
|
||||||
->where('program_id', $program->id)
|
->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();
|
->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 = [
|
$stats = [
|
||||||
'total_participants' => (int) ($ppStats->total ?? 0),
|
'total_participants' => (int) ($ppStats->total ?? 0),
|
||||||
'pre_registered' => (int) ($ppStats->pre_registered ?? 0),
|
'pre_registered' => (int) ($ppStats->pre_registered ?? 0),
|
||||||
@@ -88,6 +97,11 @@ class ProgramController extends Controller
|
|||||||
'total_attendances' => $program->attendances()->count(),
|
'total_attendances' => $program->attendances()->count(),
|
||||||
'total_certificates' => (int) ($certStats->total ?? 0),
|
'total_certificates' => (int) ($certStats->total ?? 0),
|
||||||
'generated_certificates' => (int) ($certStats->cert_generated ?? 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'));
|
return view('admin.programs.show', compact('program', 'stats'));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Mail\CertificateReadyMail;
|
|||||||
use App\Models\Certificate;
|
use App\Models\Certificate;
|
||||||
use App\Models\EmailLog;
|
use App\Models\EmailLog;
|
||||||
use App\Models\Program;
|
use App\Models\Program;
|
||||||
|
use App\Models\ProgramParticipant;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
@@ -30,6 +31,7 @@ class SendCertificateEmailJob implements ShouldQueue
|
|||||||
$email = $cert->participant->email;
|
$email = $cert->participant->email;
|
||||||
|
|
||||||
if (! $email) {
|
if (! $email) {
|
||||||
|
$this->updatePpStatus($cert, null);
|
||||||
EmailLog::where('certificate_id', $cert->id)->where('status', 'pending')->delete();
|
EmailLog::where('certificate_id', $cert->id)->where('status', 'pending')->delete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -42,10 +44,9 @@ class SendCertificateEmailJob implements ShouldQueue
|
|||||||
try {
|
try {
|
||||||
Mail::to($email)->send(new CertificateReadyMail($cert));
|
Mail::to($email)->send(new CertificateReadyMail($cert));
|
||||||
|
|
||||||
$cert->update([
|
$cert->update(['status' => 'emailed', 'emailed_at' => now()]);
|
||||||
'status' => 'emailed',
|
|
||||||
'emailed_at' => now(),
|
$this->updatePpStatus($cert, 'sent');
|
||||||
]);
|
|
||||||
|
|
||||||
if ($log) {
|
if ($log) {
|
||||||
$log->update(['status' => 'sent', 'sent_at' => now()]);
|
$log->update(['status' => 'sent', 'sent_at' => now()]);
|
||||||
@@ -62,6 +63,8 @@ class SendCertificateEmailJob implements ShouldQueue
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
$this->updatePpStatus($cert, 'failed');
|
||||||
|
|
||||||
if ($log) {
|
if ($log) {
|
||||||
$log->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
|
$log->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
|
||||||
} else {
|
} else {
|
||||||
@@ -82,13 +85,14 @@ class SendCertificateEmailJob implements ShouldQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cipta pending EmailLog dahulu, kemudian dispatch job.
|
* Cipta pending EmailLog dan set status_sent_emel = pending, kemudian dispatch job.
|
||||||
* Ini membolehkan dashboard tunjuk status "dalam antrian" sebelum job diproses.
|
|
||||||
*/
|
*/
|
||||||
public static function dispatchForCert(Certificate $cert): void
|
public static function dispatchForCert(Certificate $cert): void
|
||||||
{
|
{
|
||||||
$cert->loadMissing(['participant', 'program']);
|
$cert->loadMissing(['participant', 'program']);
|
||||||
|
|
||||||
|
self::updatePpStatusStatic($cert, 'pending');
|
||||||
|
|
||||||
EmailLog::create([
|
EmailLog::create([
|
||||||
'program_id' => $cert->program_id,
|
'program_id' => $cert->program_id,
|
||||||
'participant_id' => $cert->participant_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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ProgramParticipant extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'program_id', 'participant_id',
|
'program_id', 'participant_id',
|
||||||
'registration_source', 'is_pre_registered', 'pre_registered_session',
|
'registration_source', 'is_pre_registered', 'pre_registered_session',
|
||||||
'status', 'registered_at',
|
'status', 'status_sent_emel', 'registered_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('program_participants', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
|
|
||||||
{{-- Stats Row 1 --}}
|
{{-- Stats Row --}}
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-sm-6 col-xl-3">
|
<div class="col-sm-6 col-xl-3">
|
||||||
<div class="card stat-card h-100">
|
<div class="card stat-card h-100">
|
||||||
@@ -56,78 +56,25 @@
|
|||||||
<div class="card stat-card h-100">
|
<div class="card stat-card h-100">
|
||||||
<div class="card-body d-flex align-items-center gap-3">
|
<div class="card-body d-flex align-items-center gap-3">
|
||||||
<div class="stat-icon bg-info bg-opacity-10">
|
<div class="stat-icon bg-info bg-opacity-10">
|
||||||
<i class="bi bi-clipboard2-check-fill text-info"></i>
|
<i class="bi bi-envelope-fill text-info"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-muted small">Soalselidik Dijawab</div>
|
<div class="text-muted small">E-Sijil Dihantar</div>
|
||||||
<div class="fs-3 fw-bold">{{ $stats['total_responses'] }}</div>
|
<div class="fs-3 fw-bold">{{ $stats['emails_sent'] }}</div>
|
||||||
|
<div class="d-flex gap-2 flex-wrap mt-1">
|
||||||
@if($stats['emails_pending'] > 0)
|
@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>
|
<span class="badge bg-warning-subtle text-warning border border-warning-subtle" style="font-size:.65rem;">
|
||||||
@else
|
<i class="bi bi-clock me-1"></i>{{ $stats['emails_pending'] }} belum dihantar
|
||||||
<div class="text-muted small">Tiada emel tertunda</div>
|
</span>
|
||||||
|
@endif
|
||||||
|
@if($stats['emails_failed'] > 0)
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle" style="font-size:.65rem;">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>{{ $stats['emails_failed'] }} gagal
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if($stats['emails_pending'] === 0 && $stats['emails_failed'] === 0)
|
||||||
|
<span class="text-muted" style="font-size:.7rem;">Semua selesai</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
@csrf
|
@csrf
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary"
|
||||||
onclick="return confirm('Jana sijil untuk semua peserta hadir?')">
|
onclick="return confirm('Jana sijil untuk semua peserta hadir?')">
|
||||||
<i class="bi bi-gear me-1"></i> Jana Semua Sijil
|
<i class="bi bi-gear me-1"></i> Jana E-Sijil
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center py-5 text-muted">
|
<td colspan="6" class="text-center py-5 text-muted">
|
||||||
<i class="bi bi-award d-block fs-1 mb-3 opacity-25"></i>
|
<i class="bi bi-award d-block fs-1 mb-3 opacity-25"></i>
|
||||||
Belum ada sijil dijana. Klik "Jana Semua Sijil" untuk mula.
|
Belum ada sijil dijana. Klik "Jana E-Sijil" untuk mula.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
|
|||||||
@@ -109,7 +109,6 @@
|
|||||||
@php
|
@php
|
||||||
$p = $pp->participant;
|
$p = $pp->participant;
|
||||||
$cert = $certificates[$pp->participant_id] ?? null;
|
$cert = $certificates[$pp->participant_id] ?? null;
|
||||||
$emailLog = $cert ? ($emailLogs[$cert->id] ?? null) : null;
|
|
||||||
@endphp
|
@endphp
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td>
|
<td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td>
|
||||||
@@ -168,23 +167,21 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Emel --}}
|
{{-- Emel --}}
|
||||||
@if($emailLog)
|
@if($pp->status_sent_emel === 'sent')
|
||||||
@if($emailLog->status === 'sent')
|
|
||||||
<div class="text-success mt-1" style="font-size:.7rem;">
|
<div class="text-success mt-1" style="font-size:.7rem;">
|
||||||
<i class="bi bi-envelope-check me-1"></i>Emel Dihantar
|
<i class="bi bi-envelope-check me-1"></i>Emel Dihantar
|
||||||
</div>
|
</div>
|
||||||
@elseif($emailLog->status === 'failed')
|
@elseif($pp->status_sent_emel === 'failed')
|
||||||
<div class="text-danger mt-1" style="font-size:.7rem;">
|
<div class="text-danger mt-1" style="font-size:.7rem;">
|
||||||
<i class="bi bi-envelope-x me-1"></i>Emel Gagal
|
<i class="bi bi-envelope-x me-1"></i>Emel Gagal
|
||||||
</div>
|
</div>
|
||||||
@elseif($emailLog->status === 'pending')
|
@elseif($pp->status_sent_emel === 'pending')
|
||||||
<div class="text-warning mt-1" style="font-size:.7rem;">
|
<div class="text-warning mt-1" style="font-size:.7rem;">
|
||||||
<i class="bi bi-hourglass-split me-1"></i>Dalam Antrian
|
<i class="bi bi-hourglass-split me-1"></i>Dalam Antrian
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@elseif($cert && $cert->isGenerated() && ! $cert->emailed_at)
|
||||||
@elseif($cert->isGenerated())
|
|
||||||
<div class="text-muted mt-1" style="font-size:.7rem;">
|
<div class="text-muted mt-1" style="font-size:.7rem;">
|
||||||
<i class="bi bi-envelope me-1"></i>Belum Dihantar
|
<i class="bi bi-clock me-1"></i>Belum Dihantar
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@@ -197,6 +194,13 @@
|
|||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
|
<div class="d-flex justify-content-end gap-1">
|
||||||
|
@if($cert && $cert->isGenerated())
|
||||||
|
<a href="{{ route('admin.programs.certificates.download', [$program, $cert]) }}"
|
||||||
|
class="btn btn-sm btn-outline-primary" title="Muat Turun Sijil">
|
||||||
|
<i class="bi bi-download"></i>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
@if($pp->status !== 'checked_in')
|
@if($pp->status !== 'checked_in')
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ route('admin.programs.participants.destroy', [$program, $pp]) }}"
|
action="{{ route('admin.programs.participants.destroy', [$program, $pp]) }}"
|
||||||
@@ -207,6 +211,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@@ -107,6 +107,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Status Penghantaran E-Sijil --}}
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-semibold"><i class="bi bi-envelope-fill me-2 text-primary"></i>Status Penghantaran E-Sijil</span>
|
||||||
|
@if($stats['emails_sent'] + $stats['emails_pending'] + $stats['emails_failed'] === 0)
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Belum Dihantar</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<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">
|
||||||
|
<i class="bi bi-clock text-secondary fs-3 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">Belum Dihantar</div>
|
||||||
|
<div class="fs-3 fw-bold lh-1">{{ $stats['emails_pending'] }}</div>
|
||||||
|
<div class="text-muted" style="font-size:.7rem;">sijil sedia, emel belum hantar</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">
|
||||||
|
<i class="bi bi-envelope-check-fill text-success fs-3 flex-shrink-0"></i>
|
||||||
|
<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">
|
||||||
|
<i class="bi bi-envelope-x-fill text-danger fs-3 flex-shrink-0"></i>
|
||||||
|
<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">
|
||||||
|
<i class="bi bi-download text-warning fs-3 flex-shrink-0"></i>
|
||||||
|
<div>
|
||||||
|
<div class="text-muted small">Sijil Dimuat Turun</div>
|
||||||
|
<div class="fs-3 fw-bold lh-1">{{ $stats['downloaded_certificates'] }}</div>
|
||||||
|
<div class="text-muted" style="font-size:.7rem;">{{ $stats['total_downloads'] }} kali klik pautan</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Tab Navigation --}}
|
{{-- Tab Navigation --}}
|
||||||
<ul class="nav nav-tabs mb-0" id="programTabs">
|
<ul class="nav nav-tabs mb-0" id="programTabs">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@@ -148,10 +202,17 @@
|
|||||||
{{ $stats['walk_in'] }} walk-in
|
{{ $stats['walk_in'] }} walk-in
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<a href="{{ route('admin.programs.participants.import.form', $program) }}" class="btn btn-sm btn-outline-secondary">
|
<a href="{{ route('admin.programs.participants.import.form', $program) }}" class="btn btn-sm btn-outline-secondary">
|
||||||
<i class="bi bi-upload me-1"></i> Import CSV
|
<i class="bi bi-upload me-1"></i> Import CSV
|
||||||
</a>
|
</a>
|
||||||
|
<form method="POST" action="{{ route('admin.programs.certificates.generate-all', $program) }}" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<button class="btn btn-sm btn-outline-warning"
|
||||||
|
onclick="return confirm('Jana e-sijil untuk semua peserta yang hadir?')">
|
||||||
|
<i class="bi bi-gear me-1"></i> Jana E-Sijil
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<a href="{{ route('admin.programs.participants.create', $program) }}" class="btn btn-sm btn-primary">
|
<a href="{{ route('admin.programs.participants.create', $program) }}" class="btn btn-sm btn-primary">
|
||||||
<i class="bi bi-person-plus me-1"></i> Tambah Peserta
|
<i class="bi bi-person-plus me-1"></i> Tambah Peserta
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
|
|||||||
Route::get('/', [AdminCertificateController::class, 'index'])->name('index');
|
Route::get('/', [AdminCertificateController::class, 'index'])->name('index');
|
||||||
Route::post('/generate-all', [AdminCertificateController::class, 'generateAll'])->name('generate-all');
|
Route::post('/generate-all', [AdminCertificateController::class, 'generateAll'])->name('generate-all');
|
||||||
Route::post('/email-all', [AdminCertificateController::class, 'emailAll'])->name('email-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)
|
// User Management (Super Admin only)
|
||||||
|
|||||||
Reference in New Issue
Block a user