Files
lotteryAdmin/src/modules/dashboard/use-dashboard-analytics.ts
kang 4484a7a77a
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Refactor agents console and related components
- Simplified share rate calculation in AgentsConsole by removing unnecessary checks and directly setting the profile share rate.
- Updated the use of `profileParentCaps` to always return total share rate in the agent profile.
- Removed unused variables and memoized calculations for improved performance.
- Cleaned up imports in various files, removing unused components and optimizing the code structure.
- Added `tRef` dependency to several useEffect hooks to ensure proper reactivity to translation changes.
- Enhanced report preview tables with better label handling for various statuses and actions.
- Updated wallet filter options to align with player-side transaction types.
- Introduced new properties in types for better type safety and clarity.
2026-06-30 17:55:00 +08:00

249 lines
7.5 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, tRef]);
useAsyncEffect(() => {
void load();
}, [enabled, period, playCode, customFrom, customTo, scope.agentNodeId, scope.siteCode]);
const currency = data?.currency_code ?? null;
const summary = data?.summary;
const profitScope = data?.profit_scope ?? "house_gross";
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,
profitScope,
periodRangeLabel,
playFilterLabel,
playOptions,
resolvePlayLabel,
topPlayRows,
topAgentRows,
sparklines,
formatMoney: formatDashboardMoneyMinor,
formatSignedMoney: formatDashboardSignedMoneyMinor,
t,
};
}
export type DashboardAnalyticsState = ReturnType<typeof useDashboardAnalytics>;