"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle, ClipboardList, Diamond, FileSearch, RefreshCw, ScrollText, Settings, Shield, Ticket, Wallet, BarChart3, Scale, } 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 { 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 { AbnormalTransferPanelFooter, CapUsageBar, FinanceStructureChart, HotUsageBars, ResultBatchQueueSummary, PlatformLifetimePayoutSnapshot, DashboardPanelCard, SettlementStatusChart, } from "@/modules/dashboard/dashboard-visuals"; 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 { AdminDashboardDrawPanel, AdminDashboardLifetimeFinance, AdminDashboardPlatformRisk, AdminDashboardResultBatchQueue, } 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"; 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 [drawPanel, setDrawPanel] = 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 [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); setDrawPanel(null); setResultBatchQueue(null); setLifetimeFinance(null); 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); setPlatformRisk(d.platform_risk); if (d.draw != null) { setDrawPanel(d.draw); } 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 = 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 platformUsagePct = typeof rawPlatformUsagePct === "number" && Number.isFinite(rawPlatformUsagePct) ? Math.min(100, Math.max(0, rawPlatformUsagePct)) : platformCap > 0 ? (platformLocked / platformCap) * 100 : 0; 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; const quickLinks: { href: string; label: string; icon: ReactNode }[] = [ { href: "/admin/draws", label: t("quickLinks.createDrawPlan"), icon: }, { href: "/admin/draws", label: t("quickLinks.drawSchedule"), icon: }, { href: drawId != null ? `/admin/draws/${drawId}/results` : "/admin/draws", label: t("quickLinks.results"), icon: , }, { href: "/admin/tickets", label: t("quickLinks.tickets"), icon: }, { href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: }, { href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: }, { href: "/admin/reports", label: t("quickLinks.reports"), icon: }, { href: "/admin/rules/odds", label: t("quickLinks.payoutRules"), icon: }, { href: "/admin/risk", label: t("quickLinks.riskMonitor"), icon: }, { href: "/admin/settings", label: t("quickLinks.systemSettings"), icon: }, ]; return (

{t("title")}

{todayLabel}

{error ? ( {t("notice")} {error} ) : null} {!loading && capabilities && !capabilities.draw_finance_risk ? ( {t("notice")} {t("warnings.drawPermission")} ) : 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} > 0 ? t("platformLockedAndCap", { locked: formatMoneyMinor(platformLocked, currency), cap: formatMoneyMinor(platformCap, currency), }) : t("platformCapNotConfigured", { locked: formatMoneyMinor(platformLocked, currency), }) } actionLabel={t("occupancyDetails")} icon={} accent={ platformUsagePct >= 90 ? "destructive" : platformUsagePct >= 70 ? "primary" : "muted" } loading={loading} > {platformRisk != null ? ( ) : 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 ? ( ) : ( )} {t("quickLinksTitle")} {quickLinks.map((q) => ( {q.icon} {q.label} ))}
) : null}
{showAnalytics ? ( ) : null}
); }