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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user