feat(dashboard, i18n): enhance agent dashboard and localization support

Updated the agent dashboard to include new metrics for today's bets and payouts, improving visibility for users. Enhanced localization files with additional hints and labels for better user experience across English, Nepali, and Chinese. Introduced new functions for formatting business dates and improved the handling of analytics permissions in the dashboard components.
This commit is contained in:
2026-06-16 14:18:58 +08:00
parent a4454a54a4
commit d4cf4ff436
17 changed files with 534 additions and 415 deletions

View File

@@ -36,6 +36,7 @@ import {
getAdminCurrencyDecimalPlaces,
} from "@/lib/money";
import { cn } from "@/lib/utils";
import { SignedMoney, signedMoneyClass } from "@/lib/admin-signed-money";
import {
buildBatchProgressConfig,
buildFinanceStructureConfig,
@@ -46,6 +47,7 @@ import {
DASHBOARD_CHART_COLORS,
} from "@/modules/dashboard/dashboard-chart-config";
import { DashboardChartEmpty } from "@/modules/dashboard/dashboard-chart-empty";
import { formatDashboardSignedMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import type { AdminDashboardLifetimeFinance } from "@/types/api/admin-dashboard";
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
@@ -179,6 +181,73 @@ function kpiAccentClass(accent: DashboardKpiAccent): string {
}
}
/** 站点/代理卡片内 label | amount 行,金额过长时可换行 */
export function DashboardStatRow({
label,
value,
valueClassName,
}: {
label: string;
value: string;
valueClassName?: string;
}): ReactElement {
return (
<div className="flex items-start justify-between gap-3 text-sm">
<span className="shrink-0 text-muted-foreground">{label}</span>
<span
className={cn(
"min-w-0 break-all text-right font-semibold tabular-nums leading-tight",
valueClassName ?? "text-foreground",
)}
>
{value}
</span>
</div>
);
}
/** 盈亏行:正绿、负红、零灰(与 {@link signedMoneyClass} 一致) */
export function DashboardSignedStatRow({
label,
amountMinor,
currencyCode,
}: {
label: string;
amountMinor: number;
currencyCode: string | null;
}): ReactElement {
return (
<div className="flex items-start justify-between gap-3 text-sm">
<span className="shrink-0 text-muted-foreground">{label}</span>
<SignedMoney
amount={amountMinor}
emphasize
className="min-w-0 break-all text-right leading-tight"
>
{formatDashboardSignedMoneyMinor(amountMinor, currencyCode)}
</SignedMoney>
</div>
);
}
/** 规模/授信等栅格指标,金额可换行 */
export function DashboardScopeMetric({
label,
value,
}: {
label: string;
value: string;
}): ReactElement {
return (
<div className="rounded-lg border bg-muted/30 px-3 py-2.5">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="mt-1 break-all text-base font-semibold tabular-nums leading-tight text-foreground">
{value}
</p>
</div>
);
}
/** 财务概览区紧凑 KPI避免 StatCard 在窄栅格内撑破布局 */
export function DashboardKpiCard({
label,
@@ -187,43 +256,60 @@ export function DashboardKpiCard({
icon,
accent = "primary",
valueClassName,
signedAmountMinor,
currencyCode,
sparklineValues,
deltaLabel,
}: {
label: string;
value: ReactNode;
value?: ReactNode;
hint?: ReactNode;
icon: ReactNode;
accent?: DashboardKpiAccent;
/** 覆盖主数值颜色(如盈亏红绿) */
valueClassName?: string;
/** 盈亏类 KPI自动带 +/- 与红绿 */
signedAmountMinor?: number;
currencyCode?: string | null;
sparklineValues?: number[];
deltaLabel?: ReactNode;
}): ReactElement {
const resolvedValue =
typeof signedAmountMinor === "number" && currencyCode !== undefined
? formatDashboardSignedMoneyMinor(signedAmountMinor, currencyCode)
: value;
const resolvedValueClassName =
typeof signedAmountMinor === "number"
? signedMoneyClass(signedAmountMinor, true)
: valueClassName;
const valueTitle =
typeof resolvedValue === "string" || typeof resolvedValue === "number"
? String(resolvedValue)
: undefined;
return (
<div className="flex h-full min-w-0 flex-col rounded-xl border border-border/60 bg-card p-4">
<div className="flex min-w-0 items-start gap-3">
<div className="flex items-start justify-between gap-2">
<p className="min-w-0 flex-1 text-xs font-medium leading-snug text-muted-foreground">{label}</p>
<div
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-lg",
"flex size-9 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4",
kpiAccentClass(accent),
)}
>
{icon}
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<p
className={cn(
"mt-1 truncate text-xl font-bold tabular-nums tracking-tight",
valueClassName ?? "text-foreground",
)}
>
{value}
</p>
{deltaLabel ? <div className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</div> : null}
</div>
</div>
<p
title={valueTitle}
className={cn(
"mt-2 break-words text-base font-bold tabular-nums leading-tight tracking-tight sm:text-lg",
resolvedValueClassName ?? "text-foreground",
)}
>
{resolvedValue}
</p>
{deltaLabel ? <div className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</div> : null}
{sparklineValues && sparklineValues.length >= 2 ? (
<div className="mt-3 flex justify-end">
<MiniSparkline
@@ -239,7 +325,7 @@ export function DashboardKpiCard({
</div>
) : null}
{hint ? (
<p className="mt-2 line-clamp-2 text-[11px] leading-snug text-muted-foreground">{hint}</p>
<p className="mt-2 text-[11px] leading-snug text-muted-foreground">{hint}</p>
) : null}
</div>
);
@@ -477,9 +563,7 @@ export function DashboardPanelCard({
</p>
)}
{subtitle && !loading ? (
<p className="mt-2 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
{subtitle}
</p>
<div className="mt-2 text-xs leading-snug text-muted-foreground">{subtitle}</div>
) : null}
</div>
@@ -621,19 +705,48 @@ export function CapUsageBar({
const radialData = useMemo(() => [{ usage: pct, fill }], [pct, fill]);
if (compact) {
const lockedLabel = formatMoney(locked, currency);
const capLabel = cap > 0 ? formatMoney(cap, currency) : t("platformCapUnset");
return (
<div
className="h-2 overflow-hidden rounded-full bg-muted"
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={t("riskCapUsage")}
>
<div className="space-y-2.5">
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-primary/5 px-2.5 py-2 ring-1 ring-primary/15">
<p className="text-[10px] font-medium uppercase tracking-wide text-primary/80">
{t("platformLockedLabel")}
</p>
<p
className="mt-1 break-all text-sm font-semibold tabular-nums leading-tight text-foreground"
title={lockedLabel}
>
{lockedLabel}
</p>
</div>
<div className="rounded-lg bg-muted/50 px-2.5 py-2 ring-1 ring-border/60">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{t("platformCapLabel")}
</p>
<p
className="mt-1 break-all text-sm font-semibold tabular-nums leading-tight text-foreground"
title={capLabel}
>
{capLabel}
</p>
</div>
</div>
<div
className="h-full rounded-full transition-[width] duration-500"
style={{ width: `${pct}%`, backgroundColor: fill }}
/>
className="h-2 overflow-hidden rounded-full bg-muted"
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={t("riskCapUsage")}
>
<div
className="h-full rounded-full transition-[width] duration-500"
style={{ width: `${pct}%`, backgroundColor: fill }}
/>
</div>
</div>
);
}
@@ -741,6 +854,12 @@ export function FinanceStructureChart({
<p className="text-center text-xs text-muted-foreground">
{t("payoutRateOfBet", { rate: payoutRate })}
</p>
<p className="text-center text-sm tabular-nums">
<span className="text-muted-foreground">{t("houseGross")} </span>
<SignedMoney amount={gross} emphasize>
{formatDashboardSignedMoneyMinor(gross, currency)}
</SignedMoney>
</p>
<ChartLegend content={<ChartLegendContent />} />
</div>
);