"use client"; import { useCallback, useMemo, useState } from "react"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { format, subDays } from "date-fns"; import { useTranslation } from "react-i18next"; import { getAdminDashboardAnalytics } from "@/api/admin-dashboard"; import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog"; import { getAdminRequestLocale } from "@/lib/admin-locale"; import { coerceAdminMinor, formatAdminCreditMajor, formatAdminMinorUnits, getAdminCurrencyDecimalPlaces, } from "@/lib/money"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminDashboardAnalyticsData, AdminDashboardAnalyticsAgentRow, DashboardAnalyticsMetric, DashboardAnalyticsPeriod, } from "@/types/api/admin-dashboard-analytics"; export const DASHBOARD_ANALYTICS_PERIODS: DashboardAnalyticsPeriod[] = [ "today", "last_7_days", "last_30_days", "this_month", "lifetime", "custom", ]; export const DASHBOARD_RANKING_METRICS: DashboardAnalyticsMetric[] = ["bet", "payout", "profit"]; /** 代理/玩家授信类字段:主货币整数 → 带小数展示(勿用 {@link formatDashboardMoneyMinor})。 */ export function formatDashboardCreditMajor(major: number, currencyCode: string | null): string { const code = (currencyCode ?? "NPR").toUpperCase(); const safeMajor = Number.isFinite(major) ? major : 0; const decimals = getAdminCurrencyDecimalPlaces(code); try { return new Intl.NumberFormat(getAdminRequestLocale(), { style: "currency", currency: code, minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(safeMajor); } catch { return formatAdminCreditMajor(safeMajor, code, decimals); } } export function formatDashboardMoneyMinor(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); } } export function formatDashboardSignedMoneyMinor(minor: number, currencyCode: string | null): string { if (minor === 0) { return formatDashboardMoneyMinor(0, currencyCode); } const s = minor > 0 ? "+" : "−"; return `${s}${formatDashboardMoneyMinor(Math.abs(minor), currencyCode)}`; } export function useDashboardAnalytics({ enabled, playOptions, scope, }: { enabled: boolean; playOptions: { code: string; label: string }[]; scope: { siteCode: string; agentNodeId: number | undefined }; }) { const { t } = useTranslation(["dashboard", "common"]); const tRef = useTranslationRef(["dashboard", "common"]); const playLabel = useAdminPlayCodeLabel(); const [period, setPeriod] = useState("last_7_days"); const [rankingMetric, setRankingMetric] = useState("bet"); const [playCode, setPlayCode] = useState(""); const [customFrom, setCustomFrom] = useState(() => format(subDays(new Date(), 6), "yyyy-MM-dd")); const [customTo, setCustomTo] = useState(() => format(new Date(), "yyyy-MM-dd")); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [data, setData] = useState(null); const load = useCallback(async () => { if (!enabled) { setLoading(false); setData(null); return; } setLoading(true); setError(null); try { const payload = await getAdminDashboardAnalytics({ period, metric: "overview", play_code: playCode !== "" ? playCode : undefined, site_code: scope.siteCode || undefined, agent_node_id: scope.agentNodeId, ...(period === "custom" ? { date_from: customFrom, date_to: customTo } : {}), }); setData(payload); } catch (e) { setData(null); const raw = e instanceof LotteryApiBizError ? e.message : ""; const needsAuthSync = raw.includes("admin.dashboard.analytics") || raw.includes("资源未配置"); setError( needsAuthSync ? tRef.current("warnings.apiResourceMissing") : raw || tRef.current("warnings.loadFailed"), ); } finally { setLoading(false); } }, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode]); useAsyncEffect(() => { void load(); }, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode]); const currency = data?.currency_code ?? null; const summary = data?.summary; const periodRangeLabel = useMemo(() => { if (!data) { return null; } return data.date_from === data.date_to ? data.date_from : `${data.date_from} — ${data.date_to}`; }, [data]); const playFilterLabel = useMemo(() => { if (playCode === "") { return t("analytics.allPlays"); } return playOptions.find((p) => p.code === playCode)?.label ?? playCode; }, [playCode, playOptions, t]); const resolvePlayLabel = useCallback( (code: string, dimension: number) => { const base = playLabel(code); return dimension > 0 ? `${base} · ${dimension}D` : base; }, [playLabel], ); const topPlayRows = useMemo(() => { if (!data) { return []; } const rows = [...data.play_breakdown]; rows.sort((a, b) => { if (rankingMetric === "payout") { return b.total_payout_minor - a.total_payout_minor; } if (rankingMetric === "profit") { return b.approx_house_gross_minor - a.approx_house_gross_minor; } return b.total_bet_minor - a.total_bet_minor; }); return rows.slice(0, 5); }, [data, rankingMetric]); const metricAgentValue = useCallback( (row: AdminDashboardAnalyticsAgentRow): number => { if (rankingMetric === "payout") { return row.total_payout_minor; } if (rankingMetric === "profit") { return row.approx_house_gross_minor; } return row.total_bet_minor; }, [rankingMetric], ); const topAgentRows = useMemo(() => { if (!data) { return []; } const rows = [...data.agent_breakdown]; rows.sort((a, b) => metricAgentValue(b) - metricAgentValue(a)); return rows.slice(0, 5); }, [data, metricAgentValue]); const sparklines = useMemo(() => { const series = data?.daily_series ?? []; return { bet: series.map((d) => d.total_bet_minor), payout: series.map((d) => d.total_payout_minor), profit: series.map((d) => d.approx_house_gross_minor), }; }, [data?.daily_series]); return { enabled, period, setPeriod, rankingMetric, setRankingMetric, playCode, setPlayCode, customFrom, setCustomFrom, customTo, setCustomTo, loading, error, data, currency, summary, periodRangeLabel, playFilterLabel, playOptions, resolvePlayLabel, topPlayRows, topAgentRows, sparklines, formatMoney: formatDashboardMoneyMinor, formatSignedMoney: formatDashboardSignedMoneyMinor, t, }; } export type DashboardAnalyticsState = ReturnType;