Files
lotteryAdmin/src/modules/dashboard/use-dashboard-analytics.ts
kang a550c418e5 refactor(layout, i18n, admin): 优化布局结构与多语言支持
调整 AdminShell 组件的子组件顺序,提升代码可读性。更新 admin-breadcrumb 组件,简化导航标签翻译逻辑,确保多语言支持的一致性。重构 admin-language-switcher 组件,优化语言切换的用户体验,增强界面交互性。更新多语言配置,新增登录界面的副标题,提升用户体验。
2026-05-30 17:46:27 +08:00

194 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<DashboardAnalyticsPeriod>("last_7_days");
const [rankingMetric, setRankingMetric] = useState<DashboardAnalyticsMetric>("bet");
const [playCode, setPlayCode] = useState<string>("");
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<string | null>(null);
const [data, setData] = useState<AdminDashboardAnalyticsData | null>(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<typeof useDashboardAnalytics>;