"use client"; import type { ReactElement, ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge"; import { cn } from "@/lib/utils"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { AdminDashboardDrawPanel, AdminDashboardSoldOutBuckets, } from "@/types/api/admin-dashboard"; export type SoldOutBuckets = AdminDashboardSoldOutBuckets; type MoneyFormatter = (minor: number, currency: string | null) => string; export function StatCard({ label, value, hint, icon, accent = "primary", }: { label: string; value: ReactNode; hint?: ReactNode; icon: ReactNode; accent?: "primary" | "destructive" | "muted"; }): ReactElement { const accentClass = accent === "destructive" ? "bg-destructive text-destructive-foreground" : accent === "muted" ? "bg-muted text-foreground" : "bg-primary text-primary-foreground"; return (
{icon}

{label}

{value}

{hint ?
{hint}
: null}
); } export function CapUsageBar({ locked, cap, usagePct, formatMoney, currency, }: { locked: number; cap: number; usagePct: number; formatMoney: MoneyFormatter; currency: string | null; }): ReactElement { const { t } = useTranslation("dashboard"); const pct = Math.min(100, Math.max(0, usagePct)); return (
{t("riskCapUsage")} {pct.toFixed(1)}%
= 90 ? "bg-destructive" : pct >= 70 ? "bg-amber-500" : "bg-primary", )} style={{ width: `${pct}%` }} />

