import { redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { emailRepo } from "@/server/repositories/email.repo"; import { RetryEmailButton, ResendEmailButton, } from "@/features/email/components/email-row-actions"; type Tab = "failed" | "queue" | "sent"; const TABS: { key: Tab; label: string }[] = [ { key: "failed", label: "Gagal" }, { key: "queue", label: "Antrian" }, { key: "sent", label: "Terkirim" }, ]; interface PageProps { searchParams: Promise<{ tab?: string; to?: string; template?: string }>; } export default async function AdminEmailsPage({ searchParams }: PageProps) { const session = await getServerSession(authOptions); if (!session?.user) redirect("/login?callbackUrl=/admin/emails"); if (!isAdminEmail(session.user.email)) { return (

Halaman ini hanya untuk admin SeTrip.

); } const params = await searchParams; const tab: Tab = TABS.some((t) => t.key === params.tab) ? (params.tab as Tab) : "failed"; const filters = { to: params.to?.trim() || undefined, template: params.template?.trim() || undefined, }; const stats = await emailRepo.stats(); const jobs = tab === "sent" ? [] : await emailRepo.listJobs( tab === "failed" ? ["FAILED"] : ["PENDING", "PROCESSING"], filters ); const sent = tab === "sent" ? await emailRepo.listSent(filters) : []; return (

Email Log

Pantau pengiriman email transaksional. Email yang gagal dikirim bisa di-retry manual; email terkirim bisa di-resend kalau peserta lapor tidak menerima.

{/* Kartu ringkasan */}
0 ? "amber" : "ok"} hint="Job menunggu cron / retry" /> 0 ? "red" : "ok"} hint="Job gagal dalam sehari terakhir" /> 0 ? "red" : "ok"} hint="Gagal & habis 5 attempt — cron berhenti retry" />
{/* Tabs */}
{TABS.map((t) => ( {t.label} ))}
{/* Filter */}
{(filters.to || filters.template) && ( Reset )}
{tab === "sent" ? ( ) : ( )}
); } function StatCard({ label, value, tone, hint, }: { label: string; value: number; tone: "ok" | "amber" | "red"; hint: string; }) { const cls = tone === "red" ? "border-red-200 bg-red-50/60" : tone === "amber" ? "border-amber-200 bg-amber-50/60" : "border-emerald-200 bg-emerald-50/50"; const valueCls = tone === "red" ? "text-red-700" : tone === "amber" ? "text-amber-700" : "text-emerald-700"; return (

{label}

{value}

{hint}

); } function JobTable({ rows, tab, }: { rows: Awaited>; tab: "failed" | "queue"; }) { if (rows.length === 0) { return ( ); } return (
{rows.map((r) => ( ))}
Penerima Template Status Attempt {tab === "failed" ? "Error terakhir" : "Dijadwalkan"} Aksi
{r.to} {r.template} {r.attempts} {r.attempts >= 5 && ( (mati) )} {tab === "failed" ? r.lastError ? truncate(r.lastError, 90) : "—" : formatDateTime(r.scheduledAt)}
); } function SentTable({ rows, }: { rows: Awaited>; }) { if (rows.length === 0) { return ; } return (
{rows.map((r) => ( ))}
Penerima Template Subject Terkirim Aksi
{r.to} {r.template} {truncate(r.subject, 60)} {formatDateTime(r.sentAt)}
); } function EmptyState({ message }: { message: string }) { return (

{message}

); } function EmailBadge({ value }: { value: string }) { const cls = value === "SUCCESS" ? "bg-emerald-100 text-emerald-800" : value === "FAILED" ? "bg-red-100 text-red-800" : "bg-amber-100 text-amber-800"; return ( {value} ); } function formatDateTime(d: Date): string { return d.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function truncate(s: string, max: number): string { return s.length > max ? `${s.slice(0, max)}…` : s; }