"use client"; import { useState } from "react"; import { useTranslations, useFormatter } from "next-intl"; import { Card } from "@/components/ui/card"; import type { CronRun } from "@/types"; interface Props { initialRecent: CronRun[]; initialLastSuccess: { monthlyIssue: CronRun | null; reminders: CronRun | null; }; } /** * Admin cron dashboard. Server pre-loads `initialRecent` and * `initialLastSuccess`; "Run now" clicks POST to the admin * endpoints, then re-fetch the history via GET /api/admin/cron/runs. * * The trigger buttons disable while busy and surface the resulting * counters inline so the admin gets immediate feedback without * needing to scroll to the history table. */ export function CronControls({ initialRecent, initialLastSuccess }: Props) { const t = useTranslations("adminCron"); const fmt = useFormatter(); const [recent, setRecent] = useState(initialRecent); const [lastSuccess, setLastSuccess] = useState(initialLastSuccess); const [busy, setBusy] = useState(null); const [flash, setFlash] = useState(null); const refresh = async () => { try { const res = await fetch("/api/admin/cron/runs"); if (!res.ok) return; const data = await res.json(); setRecent(data.recent); setLastSuccess(data.lastSuccess); } catch { // swallow — refresh is opportunistic } }; const triggerIssue = async () => { setBusy("issue"); setFlash(null); try { const res = await fetch("/api/admin/cron/issue-monthly", { method: "POST", }); const j = await res.json(); if (!res.ok) { setFlash({ kind: "issue", ok: false, summary: j.error ?? `HTTP ${res.status}`, }); } else { setFlash({ kind: "issue", ok: true, summary: t("flashIssueOk", { success: j.successCount, skipped: j.skippedCount, failure: j.failureCount, }), }); } await refresh(); } finally { setBusy(null); } }; const triggerReminders = async () => { setBusy("reminders"); setFlash(null); try { const res = await fetch("/api/admin/cron/send-reminders", { method: "POST", }); const j = await res.json(); if (!res.ok) { setFlash({ kind: "reminders", ok: false, summary: j.error ?? `HTTP ${res.status}`, }); } else { setFlash({ kind: "reminders", ok: true, summary: t("flashRemindersOk", { success: j.successCount, skipped: j.skippedCount, failure: j.failureCount, }), }); } await refresh(); } finally { setBusy(null); } }; const fmtRelative = (iso: string | null) => { if (!iso) return t("never"); return fmt.dateTime(new Date(iso), { dateStyle: "medium", timeStyle: "short", }); }; // Phase 6: surface failures prominently. Any run in the recent // window with a non-zero failure_count drives a top-of-page // banner — the row in the table is already red, but a banner // means the admin doesn't have to scroll to notice. const recentFailures = recent.filter((r) => r.failureCount > 0); const hasRecentFailures = recentFailures.length > 0; return (
{hasRecentFailures && (

{t("failureBannerTitle")}

{t("failureBannerBody", { count: recentFailures.length })}

)}

{t("monthlyIssue")}

{t("scheduleIssueLabel")}: {t("scheduleIssueValue")}

{t("lastSuccess")}: {fmtRelative(lastSuccess.monthlyIssue?.startedAt ?? null)}

{t("reminders")}

{t("scheduleReminderLabel")}: {t("scheduleReminderValue")}

{t("lastSuccess")}: {fmtRelative(lastSuccess.reminders?.startedAt ?? null)}

{flash && (
{flash.summary}
)}

{t("recentRuns")}

{recent.length === 0 ? (

{t("noRunsYet")}

) : ( {recent.map((r) => ( 0 ? "bg-error/5" : "" }`} > ))}
{t("startedCol")} {t("kindCol")} {t("triggeredByCol")} {t("okCol")} {t("skipCol")} {t("failCol")}
{fmtRelative(r.startedAt)} {t(`kind.${r.runKind}` as any)} {r.triggeredBy === "cron" ? t("triggeredByCron") : r.triggeredBy.slice(0, 8) + "…"} {r.successCount} {r.skippedCount} 0 ? "text-error" : "text-text-muted" }`} > {r.failureCount}
)}
); }