{t("lockedAndCap", { locked: formatMoney(locked, currency), cap: formatMoney(cap, currency), })}

); } export function FinanceStructureChart({ finance, formatMoney, }: { finance: AdminDrawFinanceSummaryData; formatMoney: MoneyFormatter; }): ReactElement { const { t } = useTranslation("dashboard"); const currency = finance.currency_code; const bet = finance.total_bet_minor; const win = finance.total_win_payout_minor; const jackpot = finance.total_jackpot_win_minor; const payout = finance.total_payout_minor; const gross = finance.approx_house_gross_minor; if (bet <= 0) { return

{t("noFinanceActivity")}

; } const winW = (win / bet) * 100; const jpW = (jackpot / bet) * 100; const grossW = Math.max(0, (gross / bet) * 100); const payoutRate = ((payout / bet) * 100).toFixed(1); const segments = [ { key: "win", width: winW, className: "bg-emerald-500", label: t("winPayout"), value: win }, { key: "jackpot", width: jpW, className: "bg-violet-500", label: t("jackpotPayout"), value: jackpot }, { key: "gross", width: grossW, className: "bg-primary", label: t("houseGross"), value: gross }, ].filter((s) => s.width > 0.05); return (
{segments.map((s) => (
))}

{t("payoutRateOfBet", { rate: payoutRate })}

    {segments.map((s) => (
  • {s.label}

    {formatMoney(s.value, currency)}

  • ))}
); } export function PayoutCompositionChart({ finance, formatMoney, }: { finance: AdminDrawFinanceSummaryData; formatMoney: MoneyFormatter; }): ReactElement { const { t } = useTranslation("dashboard"); const currency = finance.currency_code; const win = finance.total_win_payout_minor; const jackpot = finance.total_jackpot_win_minor; const total = win + jackpot; if (total <= 0) { return

{t("noPayoutYet")}

; } const winPct = (win / total) * 100; const winColor = "oklch(0.62 0.17 162)"; const jackpotColor = "oklch(0.56 0.22 303)"; const items = [ { label: t("winPayout"), value: win, pct: winPct, className: "bg-emerald-500", color: winColor }, { label: t("jackpotPayout"), value: jackpot, pct: 100 - winPct, className: "bg-violet-500", color: jackpotColor, }, ]; return (
    {items.map((item) => (
  • {item.label} {item.pct.toFixed(1)}%

    {formatMoney(item.value, currency)}

  • ))}
); } export function HotUsageBars({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement { const { t } = useTranslation("dashboard"); if (rows.length === 0) { return

{t("noPoolData")}

; } return (
    {rows.map((row) => { const pct = Math.min(100, Math.max(0, (row.usage_ratio ?? 0) * 100)); return (
  • {row.normalized_number.trim()} {pct.toFixed(1)}%
    = 95 ? "bg-destructive" : pct >= 80 ? "bg-amber-500" : "bg-primary", )} style={{ width: `${pct}%` }} />
  • ); })}
); } export function SoldOutRing({ buckets }: { buckets: SoldOutBuckets }): ReactElement { const { t } = useTranslation("dashboard"); const entries: { key: keyof SoldOutBuckets; label: string; color: string; swatch: string }[] = [ { key: "d4", label: t("soldOutBuckets.d4"), color: "oklch(0.52 0.19 264)", swatch: "bg-blue-600" }, { key: "d3", label: t("soldOutBuckets.d3"), color: "oklch(0.62 0.17 162)", swatch: "bg-emerald-500" }, { key: "d2", label: t("soldOutBuckets.d2"), color: "oklch(0.72 0.16 75)", swatch: "bg-amber-500" }, { key: "special", label: t("soldOutBuckets.special"), color: "oklch(0.56 0.22 303)", swatch: "bg-violet-500" }, { key: "other", label: t("soldOutBuckets.other"), color: "oklch(0.58 0.2 25)", swatch: "bg-rose-500" }, ]; const total = entries.reduce((s, e) => s + buckets[e.key], 0); if (total === 0) { return

{t("noSoldOutNumbers")}

; } let acc = 0; const parts = entries .filter((e) => buckets[e.key] > 0) .map((e) => { const frac = buckets[e.key] / total; const start = acc; acc += frac; return { ...e, frac, start }; }); const gradientStops = parts.length === 1 ? `${parts[0].color} 0deg 360deg` : parts .map((p) => { const a0 = p.start * 360; const a1 = (p.start + p.frac) * 360; return `${p.color} ${a0}deg ${a1}deg`; }) .join(", "); return (

{total}

{t("soldOutTotal")}

    {entries.map((e) => { const count = buckets[e.key]; const pct = total > 0 ? (count / total) * 100 : 0; return (
  • {e.label} {count} ({pct.toFixed(0)}%)
  • ); })}
); } export function ResultBatchProgress({ draw }: { draw: AdminDashboardDrawPanel }): ReactElement { const { t } = useTranslation("dashboard"); const { total, pending_review, published } = draw.result_batch_counts; const pendingW = total > 0 ? (pending_review / total) * 100 : 0; const publishedW = total > 0 ? (published / total) * 100 : 0; const otherW = Math.max(0, 100 - pendingW - publishedW); return (
{pendingW > 0 ? (
) : null} {publishedW > 0 ? (
) : null} {otherW > 0 ?
: null}

{pending_review}

{t("batchPending")}

{published}

{t("batchPublished")}

{total}

{t("batchTotal")}

); } export function SettlementStatusChart({ finance, }: { finance: AdminDrawFinanceSummaryData; }): ReactElement { const { t } = useTranslation("dashboard"); const batches = finance.settlement_batches ?? []; if (batches.length === 0) { return

{t("noSettlementBatches")}

; } const counts = new Map(); for (const b of batches) { counts.set(b.status, (counts.get(b.status) ?? 0) + 1); } const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]); const max = Math.max(...entries.map((e) => e[1])); const barTone = (status: string): string => { switch (status) { case "pending_review": return "bg-amber-500"; case "approved": return "bg-sky-500"; case "paid": case "completed": return "bg-emerald-600"; case "running": return "bg-blue-500"; case "rejected": case "failed": return "bg-rose-500"; default: return "bg-violet-500"; } }; return (
    {entries.map(([status, count]) => (
  • {status} {count}
    0 ? (count / max) * 100 : 0}%` }} />
  • ))}
); }