Files
lotteryAdmin/src/modules/dashboard/use-dashboard-analytics.ts
kang 65eaeecf8c feat(agents, i18n): enhance agent management and settlement features with new translations and UI updates
Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
2026-06-04 18:01:05 +08:00

247 lines
7.4 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, 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<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,
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<typeof useDashboardAnalytics>;