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

@@ -21,6 +21,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { getAdminRequestLocale } from "@/lib/admin-locale";
import { cn } from "@/lib/utils";
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
import {
DailyTrendChart,
PlayBreakdownChart,
@@ -356,6 +357,112 @@ export function DashboardPlayRankingCard({
);
}
export function DashboardAgentRankingCard({
analytics,
}: {
analytics: DashboardAnalyticsState;
}): ReactNode {
const { t } = useTranslation(["dashboard", "common"]);
const {
enabled,
rankingMetric,
loading,
topAgentRows,
currency,
formatMoney,
formatSignedMoney,
} = analytics;
if (!enabled) {
return null;
}
const metricValue = (row: (typeof topAgentRows)[number]): number => {
if (rankingMetric === "payout") {
return row.total_payout_minor;
}
if (rankingMetric === "profit") {
return row.approx_house_gross_minor;
}
return row.total_bet_minor;
};
const maxAbs = Math.max(1, ...topAgentRows.map((r) => Math.abs(metricValue(r))));
const formatRowValue = (row: (typeof topAgentRows)[number]): string => {
const v = metricValue(row);
if (rankingMetric === "profit") {
return formatSignedMoney(v, currency);
}
return formatMoney(v, currency);
};
const barColor = (row: (typeof topAgentRows)[number]): string => {
if (rankingMetric === "bet") {
return DASHBOARD_CHART_COLORS.primary;
}
if (rankingMetric === "payout") {
return DASHBOARD_CHART_COLORS.rose;
}
return row.approx_house_gross_minor >= 0 ? DASHBOARD_CHART_COLORS.success : DASHBOARD_CHART_COLORS.warning;
};
return (
<Card className="admin-list-card flex min-w-0 flex-col overflow-hidden py-0">
<CardHeader className="space-y-2 border-b border-border/60 px-4 py-3">
<CardTitle className="text-sm font-semibold">{t("analytics.agentRanking")}</CardTitle>
<p className="text-xs text-muted-foreground">
{t(`analytics.rankingMetrics.${rankingMetric}`)}
</p>
</CardHeader>
<CardContent className="min-w-0 flex-1 overflow-hidden px-3 py-3">
{loading ? (
<Skeleton className="h-[210px] w-full" />
) : topAgentRows.length > 0 ? (
<div className="space-y-1.5">
{topAgentRows.map((row, idx) => {
const v = metricValue(row);
const pct = (Math.abs(v) / maxAbs) * 100;
const color = barColor(row);
return (
<div key={row.agent_node_id} className="rounded-lg bg-muted/20 px-2 py-2">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-start gap-2">
<span className="mt-0.5 w-5 shrink-0 text-center text-[11px] font-semibold text-muted-foreground">
#{idx + 1}
</span>
<div className="min-w-0">
<p className="truncate text-xs font-medium">{row.agent_name || "-"}</p>
<p className="truncate text-[11px] text-muted-foreground">{row.agent_code || ""}</p>
</div>
</div>
<div className="shrink-0 text-right text-xs font-semibold tabular-nums">
{formatRowValue(row)}
</div>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-muted/30">
<div
className="h-full rounded-full"
style={{
width: `${Math.max(2, pct)}%`,
backgroundColor: color,
opacity: 0.35,
}}
/>
</div>
</div>
);
})}
</div>
) : (
<p className="py-10 text-center text-sm text-muted-foreground">{t("analytics.noAgentData")}</p>
)}
</CardContent>
</Card>
);
}
/** 单列堆叠布局(兼容旧用法) */
export function DashboardAnalyticsPanel({
enabled,