feat(agents, i18n): enhance agent management and settlement features with new translations and UI updates
Added new translations for agent management and settlement features in English, Nepali, and Chinese, improving multi-language support. Updated the agents console to reflect changes in funding modes and player details, enhancing user experience. Refactored the admin permission gate to include new logic for handling bound line agents, ensuring better permission management. Additionally, streamlined the UI for agent-related pages and improved navigation to the settlement center, consolidating related functionalities for better accessibility.
This commit is contained in:
303
src/modules/dashboard/agent-dashboard-console.tsx
Normal file
303
src/modules/dashboard/agent-dashboard-console.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BarChart3,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Ticket,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
|
||||
import { getAdminDashboard } from "@/api/admin-dashboard";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
import { useCachedPlayTypeOptions } from "@/hooks/use-cached-play-type-options";
|
||||
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import {
|
||||
PRD_AGENT_HUB_ACCESS_ANY,
|
||||
PRD_PLAYERS_ACCESS_ANY,
|
||||
PRD_REPORTS_VIEW_ACCESS_ANY,
|
||||
PRD_SETTLEMENT_AGENT_ACCESS_ANY,
|
||||
PRD_TICKETS_ACCESS_ANY,
|
||||
} from "@/lib/admin-prd";
|
||||
import { normalizeAdminLanguage } from "@/i18n";
|
||||
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
||||
import { DashboardAnalyticsPanel } from "@/modules/dashboard/dashboard-analytics-panel";
|
||||
import {
|
||||
formatDashboardCreditMajor,
|
||||
formatDashboardMoneyMinor,
|
||||
} from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import type { AdminDashboardAgentOverview } from "@/types/api/admin-dashboard";
|
||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
export function AgentDashboardConsole(): ReactElement {
|
||||
const { t, i18n } = useTranslation(["dashboard", "common", "agents"]);
|
||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const agent = profile?.agent ?? null;
|
||||
const permissions = profile?.permissions ?? [];
|
||||
|
||||
const todayLabel = useMemo(() => {
|
||||
const locale = normalizeAdminLanguage(i18n.resolvedLanguage ?? i18n.language);
|
||||
const weekday = t(`date.weekdays.${adminWeekdayKeyForDate()}`, { ns: "common" });
|
||||
|
||||
return formatAdminCalendarToday(locale, weekday);
|
||||
}, [i18n.language, i18n.resolvedLanguage, t]);
|
||||
|
||||
useAdminCurrencyCatalog();
|
||||
const playOptions = useCachedPlayTypeOptions();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
||||
const [drawId, setDrawId] = useState<number | null>(null);
|
||||
const [overview, setOverview] = useState<AdminDashboardAgentOverview | null>(null);
|
||||
const [canFinance, setCanFinance] = useState(false);
|
||||
|
||||
const analyticsScope = useMemo(
|
||||
() => ({
|
||||
siteCode: agent?.site_code ?? "",
|
||||
agentNodeId: agent?.id,
|
||||
}),
|
||||
[agent?.id, agent?.site_code],
|
||||
);
|
||||
|
||||
const load = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const d = await getAdminDashboard();
|
||||
setHall(d.hall);
|
||||
setOverview(d.agent_overview);
|
||||
setCanFinance(d.capabilities.draw_finance_risk);
|
||||
if (d.resolved_draw != null) {
|
||||
setDrawId(d.resolved_draw.id);
|
||||
} else {
|
||||
setDrawId(null);
|
||||
}
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : tRef.current("warnings.loadFailed");
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void load(false);
|
||||
}, []);
|
||||
|
||||
const currency = "NPR";
|
||||
|
||||
const quickLinks = useMemo(() => {
|
||||
const links: { href: string; label: string; icon: ReactElement }[] = [];
|
||||
if (adminHasAnyPermission(permissions, [...PRD_TICKETS_ACCESS_ANY])) {
|
||||
links.push({
|
||||
href: "/admin/tickets",
|
||||
label: t("agent.quickLinks.tickets"),
|
||||
icon: <Ticket className="size-4" />,
|
||||
});
|
||||
}
|
||||
if (adminHasAnyPermission(permissions, [...PRD_PLAYERS_ACCESS_ANY])) {
|
||||
links.push({
|
||||
href: "/admin/players",
|
||||
label: t("agent.quickLinks.players"),
|
||||
icon: <Users className="size-4" />,
|
||||
});
|
||||
}
|
||||
if (adminHasAnyPermission(permissions, [...PRD_REPORTS_VIEW_ACCESS_ANY])) {
|
||||
links.push({
|
||||
href: "/admin/reports",
|
||||
label: t("agent.quickLinks.reports"),
|
||||
icon: <BarChart3 className="size-4" />,
|
||||
});
|
||||
}
|
||||
if (adminHasAnyPermission(permissions, [...PRD_AGENT_HUB_ACCESS_ANY])) {
|
||||
links.push({
|
||||
href: "/admin/agents",
|
||||
label: t("agent.quickLinks.agents"),
|
||||
icon: <Network className="size-4" />,
|
||||
});
|
||||
}
|
||||
if (adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY])) {
|
||||
links.push({
|
||||
href: "/admin/settlement-center",
|
||||
label: t("agent.quickLinks.bills"),
|
||||
icon: <Wallet className="size-4" />,
|
||||
});
|
||||
}
|
||||
|
||||
return links;
|
||||
}, [permissions, t]);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 w-full max-w-none flex-col gap-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h1 className="admin-list-title">{t("agent.title")}</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{agent
|
||||
? t("agent.subtitle", { name: agent.name || agent.code })
|
||||
: todayLabel}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={loading || refreshing}
|
||||
onClick={() => void load(true)}
|
||||
>
|
||||
<RefreshCw className={cn("size-3.5", refreshing && "animate-spin")} />
|
||||
{t("actions.refresh", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-28 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : overview ? (
|
||||
<section className="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.creditTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 text-sm">
|
||||
<p className="text-2xl font-semibold tabular-nums">
|
||||
{formatDashboardCreditMajor(overview.credit_limit, currency)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.creditAvailable", {
|
||||
amount: formatDashboardCreditMajor(overview.available_credit, currency),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.creditAllocated", {
|
||||
amount: formatDashboardCreditMajor(overview.allocated_credit, currency),
|
||||
})}
|
||||
{" · "}
|
||||
{t("agent.creditUsed", {
|
||||
amount: formatDashboardCreditMajor(overview.used_credit, currency),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("agent.shareRate", { rate: overview.total_share_rate })}
|
||||
{" · "}
|
||||
{t("agent.settlementCycle", { cycle: overview.settlement_cycle })}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.teamTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directChildren")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_child_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("agent.directPlayers")}</p>
|
||||
<p className="text-xl font-semibold tabular-nums">{overview.direct_player_count}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-xs text-muted-foreground">{t("agent.subtreeAgents")}</p>
|
||||
<p className="text-lg font-semibold tabular-nums">{overview.subtree_agent_count}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="sm:col-span-2 xl:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("agent.pendingBills")}</CardTitle>
|
||||
{adminHasAnyPermission(permissions, [...PRD_SETTLEMENT_AGENT_ACCESS_ANY]) ? (
|
||||
<Link
|
||||
href="/admin/settlement-center"
|
||||
className={cn(buttonVariants({ variant: "link", size: "sm" }), "h-auto px-0 text-xs")}
|
||||
>
|
||||
{t("agent.viewBills")}
|
||||
</Link>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-semibold tabular-nums">{overview.pending_bill_count}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("agent.pendingUnpaid", {
|
||||
amount: formatDashboardMoneyMinor(overview.pending_unpaid_minor, currency),
|
||||
})}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<DashboardCurrentDrawCard
|
||||
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
||||
hall={hall}
|
||||
drawId={drawId}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{canFinance ? (
|
||||
<DashboardAnalyticsPanel
|
||||
enabled={canFinance}
|
||||
playOptions={playOptions}
|
||||
scope={analyticsScope}
|
||||
/>
|
||||
) : (
|
||||
<Alert className="border-muted">
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{t("warnings.drawPermission")}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{quickLinks.length > 0 ? (
|
||||
<section className="flex flex-wrap gap-2">
|
||||
{quickLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-8 gap-1.5")}
|
||||
>
|
||||
{link.icon}
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { BarChart3, Gift, TrendingUp, Wallet } from "lucide-react";
|
||||
|
||||
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -264,9 +265,7 @@ export function DashboardAnalyticsMain({ analytics }: { analytics: DashboardAnal
|
||||
currency={currency}
|
||||
/>
|
||||
) : (
|
||||
<p className="py-10 text-center text-sm text-muted-foreground">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</p>
|
||||
<AdminNoResourceState className="py-10 text-center text-sm text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,9 +347,7 @@ export function DashboardPlayRankingCard({
|
||||
compact
|
||||
/>
|
||||
) : (
|
||||
<p className="py-10 text-center text-sm text-muted-foreground">
|
||||
{t("analytics.noPlayData")}
|
||||
</p>
|
||||
<AdminNoResourceState className="py-10" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -456,7 +453,7 @@ export function DashboardAgentRankingCard({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-10 text-center text-sm text-muted-foreground">{t("analytics.noAgentData")}</p>
|
||||
<AdminNoResourceState className="py-10" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,23 +2,14 @@
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
|
||||
export function DashboardChartEmpty({
|
||||
message,
|
||||
compact = false,
|
||||
}: {
|
||||
message: string;
|
||||
message?: string;
|
||||
compact?: boolean;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
"text-center text-muted-foreground",
|
||||
compact ? "py-1 text-[11px] leading-snug" : "py-10 text-sm",
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</p>
|
||||
);
|
||||
return <AdminNoResourceState message={message} compact={compact} />;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
||||
import { useDashboardAnalytics } from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -396,7 +397,7 @@ export function DashboardConsole(): ReactElement {
|
||||
orders: lifetimeFinance.order_count,
|
||||
tickets: lifetimeFinance.ticket_item_count,
|
||||
})
|
||||
: t("states.noData", { ns: "common" })
|
||||
: t("states.noResource", { ns: "common" })
|
||||
}
|
||||
actionLabel={t("actions.viewAll", { ns: "common" })}
|
||||
icon={<Wallet className="size-5" aria-hidden />}
|
||||
@@ -446,9 +447,7 @@ export function DashboardConsole(): ReactElement {
|
||||
) : finance ? (
|
||||
<SettlementStatusChart finance={finance} />
|
||||
) : (
|
||||
<p className="py-10 text-center text-xs text-muted-foreground">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</p>
|
||||
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -505,9 +504,7 @@ export function DashboardConsole(): ReactElement {
|
||||
) : finance ? (
|
||||
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
||||
) : (
|
||||
<p className="py-10 text-center text-xs text-muted-foreground">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</p>
|
||||
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -550,9 +547,7 @@ export function DashboardConsole(): ReactElement {
|
||||
) : finance ? (
|
||||
<FinanceStructureChart finance={finance} formatMoney={formatMoneyMinor} />
|
||||
) : (
|
||||
<p className="py-10 text-center text-xs text-muted-foreground">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</p>
|
||||
<AdminNoResourceState className="py-10 text-center text-xs text-muted-foreground" />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { ArrowRight, Clock, Ticket } from "lucide-react";
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
@@ -45,9 +46,8 @@ export function DashboardCurrentDrawCard({
|
||||
return (
|
||||
<Card className="admin-list-card overflow-hidden py-0">
|
||||
<CardContent className="flex min-h-[5.5rem] flex-col items-center justify-center gap-2 p-6 text-center">
|
||||
<Ticket className="size-9 text-muted-foreground/40" aria-hidden />
|
||||
<p className="text-sm font-medium text-muted-foreground">{t("sections.currentDraw")}</p>
|
||||
<p className="text-xs text-muted-foreground">{t("states.noData", { ns: "common" })}</p>
|
||||
<AdminNoResourceState compact className="py-4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
20
src/modules/dashboard/dashboard-page-client.tsx
Normal file
20
src/modules/dashboard/dashboard-page-client.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { AgentDashboardConsole } from "@/modules/dashboard/agent-dashboard-console";
|
||||
import { DashboardConsole } from "@/modules/dashboard/dashboard-console";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
|
||||
/** 平台账号走全站仪表盘;绑定代理节点的经营账号走代理仪表盘。 */
|
||||
export function DashboardPageClient(): ReactElement {
|
||||
const profile = useAdminProfile();
|
||||
const isAgentOperator =
|
||||
profile?.agent != null && profile.is_super_admin !== true;
|
||||
|
||||
if (isAgentOperator) {
|
||||
return <AgentDashboardConsole />;
|
||||
}
|
||||
|
||||
return <DashboardConsole />;
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export function DailyTrendChart({
|
||||
);
|
||||
|
||||
if (series.length === 0) {
|
||||
return <DashboardChartEmpty message={t("states.noData", { ns: "common" })} />;
|
||||
return <DashboardChartEmpty />;
|
||||
}
|
||||
|
||||
const plotHeight = series.length <= 7 ? 240 : series.length <= 14 ? 260 : 280;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
ChartContainer,
|
||||
@@ -516,9 +517,7 @@ export function AbnormalTransferPanelFooter({
|
||||
|
||||
if (total == null) {
|
||||
return (
|
||||
<p className="rounded-lg bg-muted/40 px-3 py-2.5 text-center text-[11px] text-muted-foreground ring-1 ring-border/50">
|
||||
{t("states.noData", { ns: "common" })}
|
||||
</p>
|
||||
<AdminNoResourceState className="rounded-lg bg-muted/40 px-3 py-2.5 text-center text-[11px] text-muted-foreground ring-1 ring-border/50" />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
|
||||
import { getAdminRequestLocale } from "@/lib/admin-locale";
|
||||
import {
|
||||
coerceAdminMinor,
|
||||
formatAdminCreditMajor,
|
||||
formatAdminMinorUnits,
|
||||
getAdminCurrencyDecimalPlaces,
|
||||
} from "@/lib/money";
|
||||
@@ -33,6 +34,23 @@ export const DASHBOARD_ANALYTICS_PERIODS: DashboardAnalyticsPeriod[] = [
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user