refactor(admin-reports, i18n): remove rebate commission report and enhance localization
Removed the `getAdminReportRebateCommission` function and its references from the admin reports API and localization files. Updated CSS for improved money display handling in admin components. Enhanced localization support by adding new finance and support workspace entries in English, Nepali, and Chinese, improving user experience across the application.
This commit is contained in:
@@ -17,6 +17,7 @@ import { adminWeekdayKeyForDate, formatAdminBusinessDateIso, formatAdminCalendar
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -207,9 +208,14 @@ export function AgentDashboardConsole(): ReactElement {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div>
|
||||
<p className="break-all text-2xl font-semibold tabular-nums leading-tight">
|
||||
<AdminMoneyDisplay
|
||||
as="p"
|
||||
value={formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||
size="xl"
|
||||
className="text-foreground"
|
||||
>
|
||||
{formatDashboardCreditMajor(overview.credit_limit, displayCurrency)}
|
||||
</p>
|
||||
</AdminMoneyDisplay>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("agent.creditAvailable", {
|
||||
amount: formatDashboardCreditMajor(overview.available_credit, displayCurrency),
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
import { isAgentOperator, isSiteAdminOperator } from "@/lib/admin-session-variants";
|
||||
import { isAgentOperator, isSiteFinanceOperator, isSiteCsOperator, isSiteOperator } from "@/lib/admin-session-variants";
|
||||
import { AgentDashboardConsole } from "@/modules/dashboard/agent-dashboard-console";
|
||||
import { DashboardConsole } from "@/modules/dashboard/dashboard-console";
|
||||
import { SiteCsDashboardConsole } from "@/modules/dashboard/site-cs-dashboard-console";
|
||||
import { SiteFinanceDashboardConsole } from "@/modules/dashboard/site-finance-dashboard-console";
|
||||
import { SiteDashboardConsole } from "@/modules/dashboard/site-dashboard-console";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
|
||||
/** 超管/平台账号走全站仪表盘;站点管理员走站点仪表盘;代理经营账号走代理仪表盘。 */
|
||||
/** 超管/平台账号走全站仪表盘;站点运营账号走站点仪表盘;代理经营账号走代理仪表盘。 */
|
||||
export function DashboardPageClient(): ReactElement {
|
||||
const profile = useAdminProfile();
|
||||
|
||||
@@ -16,7 +18,15 @@ export function DashboardPageClient(): ReactElement {
|
||||
return <AgentDashboardConsole />;
|
||||
}
|
||||
|
||||
if (isSiteAdminOperator(profile)) {
|
||||
if (isSiteFinanceOperator(profile)) {
|
||||
return <SiteFinanceDashboardConsole />;
|
||||
}
|
||||
|
||||
if (isSiteCsOperator(profile)) {
|
||||
return <SiteCsDashboardConsole />;
|
||||
}
|
||||
|
||||
if (isSiteOperator(profile)) {
|
||||
return <SiteDashboardConsole />;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
|
||||
import { AdminMoneyDisplay } from "@/components/admin/admin-money-display";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
ChartContainer,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
} from "@/lib/money";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SignedMoney, signedMoneyClass } from "@/lib/admin-signed-money";
|
||||
import { adminMoneyDisplayClass } from "@/lib/admin-money-display";
|
||||
import {
|
||||
buildBatchProgressConfig,
|
||||
buildFinanceStructureConfig,
|
||||
@@ -68,7 +70,7 @@ type DashboardFinanceMetricCell = {
|
||||
emphasize: boolean;
|
||||
};
|
||||
|
||||
/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时省略号 + hover 看全称 */
|
||||
/** KPI 卡片底部三列:仅数字(币种见卡片主值),过长时缩小字号 + 换行 */
|
||||
function formatDashboardMetricAmount(
|
||||
minor: number,
|
||||
currencyCode: string | null,
|
||||
@@ -115,7 +117,8 @@ function DashboardFinanceMetricCells({
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 truncate text-center text-[10px] font-bold tabular-nums leading-tight",
|
||||
"mt-1 text-center font-bold tabular-nums leading-tight break-all",
|
||||
adminMoneyDisplayClass(display, { size: "sm", emphasize: true }),
|
||||
cell.emphasize ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
title={title}
|
||||
@@ -241,9 +244,14 @@ export function DashboardScopeMetric({
|
||||
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">
|
||||
<AdminMoneyDisplay
|
||||
as="p"
|
||||
value={value}
|
||||
size="md"
|
||||
className="mt-1 text-foreground"
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
</AdminMoneyDisplay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -303,8 +311,11 @@ export function DashboardKpiCard({
|
||||
<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",
|
||||
"mt-2 text-foreground",
|
||||
resolvedValueClassName,
|
||||
typeof resolvedValue === "string"
|
||||
? adminMoneyDisplayClass(resolvedValue, { size: "lg", emphasize: true })
|
||||
: "min-w-0 break-all text-base font-bold tabular-nums leading-tight tracking-tight sm:text-lg",
|
||||
)}
|
||||
>
|
||||
{resolvedValue}
|
||||
@@ -418,9 +429,21 @@ export function StatCard({
|
||||
</div>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
||||
{value}
|
||||
</p>
|
||||
{typeof value === "string" || typeof value === "number" ? (
|
||||
<AdminMoneyDisplay
|
||||
as="p"
|
||||
value={String(value)}
|
||||
size="xl"
|
||||
emphasize
|
||||
className="mt-1 text-foreground"
|
||||
>
|
||||
{value}
|
||||
</AdminMoneyDisplay>
|
||||
) : (
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums tracking-tight text-foreground">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
{deltaLabel ? (
|
||||
<p className="mt-1 text-xs font-medium tabular-nums">{deltaLabel}</p>
|
||||
) : null}
|
||||
@@ -557,6 +580,16 @@ export function DashboardPanelCard({
|
||||
<p className="text-xs font-medium text-muted-foreground">{title}</p>
|
||||
{loading ? (
|
||||
<Skeleton className="mt-2 h-8 w-24 rounded-md" />
|
||||
) : typeof value === "string" || typeof value === "number" ? (
|
||||
<AdminMoneyDisplay
|
||||
as="p"
|
||||
value={String(value)}
|
||||
size="xl"
|
||||
emphasize
|
||||
className="mt-1 text-foreground"
|
||||
>
|
||||
{value}
|
||||
</AdminMoneyDisplay>
|
||||
) : (
|
||||
<p className="mt-1 text-2xl font-bold tabular-nums leading-none tracking-tight text-foreground">
|
||||
{value}
|
||||
|
||||
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
196
src/modules/dashboard/site-cs-dashboard-console.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClipboardList, RefreshCw, Search, Users } from "lucide-react";
|
||||
|
||||
import { getAdminDashboard } from "@/api/admin-dashboard";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||
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 { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import {
|
||||
DashboardKpiCard,
|
||||
DashboardScopeMetric,
|
||||
} from "@/modules/dashboard/dashboard-visuals";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import type {
|
||||
AdminDashboardSiteCsOverview,
|
||||
AdminDashboardWarning,
|
||||
} from "@/types/api/admin-dashboard";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
export function SiteCsDashboardConsole(): ReactElement {
|
||||
const { t } = useTranslation(["dashboard", "common"]);
|
||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||
const formatDt = useAdminDateTimeFormatter();
|
||||
const profile = useAdminProfile();
|
||||
const site = profile?.site ?? null;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
||||
const [overview, setOverview] = useState<AdminDashboardSiteCsOverview | null>(null);
|
||||
|
||||
const load = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const d = await getAdminDashboard();
|
||||
setOverview(d.site_cs_overview);
|
||||
setApiWarnings(d.warnings ?? []);
|
||||
} 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 activityHint = useMemo(() => {
|
||||
if (!overview) {
|
||||
return "";
|
||||
}
|
||||
if (overview.latest_ticket_at) {
|
||||
return t("cs.latestTicketAt", { time: formatDt(overview.latest_ticket_at) });
|
||||
}
|
||||
return t("cs.noTicketToday");
|
||||
}, [formatDt, overview, t]);
|
||||
|
||||
const quickLinks = useMemo(
|
||||
() => [
|
||||
{ href: "/admin/players", label: t("cs.quickLinks.players"), icon: Users },
|
||||
{ href: "/admin/tickets", label: t("cs.quickLinks.tickets"), icon: ClipboardList },
|
||||
{ href: "/admin/wallet/transactions", label: t("cs.quickLinks.wallet"), icon: Search },
|
||||
],
|
||||
[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("cs.title")}</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{site
|
||||
? t("cs.subtitle", { name: site.name || site.code })
|
||||
: t("cs.subtitleFallback")}
|
||||
</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 && apiWarnings.length > 0 ? (
|
||||
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{apiWarnings.map((w) => w.message).join(" ")}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : overview ? (
|
||||
<section className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<DashboardKpiCard
|
||||
label={t("cs.playerCount")}
|
||||
value={overview.player_count}
|
||||
icon={<Users className="size-4" />}
|
||||
hint={t("cs.playerCountHint")}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("cs.ticketsToday")}
|
||||
value={overview.ticket_order_count_today}
|
||||
icon={<ClipboardList className="size-4" />}
|
||||
hint={activityHint}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("cs.activePlayersToday")}
|
||||
value={overview.active_player_count_today}
|
||||
icon={<Search className="size-4" />}
|
||||
hint={t("cs.activePlayersHint")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("cs.workspaceTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 sm:grid-cols-3">
|
||||
{quickLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex flex-col gap-2 rounded-xl border bg-muted/20 px-4 py-4 transition-colors hover:bg-muted/40"
|
||||
>
|
||||
<Icon className="size-5 text-primary" aria-hidden />
|
||||
<span className="text-sm font-medium">{link.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{t("cs.openModule")}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("cs.scopeTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-3 text-sm">
|
||||
<DashboardScopeMetric label={t("cs.playerCount")} value={String(overview.player_count)} />
|
||||
<DashboardScopeMetric
|
||||
label={t("cs.ticketsToday")}
|
||||
value={String(overview.ticket_order_count_today)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||
{t("cs.overviewEmpty")}
|
||||
</AdminNoResourceState>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
244
src/modules/dashboard/site-finance-dashboard-console.tsx
Normal file
244
src/modules/dashboard/site-finance-dashboard-console.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertTriangle, ClipboardList, RefreshCw, Scale, 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 { 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 { AdminNoResourceState } from "@/components/admin/admin-no-resource-state";
|
||||
import { DashboardCurrentDrawCard } from "@/modules/dashboard/dashboard-current-draw-card";
|
||||
import {
|
||||
AbnormalTransferPanelFooter,
|
||||
DashboardKpiCard,
|
||||
DashboardScopeMetric,
|
||||
DashboardStatRow,
|
||||
} from "@/modules/dashboard/dashboard-visuals";
|
||||
import { formatDashboardMoneyMinor } from "@/modules/dashboard/use-dashboard-analytics";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import type {
|
||||
AdminDashboardSiteFinanceOverview,
|
||||
AdminDashboardWarning,
|
||||
} from "@/types/api/admin-dashboard";
|
||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
|
||||
export function SiteFinanceDashboardConsole(): ReactElement {
|
||||
const { t } = useTranslation(["dashboard", "common"]);
|
||||
const tRef = useTranslationRef(["dashboard", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const site = profile?.site ?? null;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [apiWarnings, setApiWarnings] = useState<AdminDashboardWarning[]>([]);
|
||||
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
||||
const [drawId, setDrawId] = useState<number | null>(null);
|
||||
const [overview, setOverview] = useState<AdminDashboardSiteFinanceOverview | null>(null);
|
||||
const [walletPermission, setWalletPermission] = useState(false);
|
||||
|
||||
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.site_finance_overview);
|
||||
setApiWarnings(d.warnings ?? []);
|
||||
setWalletPermission(d.capabilities?.wallet_transfer_view ?? false);
|
||||
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 displayCurrency = overview?.currency_code ?? "NPR";
|
||||
const abnormalCount = overview?.abnormal_transfer_count ?? null;
|
||||
|
||||
const quickLinks = useMemo(
|
||||
() => [
|
||||
{ href: "/admin/reconcile", label: t("finance.quickLinks.reconcile") },
|
||||
{ href: "/admin/wallet/transfer-orders", label: t("finance.quickLinks.transfers") },
|
||||
{ href: "/admin/settlement-center", label: t("finance.quickLinks.bills") },
|
||||
{ href: "/admin/reports", label: t("finance.quickLinks.reports") },
|
||||
],
|
||||
[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("finance.title")}</h1>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{site
|
||||
? t("finance.subtitle", { name: site.name || site.code })
|
||||
: t("finance.subtitleFallback")}
|
||||
</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 && apiWarnings.length > 0 ? (
|
||||
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-900/60 dark:bg-amber-950/30">
|
||||
<AlertTitle>{t("notice")}</AlertTitle>
|
||||
<AlertDescription>{apiWarnings.map((w) => w.message).join(" ")}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{loading ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-24 rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
) : overview ? (
|
||||
<section className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<DashboardKpiCard
|
||||
label={t("finance.abnormalTransfers")}
|
||||
value={abnormalCount ?? "—"}
|
||||
icon={<AlertTriangle className="size-4" />}
|
||||
hint={t("abnormalTransferScope")}
|
||||
accent={(abnormalCount ?? 0) > 0 ? "destructive" : "muted"}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("finance.pendingConfirmBills")}
|
||||
value={overview.pending_confirm_bill_count}
|
||||
icon={<ClipboardList className="size-4" />}
|
||||
hint={t("finance.pendingConfirmHint")}
|
||||
accent={overview.pending_confirm_bill_count > 0 ? "primary" : "muted"}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("finance.payableBills")}
|
||||
value={overview.payable_bill_count}
|
||||
icon={<Scale className="size-4" />}
|
||||
hint={t("finance.payableUnpaid", {
|
||||
amount: formatDashboardMoneyMinor(overview.payable_unpaid_minor, displayCurrency),
|
||||
})}
|
||||
accent={overview.payable_bill_count > 0 ? "destructive" : "muted"}
|
||||
/>
|
||||
<DashboardKpiCard
|
||||
label={t("finance.walletPlayers")}
|
||||
value={overview.wallet_player_count}
|
||||
icon={<Users className="size-4" />}
|
||||
hint={t("finance.creditPlayersHint", { count: overview.credit_player_count })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("finance.settlementTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<DashboardStatRow
|
||||
label={t("finance.pendingConfirmBills")}
|
||||
value={String(overview.pending_confirm_bill_count)}
|
||||
/>
|
||||
<DashboardStatRow
|
||||
label={t("finance.payableBills")}
|
||||
value={String(overview.payable_bill_count)}
|
||||
/>
|
||||
<DashboardStatRow
|
||||
label={t("finance.payableUnpaidLabel")}
|
||||
value={formatDashboardMoneyMinor(overview.payable_unpaid_minor, displayCurrency)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("finance.reconcileTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<AbnormalTransferPanelFooter
|
||||
total={abnormalCount}
|
||||
walletPermission={walletPermission}
|
||||
/>
|
||||
<Link
|
||||
href="/admin/wallet/transfer-orders?abnormal=1"
|
||||
className={buttonVariants({ variant: "outline", size: "sm", className: "w-full" })}
|
||||
>
|
||||
<Wallet className="size-3.5" />
|
||||
{t("viewTransferOrders")}
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-semibold">{t("quickLinksTitle")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
{quickLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<AdminNoResourceState className="py-12 text-sm text-muted-foreground">
|
||||
{t("finance.overviewEmpty")}
|
||||
</AdminNoResourceState>
|
||||
)}
|
||||
|
||||
<DashboardCurrentDrawCard
|
||||
key={`${hall?.draw_no ?? "empty"}:${loading ? "loading" : "ready"}`}
|
||||
hall={hall}
|
||||
drawId={drawId}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user