import Link from "next/link"; import { WifiIcon, BellIcon, ActivityIcon, CheckCircle2Icon, AlertTriangleIcon, XCircleIcon, MinusCircleIcon, Trash2Icon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { clearHistoryAction } from "@/actions/history"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { getSeededOperator } from "@/lib/operator"; import { getDashboardStats } from "@/lib/queries"; import { PageShell } from "@/components/page-shell"; import { EmptyState } from "@/components/empty-state"; // --------------------------------------------------------------------------- // Time helpers (no external dep, server-safe) // --------------------------------------------------------------------------- function relativeTime(date: Date | string): string { const d = typeof date === "string" ? new Date(date) : date; const diffMs = Date.now() - d.getTime(); const diffSec = Math.floor(diffMs / 1000); const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); if (diffSec < 60) return rtf.format(-diffSec, "second"); if (diffSec < 3600) return rtf.format(-Math.floor(diffSec / 60), "minute"); if (diffSec < 86400) return rtf.format(-Math.floor(diffSec / 3600), "hour"); return rtf.format(-Math.floor(diffSec / 86400), "day"); } /** Absolute-time fallback used as a tooltip on relative-time displays. * 12-hour format with AM/PM so the user can read it at a glance. */ function absoluteTime(date: Date | string): string { const d = typeof date === "string" ? new Date(date) : date; return new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "numeric", minute: "2-digit", hour12: true, }).format(d); } // --------------------------------------------------------------------------- // Run-status pill // --------------------------------------------------------------------------- const RUN_STATUS_CONFIG: Record< string, { label: string; className: string; icon: React.ElementType } > = { success: { label: "Success", className: "bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border-transparent", icon: CheckCircle2Icon, }, partial: { label: "Partial", className: "bg-amber-500/15 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border-transparent", icon: AlertTriangleIcon, }, failed: { label: "Failed", className: "bg-red-500/15 text-red-600 dark:bg-red-500/20 dark:text-red-400 border-transparent", icon: XCircleIcon, }, skipped: { label: "Skipped", className: "bg-slate-200/60 text-slate-500 dark:bg-slate-700/40 dark:text-slate-400 border-transparent", icon: MinusCircleIcon, }, }; function RunStatusBadge({ status }: { status: string }) { const cfg = RUN_STATUS_CONFIG[status] ?? { label: status, className: "bg-secondary text-secondary-foreground border-transparent", icon: ActivityIcon, }; const Icon = cfg.icon; return ( {cfg.label} ); } // --------------------------------------------------------------------------- // Stat card — entire card is the link to its tab // --------------------------------------------------------------------------- function StatCard({ title, value, icon: Icon, description, href, }: { title: string; value: string | number; icon: React.ElementType; description?: string; href: string; }) { return ( {title} {value} {description && ( {description} )} ); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export default async function DashboardPage() { const op = await getSeededOperator(); const stats = await getDashboardStats(op.id); const hasRuns = stats.recentRuns.length > 0; return ( {/* Stat cards — click to drill into the corresponding tab */} {/* Recent activity */} Recent activity {hasRuns && ( {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} View all )} {hasRuns && ( Clear history Clear all run history? This permanently removes every reminder run record, including runs from reminders that have already been deleted. Reminders themselves are not affected. Yes, clear history )} {hasRuns ? ( <> {/* Mobile: card list — clickable when the reminder still exists */} {stats.recentRuns.map((run) => { const body = ( {run.name} {run.is_deleted && ( (deleted) )} {absoluteTime(run.fired_at)} · {relativeTime(run.fired_at)} ); return run.reminder_id && !run.is_deleted ? ( {body} ) : ( {body} ); })} {/* Desktop: table — rows are clickable when reminder still exists */} Reminder Status Fired {stats.recentRuns.map((run) => { const clickable = run.reminder_id && !run.is_deleted; return ( {clickable ? ( {run.name} ) : ( {run.name} )} {absoluteTime(run.fired_at)} {relativeTime(run.fired_at)} ); })} > ) : ( {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} Schedule a reminder } /> )} ); }
{value}
{run.name} {run.is_deleted && ( (deleted) )}