feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support

Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
2026-06-02 14:37:08 +08:00
parent a4e7a2d228
commit b15e377187
105 changed files with 5305 additions and 1596 deletions

View File

@@ -20,14 +20,12 @@ import {
import { getAdminDashboard } from "@/api/admin-dashboard";
import { useAdminPlayTypeCatalog } from "@/hooks/use-admin-play-type-catalog";
import { getAdminPlayTypes } from "@/api/admin-config";
import {
getAdminPlayTypesLoadPromise,
getCachedAdminPlayTypes,
resolveAdminPlayTypeDisplayName,
} from "@/lib/admin-play-types";
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import {
DashboardAnalyticsMain,
DashboardAgentRankingCard,
DashboardPlayRankingCard,
} from "@/modules/dashboard/dashboard-analytics-panel";
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
@@ -50,7 +48,11 @@ import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
import { normalizeAdminLanguage } from "@/i18n";
import { getAdminRequestLocale } from "@/lib/admin-locale";
import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money";
import {
coerceAdminMinor,
formatAdminMinorUnits,
getAdminCurrencyDecimalPlaces,
} from "@/lib/money";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type {
@@ -66,9 +68,10 @@ import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
type HotPlayTab = "4D" | "3D" | "2D" | "special";
function formatMoneyMinor(minor: number, currencyCode: string | null): string {
const safeMinor = coerceAdminMinor(minor);
const code = (currencyCode ?? "NPR").toUpperCase();
const decimals = getAdminCurrencyDecimalPlaces(code);
const major = minor / 10 ** decimals;
const major = safeMinor / 10 ** decimals;
try {
return new Intl.NumberFormat(getAdminRequestLocale(), {
style: "currency",
@@ -77,7 +80,7 @@ function formatMoneyMinor(minor: number, currencyCode: string | null): string {
maximumFractionDigits: decimals,
}).format(major);
} catch {
return formatAdminMinorUnits(minor, code, decimals);
return formatAdminMinorUnits(safeMinor, code, decimals);
}
}
@@ -162,28 +165,8 @@ export function DashboardConsole(): ReactElement {
const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]);
const [abnormalTransferTotal, setAbnormalTransferTotal] = useState<number | null>(null);
const [hotTab, setHotTab] = useState<HotPlayTab>("4D");
const [playOptions, setPlayOptions] = useState<{ code: string; label: string }[]>([]);
const loadPlayOptions = useCallback(async () => {
try {
await getAdminPlayTypesLoadPromise(getAdminPlayTypes);
setPlayOptions(
getCachedAdminPlayTypes().map((item) => ({
code: item.play_code,
label:
resolveAdminPlayTypeDisplayName(item.play_code, i18n.language, item) || item.play_code,
})),
);
} catch {
setPlayOptions([]);
}
}, [i18n.language]);
useEffect(() => {
queueMicrotask(() => {
void loadPlayOptions();
});
}, [loadPlayOptions]);
const playOptions = useCachedPlayTypeOptions();
const tRef = useTranslationRef(["dashboard", "common"]);
const load = useCallback(async (isRefresh = false) => {
if (isRefresh) {
@@ -230,27 +213,30 @@ export function DashboardConsole(): ReactElement {
setAbnormalTransferTotal(d.abnormal_transfer_total);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : t("warnings.loadFailed");
e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed");
setError(msg);
} finally {
setLoading(false);
setRefreshing(false);
}
}, [t]);
}, []);
useEffect(() => {
const timer = window.setTimeout(() => {
void load(false);
}, 0);
return () => window.clearTimeout(timer);
}, [load]);
useAsyncEffect(() => {
void load(false);
}, []);
const currency =
lifetimeFinance?.currency_code ?? finance?.currency_code ?? null;
const canFinance = capabilities?.draw_finance_risk ?? false;
const platformLocked = platformRisk?.locked_amount ?? 0;
const platformCap = platformRisk?.cap_amount ?? 0;
const platformUsagePct = platformRisk?.usage_percent ?? 0;
const platformLocked = coerceAdminMinor(platformRisk?.locked_amount);
const platformCap = coerceAdminMinor(platformRisk?.cap_amount);
const rawPlatformUsagePct = platformRisk?.usage_percent;
const platformUsagePct =
typeof rawPlatformUsagePct === "number" && Number.isFinite(rawPlatformUsagePct)
? Math.min(100, Math.max(0, rawPlatformUsagePct))
: platformCap > 0
? (platformLocked / platformCap) * 100
: 0;
const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]);
@@ -359,10 +345,16 @@ export function DashboardConsole(): ReactElement {
href="/admin/risk"
title={t("riskCapUsage")}
value={`${platformUsagePct.toFixed(1)}%`}
subtitle={t("platformLockedAndCap", {
locked: formatMoneyMinor(platformLocked, currency),
cap: formatMoneyMinor(platformCap, currency),
})}
subtitle={
platformCap > 0
? t("platformLockedAndCap", {
locked: formatMoneyMinor(platformLocked, currency),
cap: formatMoneyMinor(platformCap, currency),
})
: t("platformCapNotConfigured", {
locked: formatMoneyMinor(platformLocked, currency),
})
}
actionLabel={t("occupancyDetails")}
icon={<Shield className="size-5" aria-hidden />}
accent={
@@ -542,6 +534,7 @@ export function DashboardConsole(): ReactElement {
{showAnalytics ? (
<aside className="flex min-w-0 flex-col gap-4 xl:col-span-4">
<DashboardPlayRankingCard analytics={analytics} />
<DashboardAgentRankingCard analytics={analytics} />
<Card className="admin-list-card min-w-0 py-0">
<CardHeader className="border-b border-border/60 px-4 py-3 pb-0">