"use client"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, BarChart3, ClipboardList, RefreshCw, Shield, TrendingUp, Wallet } from "lucide-react"; import { getAdminDashboardByScope } from "@/api/admin-dashboard"; import { useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog"; import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { DashboardAnalyticsMain, DashboardAgentRankingCard, DashboardPlayRankingCard, } from "@/modules/dashboard/dashboard-analytics-panel"; import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card"; import { useDashboardAnalytics } from "@/modules/dashboard/use-dashboard-analytics"; import { formatDashboardMoneyMinor, } from "@/modules/dashboard/use-dashboard-analytics"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime"; import { normalizeAdminLanguage } from "@/i18n"; import { getAdminRequestLocale } from "@/lib/admin-locale"; import { coerceAdminMinor, formatAdminMinorUnits, getAdminCurrencyDecimalPlaces, } from "@/lib/money"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminDashboardLifetimeFinance, AdminDashboardPlatformRisk, AdminDashboardResultBatchQueue, AdminDashboardTodayFinance, AdminDashboardWarning, } from "@/types/api/admin-dashboard"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; // recharts 图表组件懒加载,避免 ~200KB 进入主 bundle const DashboardKpiCard = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.DashboardKpiCard })), { ssr: false }, ); const DashboardPanelCard = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.DashboardPanelCard })), { ssr: false }, ); const AbnormalTransferPanelFooter = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.AbnormalTransferPanelFooter })), { ssr: false }, ); const CapUsageBar = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.CapUsageBar })), { ssr: false }, ); const FinanceStructureChart = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.FinanceStructureChart })), { ssr: false }, ); const HotUsageBars = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.HotUsageBars })), { ssr: false }, ); const ResultBatchQueueSummary = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.ResultBatchQueueSummary })), { ssr: false }, ); const PlatformLifetimePayoutSnapshot = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.PlatformLifetimePayoutSnapshot })), { ssr: false }, ); const SettlementStatusChart = dynamic( () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.SettlementStatusChart })), { ssr: false }, ); type HotPlayTab = "4D" | "3D" | "2D" | "special"; function formatMoneyMinor(minor: number, currencyCode: string | null): string { const safeMinor = coerceAdminMinor(minor); const code = (currencyCode ?? "NPR").toUpperCase(); const decimals = getAdminCurrencyDecimalPlaces(code); const major = safeMinor / 10 ** decimals; try { return new Intl.NumberFormat(getAdminRequestLocale(), { style: "currency", currency: code, minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(major); } catch { return formatAdminMinorUnits(safeMinor, code, decimals); } } function drawScopedHref( drawId: number | null, suffix = "", fallback = "/admin/draws", ): string { return drawId != null ? `/admin/draws/${drawId}${suffix}` : fallback; } function pendingReviewHref( drawId: number | null, queue: AdminDashboardResultBatchQueue | null, ): string { if (queue != null && queue.pending_review_total > 0 && queue.first_pending_draw_id != null) { return `/admin/draws/${queue.first_pending_draw_id}/review`; } return drawScopedHref(drawId, "/review"); } function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { const raw = normalizedNumber.trim(); const digits = raw.replace(/\D/g, ""); const digitLen = digits.length; const hasLetter = /[A-Za-z]/.test(raw); if (hasLetter && digitLen < 3) { return "special"; } if (digitLen >= 4) { return "4D"; } if (digitLen === 3) { return "3D"; } if (digitLen === 2) { return "2D"; } if (hasLetter) { return "special"; } return "other"; } function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPoolRow[] { return [...pools] .filter((p) => poolPlayCategory(p.normalized_number) === tab) .sort((a, b) => (b.usage_ratio ?? 0) - (a.usage_ratio ?? 0)) .slice(0, 10); } export function DashboardConsole(): ReactElement { const { t, i18n } = useTranslation(["dashboard", "common"]); useAdminCurrencyCatalog(); useAdminPlayTypeCatalog(); const todayLabel = useMemo(() => { const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language); const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" }); return formatAdminCalendarToday(locale, weekday); }, [i18n.language, i18n.resolvedLanguage, t]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [hall, setHall] = useState(null); const [drawId, setDrawId] = useState(null); const [finance, setFinance] = useState(null); const [capabilities, setCapabilities] = useState<{ draw_finance_risk: boolean; wallet_transfer_view: boolean } | null>(null); const [resultBatchQueue, setResultBatchQueue] = useState( null, ); const [lifetimeFinance, setLifetimeFinance] = useState( null, ); const [todayFinance, setTodayFinance] = useState(null); const [apiWarnings, setApiWarnings] = useState([]); const [platformRisk, setPlatformRisk] = useState(null); const [riskLocked, setRiskLocked] = useState(0); const [riskCap, setRiskCap] = useState(0); const [hotPoolSample, setHotPoolSample] = useState([]); const [abnormalTransferTotal, setAbnormalTransferTotal] = useState(null); const [hotTab, setHotTab] = useState("4D"); const playOptions = useCachedPlayTypeOptions(); const tRef = useTranslationRef(["dashboard", "common"]); const load = useCallback(async (isRefresh = false) => { if (isRefresh) { setRefreshing(true); } else { setLoading(true); } setError(null); setFinance(null); setCapabilities(null); setResultBatchQueue(null); setLifetimeFinance(null); setTodayFinance(null); setApiWarnings([]); setPlatformRisk(null); setDrawId(null); setRiskLocked(0); setRiskCap(0); setHotPoolSample([]); setAbnormalTransferTotal(null); try { const d = await getAdminDashboardByScope({}); setHall(d.hall); if (d.resolved_draw != null) { setDrawId(d.resolved_draw.id); } setCapabilities(d.capabilities); if (d.finance != null) { setFinance(d.finance); } setResultBatchQueue(d.result_batch_queue); setLifetimeFinance(d.lifetime_finance); setTodayFinance(d.today_finance); setApiWarnings(d.warnings ?? []); setPlatformRisk(d.platform_risk); if (d.risk != null) { setRiskLocked(d.risk.locked_amount); setRiskCap(d.risk.cap_amount); setHotPoolSample(d.risk.hot_pool_rows); } setAbnormalTransferTotal(d.abnormal_transfer_total); } catch (e) { const msg = e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed"); setError(msg); } finally { setLoading(false); setRefreshing(false); } }, []); useAsyncEffect(() => { void load(false); }, []); const currency = todayFinance?.currency_code ?? lifetimeFinance?.currency_code ?? finance?.currency_code ?? null; const canFinance = capabilities?.draw_finance_risk ?? false; const platformLocked = coerceAdminMinor(platformRisk?.locked_amount); const platformCap = coerceAdminMinor(platformRisk?.cap_amount); const rawPlatformUsagePct = platformRisk?.usage_percent; const useCurrentDrawRisk = drawId != null; const capUsageLocked = useCurrentDrawRisk ? riskLocked : platformLocked; const capUsageCap = useCurrentDrawRisk ? riskCap : platformCap; const capUsagePct = capUsageCap > 0 ? (capUsageLocked / capUsageCap) * 100 : typeof rawPlatformUsagePct === "number" && Number.isFinite(rawPlatformUsagePct) ? Math.min(100, Math.max(0, rawPlatformUsagePct)) : platformCap > 0 ? (platformLocked / platformCap) * 100 : 0; const showCapUsageCard = useCurrentDrawRisk || platformRisk != null; const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]); const pendingReviewTotal = resultBatchQueue?.pending_review_total ?? 0; const analytics = useDashboardAnalytics({ enabled: canFinance, playOptions, scope: { siteCode: "", agentNodeId: undefined }, }); const showAnalytics = canFinance; return (

{t("title")}

{todayLabel}

{error ? ( {t("notice")} {error} ) : null} {!loading && capabilities && !capabilities.draw_finance_risk ? ( {t("notice")} {t("warnings.drawPermission")} ) : null} {!loading && apiWarnings.length > 0 ? ( {t("notice")} {apiWarnings.map((w) => w.message).join(" ")} ) : null}
{canFinance && !loading && todayFinance ? (
} hint={t("todayBusinessDateHint", { date: todayFinance.business_date })} /> } hint={t("todayPayoutHint", { amount: formatDashboardMoneyMinor(todayFinance.total_payout_minor, currency), })} /> } hint={ lifetimeFinance ? t("lifetimeActivityHint", { draws: lifetimeFinance.draw_count, days: lifetimeFinance.business_day_count, }) : undefined } value={lifetimeFinance ? undefined : "—"} />
) : null}
0 ? t("actions.reviewNow", { ns: "common" }) : t("drawDetails") } icon={} accent={pendingReviewTotal > 0 ? "destructive" : "muted"} highlight={pendingReviewTotal > 0} loading={loading} > {resultBatchQueue != null ? ( ) : null} } accent={(abnormalTransferTotal ?? 0) > 0 ? "warning" : "muted"} loading={loading} highlight={(abnormalTransferTotal ?? 0) > 0} > } accent={ capUsagePct >= 90 ? "destructive" : capUsagePct >= 70 ? "primary" : "muted" } loading={loading} > {showCapUsageCard ? ( ) : null} } accent="primary" loading={loading} > {lifetimeFinance ? ( ) : null}
{showAnalytics ? : null}
{t("settlementOverview")} {drawId != null ? ( {t("actions.viewAll", { ns: "common" })} ) : null} {loading ? ( ) : finance ? ( ) : ( )} {t("hotNumbersTop10")}
{([ { value: "4D", label: t("tabs.4d") }, { value: "3D", label: t("tabs.3d") }, { value: "2D", label: t("tabs.2d") }, { value: "special", label: t("tabs.special") }, ] as const).map((tab) => ( ))}
{loading ? ( ) : ( )}
{showAnalytics ? ( {t("financeStructure")} {loading ? ( ) : finance ? ( ) : ( )} ) : null} {!showAnalytics ? ( {t("financeStructure")} {loading ? ( ) : finance ? ( ) : ( )} ) : null}
{showAnalytics ? ( ) : null}
); }