"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; 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 { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money"; import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminDashboardAnalyticsData, 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"]; export function formatDashboardMoneyMinor(minor: number, currencyCode: string | null): string { const code = (currencyCode ?? "NPR").toUpperCase(); const decimals = getAdminCurrencyDecimalPlaces(code); const major = minor / 10 ** decimals; try { return new Intl.NumberFormat(getAdminRequestLocale(), { style: "currency", currency: code, minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(major); } catch { return formatAdminMinorUnits(minor, 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, }: { enabled: boolean; playOptions: { code: string; label: string }[]; }) { const { t } = useTranslation(["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, ...(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 ? t("warnings.apiResourceMissing") : raw || t("warnings.loadFailed"), ); } finally { setLoading(false); } }, [enabled, period, playCode, customFrom, customTo, t]); useEffect(() => { const timer = window.setTimeout(() => { void load(); }, 0); return () => window.clearTimeout(timer); }, [load]); 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 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, sparklines, formatMoney: formatDashboardMoneyMinor, formatSignedMoney: formatDashboardSignedMoneyMinor, t, }; } export type DashboardAnalyticsState = ReturnType;