Files
lotteryAdmin/src/modules/players/player-detail-console.tsx
kang 4484a7a77a
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Refactor agents console and related components
- Simplified share rate calculation in AgentsConsole by removing unnecessary checks and directly setting the profile share rate.
- Updated the use of `profileParentCaps` to always return total share rate in the agent profile.
- Removed unused variables and memoized calculations for improved performance.
- Cleaned up imports in various files, removing unused components and optimizing the code structure.
- Added `tRef` dependency to several useEffect hooks to ensure proper reactivity to translation changes.
- Enhanced report preview tables with better label handling for various statuses and actions.
- Updated wallet filter options to align with player-side transaction types.
- Introduced new properties in types for better type safety and clarity.
2026-06-30 17:55:00 +08:00

667 lines
29 KiB
TypeScript

"use client";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { useCallback, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { getAdminPlayer } from "@/api/admin-player";
import { getAdminPlayerTicketItems } from "@/api/admin-player-tickets";
import { getAdminTransferOrders, getAdminWalletTransactions } from "@/api/admin-wallet";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admin/admin-no-resource-state";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminLoadingState, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useAdminPlayCodeLabel } from "@/hooks/use-admin-play-type-catalog";
import { resolvePlayerStatusTone } from "@/lib/admin-status-tone";
import { AdminTableMoney } from "@/components/admin/admin-table-money";
import { formatAdminMinorUnits } from "@/lib/money";
import { PlayerCreditLedgerTab } from "@/modules/players/player-credit-ledger-tab";
import {
isCreditFundingPlayer,
playerAuthSourceLabel,
playerFundingModeLabel,
playerShowsTransferOrders,
} from "@/lib/player-funding";
import { formatPlayerCreditAmount } from "@/lib/admin-player-display";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminPlayerRow, AdminPlayerWalletRow } from "@/types/api/admin-player";
import type { AdminPlayerTicketItemRow } from "@/types/api/admin-player-tickets";
import type { AdminTransferOrderItem, AdminWalletTxnItem } from "@/types/api/admin-wallet";
function playerStatusLabel(status: number, t: (key: string) => string): string {
if (status === 0) return t("statusNormal");
if (status === 1) return t("statusFrozen");
if (status === 2) return t("statusBanned");
return String(status);
}
function ticketStatusText(status: string, t: (key: string, opts?: { ns?: string }) => string): string {
const key = `statusOptions.${status}`;
const translated = t(key, { ns: "tickets" });
return translated === key ? status : translated;
}
function walletStatusLabel(status: string, t: (key: string, opts?: { ns?: string }) => string): string {
switch (status) {
case "processing":
return t("statusProcessing", { ns: "wallet" });
case "success":
return t("statusSuccess", { ns: "wallet" });
case "failed":
return t("statusFailed", { ns: "wallet" });
case "pending_reconcile":
return t("statusPendingReconcile", { ns: "wallet" });
case "reversed":
return t("statusReversed", { ns: "wallet" });
case "manually_processed":
return t("statusCaseClosed", { ns: "wallet" });
case "posted":
return t("statusPosted", { ns: "wallet" });
default:
return status;
}
}
function PlayerStatusBadge({ status, t }: { status: number; t: (key: string) => string }) {
return (
<AdminStatusBadge status={String(status)} tone={resolvePlayerStatusTone(status)}>
{playerStatusLabel(status, t)}
</AdminStatusBadge>
);
}
function playerDisplayName(row: AdminPlayerRow): string {
return row.nickname?.trim() || row.username?.trim() || row.site_player_id;
}
function ProfileField({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="grid gap-1 sm:grid-cols-[7.5rem_1fr] sm:items-start">
<dt className="text-xs font-medium text-muted-foreground">{label}</dt>
<dd className="text-sm">{children}</dd>
</div>
);
}
export function PlayerDetailConsole({ playerId }: { playerId: number }) {
const { t } = useTranslation(["players", "tickets", "wallet", "common"]);
const tRef = useTranslationRef(["players", "tickets", "wallet", "common"]);
const formatDt = useAdminDateTimeFormatter();
const playCodeLabel = useAdminPlayCodeLabel();
useAdminCurrencyCatalog();
const [player, setPlayer] = useState<AdminPlayerRow | null>(null);
const [playerLoading, setPlayerLoading] = useState(true);
const [playerErr, setPlayerErr] = useState<string | null>(null);
const [ticketPage, setTicketPage] = useState(1);
const [ticketPerPage, setTicketPerPage] = useState(10);
const [tickets, setTickets] = useState<AdminPlayerTicketItemRow[]>([]);
const [ticketTotal, setTicketTotal] = useState(0);
const [ticketLastPage, setTicketLastPage] = useState(1);
const [ticketsLoading, setTicketsLoading] = useState(false);
const [ticketsErr, setTicketsErr] = useState<string | null>(null);
const [txnPage, setTxnPage] = useState(1);
const [txnPerPage, setTxnPerPage] = useState(10);
const [txns, setTxns] = useState<AdminWalletTxnItem[]>([]);
const [txnTotal, setTxnTotal] = useState(0);
const [txnLastPage, setTxnLastPage] = useState(1);
const [txnsLoading, setTxnsLoading] = useState(false);
const [txnsErr, setTxnsErr] = useState<string | null>(null);
const [transferPage, setTransferPage] = useState(1);
const [transferPerPage, setTransferPerPage] = useState(10);
const [transfers, setTransfers] = useState<AdminTransferOrderItem[]>([]);
const [transferTotal, setTransferTotal] = useState(0);
const [transferLastPage, setTransferLastPage] = useState(1);
const [transfersLoading, setTransfersLoading] = useState(false);
const [transfersErr, setTransfersErr] = useState<string | null>(null);
const loadPlayer = useCallback(async () => {
setPlayerLoading(true);
setPlayerErr(null);
try {
setPlayer(await getAdminPlayer(playerId));
} catch (e) {
setPlayer(null);
setPlayerErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
} finally {
setPlayerLoading(false);
}
}, [playerId, tRef]);
const loadTickets = useCallback(async () => {
setTicketsLoading(true);
setTicketsErr(null);
try {
const d = await getAdminPlayerTicketItems(playerId, {
page: ticketPage,
per_page: ticketPerPage,
});
setTickets(d.items);
setTicketTotal(d.total);
setTicketLastPage(Math.max(1, d.last_page));
} catch (e) {
setTickets([]);
setTicketTotal(0);
setTicketLastPage(1);
setTicketsErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
} finally {
setTicketsLoading(false);
}
}, [playerId, ticketPage, ticketPerPage, tRef]);
const loadTxns = useCallback(async () => {
setTxnsLoading(true);
setTxnsErr(null);
try {
const d = await getAdminWalletTransactions({
player_id: playerId,
page: txnPage,
per_page: txnPerPage,
});
setTxns(d.items);
setTxnTotal(d.total);
setTxnLastPage(Math.max(1, Math.ceil(d.total / d.per_page) || 1));
} catch (e) {
setTxns([]);
setTxnTotal(0);
setTxnLastPage(1);
setTxnsErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
} finally {
setTxnsLoading(false);
}
}, [playerId, txnPage, txnPerPage, tRef]);
const loadTransfers = useCallback(async () => {
setTransfersLoading(true);
setTransfersErr(null);
try {
const d = await getAdminTransferOrders({
player_id: playerId,
page: transferPage,
per_page: transferPerPage,
});
setTransfers(d.items);
setTransferTotal(d.total);
setTransferLastPage(Math.max(1, Math.ceil(d.total / d.per_page) || 1));
} catch (e) {
setTransfers([]);
setTransferTotal(0);
setTransferLastPage(1);
setTransfersErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
} finally {
setTransfersLoading(false);
}
}, [playerId, transferPage, transferPerPage, tRef]);
useAsyncEffect(() => {
void loadPlayer();
}, [loadPlayer]);
useAsyncEffect(() => {
if (!player) return;
void loadTickets();
}, [player, loadTickets]);
useAsyncEffect(() => {
if (!player || isCreditFundingPlayer(player)) return;
void loadTxns();
}, [player, loadTxns]);
useAsyncEffect(() => {
if (!player || !playerShowsTransferOrders(player)) return;
void loadTransfers();
}, [player, loadTransfers]);
if (playerLoading && !player) {
return <AdminLoadingState minHeight="8rem" className="py-8" />;
}
const isCreditPlayer = player ? isCreditFundingPlayer(player) : false;
const showTransferTab = player ? playerShowsTransferOrders(player) : false;
if (playerErr || !player) {
return (
<div className="space-y-4">
<Link href="/admin/players" className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "gap-1")}>
<ArrowLeft className="size-4" aria-hidden />
{t("backToList")}
</Link>
{playerErr ? (
<p className="text-sm text-destructive">{playerErr}</p>
) : (
<AdminNoResourceState />
)}
</div>
);
}
return (
<div className="space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-2">
<Link
href="/admin/players"
className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "-ml-2 gap-1")}
>
<ArrowLeft className="size-4" aria-hidden />
{t("backToList")}
</Link>
<div>
<h1 className="text-xl font-semibold tracking-tight">{playerDisplayName(player)}</h1>
<p className="mt-1 text-sm text-muted-foreground">
{t("detailSubtitle", {
site: player.site_code,
sitePlayerId: player.site_player_id,
playerId: player.id,
})}
</p>
</div>
</div>
<PlayerStatusBadge status={player.status} t={t} />
</div>
<Tabs defaultValue="overview" className="gap-4">
<TabsList
variant="line"
className="h-auto w-full justify-start gap-1 rounded-none border-b border-border/60 bg-transparent p-0 px-1"
>
<TabsTrigger value="overview">{t("tabOverview")}</TabsTrigger>
<TabsTrigger value="tickets">{t("tabTickets")}</TabsTrigger>
<TabsTrigger value="wallet">
{isCreditPlayer ? t("tabCreditLedger") : t("tabWalletTxns")}
</TabsTrigger>
{showTransferTab ? (
<TabsTrigger value="transfers">{t("tabTransferOrders")}</TabsTrigger>
) : null}
</TabsList>
<TabsContent value="overview" className="mt-0 space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">{t("profileSection")}</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<dl className="space-y-3">
<ProfileField label={t("site")}>{player.site_code}</ProfileField>
<ProfileField label={t("sitePlayerId")}>
<span className="font-mono">{player.site_player_id}</span>
</ProfileField>
<ProfileField label="ID">
<span className="font-mono">{player.id}</span>
</ProfileField>
<ProfileField label={t("username")}>{player.username ?? "—"}</ProfileField>
<ProfileField label={t("nickname")}>{player.nickname ?? "—"}</ProfileField>
</dl>
<dl className="space-y-3">
<ProfileField label={t("currency")}>{player.default_currency}</ProfileField>
<ProfileField label={t("fundingMode")}>
{playerFundingModeLabel(player, t)}
</ProfileField>
<ProfileField label={t("authSource")}>
{playerAuthSourceLabel(player, t)}
</ProfileField>
<ProfileField label={t("riskTags", { defaultValue: "风控标签" })}>
{player.risk_tags && player.risk_tags.length > 0 ? player.risk_tags.join(", ") : "—"}
</ProfileField>
<ProfileField label={t("status")}>
<PlayerStatusBadge status={player.status} t={t} />
</ProfileField>
<ProfileField label={t("lastLogin")}>
{player.last_login_at ? formatDt(player.last_login_at) : "—"}
</ProfileField>
<ProfileField label={t("createdAt")}>
{formatDt(player.created_at)}
</ProfileField>
<ProfileField label={t("agent")}>
{player.agent_name ?? player.agent_code ?? "—"}
{player.agent_code && player.agent_name ? (
<span className="ml-1 font-mono text-xs text-muted-foreground">
({player.agent_code})
</span>
) : null}
</ProfileField>
</dl>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">
{isCreditPlayer ? t("creditSection") : t("walletsSection")}
</CardTitle>
</CardHeader>
<CardContent>
{isCreditPlayer ? (
<dl className="grid gap-3 sm:grid-cols-3">
<div>
<p className="text-xs text-muted-foreground">{t("creditLimit")}</p>
<AdminTableMoney className="mt-1 text-sm font-semibold">
{player.credit_limit != null
? formatPlayerCreditAmount(player.credit_limit, player.default_currency)
: "—"}
</AdminTableMoney>
</div>
<div>
<p className="text-xs text-muted-foreground">{t("availableCredit")}</p>
<AdminTableMoney className="mt-1 text-sm font-semibold">
{player.available_credit != null
? formatPlayerCreditAmount(player.available_credit, player.default_currency)
: "—"}
</AdminTableMoney>
</div>
<div>
<p className="text-xs text-muted-foreground">{t("usedCredit")}</p>
<AdminTableMoney className="mt-1 text-sm text-muted-foreground">
{player.used_credit != null
? formatPlayerCreditAmount(player.used_credit, player.default_currency)
: "—"}
</AdminTableMoney>
</div>
</dl>
) : player.wallets.length === 0 ? (
<AdminNoResourceState className="text-sm text-muted-foreground" />
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{player.wallets.map((w: AdminPlayerWalletRow) => (
<div
key={`${w.wallet_type}-${w.currency_code}`}
className="rounded-lg border bg-muted/20 px-3 py-2.5"
>
<p className="text-xs font-medium text-muted-foreground">
{w.wallet_type} · {w.currency_code}
</p>
<p className="mt-1 text-sm">
{t("balance")}{" "}
<span className="font-semibold tabular-nums">
{formatAdminMinorUnits(w.balance, w.currency_code)}
</span>
</p>
<p className="text-xs text-muted-foreground">
{t("available")}{" "}
<span className="tabular-nums">
{formatAdminMinorUnits(w.available_balance, w.currency_code)}
</span>
{w.frozen_balance > 0 ? (
<>
{" · "}
{t("frozen", { defaultValue: "冻结" })}{" "}
<span className="tabular-nums">
{formatAdminMinorUnits(w.frozen_balance, w.currency_code)}
</span>
</>
) : null}
</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="tickets" className="mt-0">
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<div className="space-y-1">
<CardTitle className="admin-list-title">{t("tabTickets")}</CardTitle>
<p className="text-sm text-muted-foreground">{t("ticketTableHint")}</p>
</div>
</CardHeader>
<CardContent className="admin-list-content">
{ticketsErr ? (
<p className="mb-3 text-sm text-destructive">{ticketsErr}</p>
) : null}
<div className="admin-table-shell">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("ticketNo", { ns: "tickets" })}</TableHead>
<TableHead>{t("orderNo", { ns: "tickets" })}</TableHead>
<TableHead>{t("drawNo", { ns: "tickets" })}</TableHead>
<TableHead>{t("playCode", { ns: "tickets" })}</TableHead>
<TableHead>{t("number", { ns: "tickets" })}</TableHead>
<TableHead className="text-center">{t("actualDeduct", { ns: "tickets" })}</TableHead>
<TableHead>{t("status", { ns: "tickets" })}</TableHead>
<TableHead>{t("failReason", { ns: "tickets" })}</TableHead>
<TableHead className="text-center">{t("winAmount", { ns: "tickets" })}</TableHead>
<TableHead>{t("placedAt", { ns: "tickets" })}</TableHead>
<TableHead>{t("updatedAt", { ns: "tickets" })}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ticketsLoading && tickets.length === 0 ? (
<AdminTableLoadingRow colSpan={10} />
) : null}
{tickets.map((row) => (
<TableRow key={row.ticket_no}>
<TableCell className="font-mono text-xs">
<Link
href={`/admin/tickets/${encodeURIComponent(row.ticket_no)}`}
className="text-primary hover:underline"
>
{row.ticket_no}
</Link>
</TableCell>
<TableCell className="font-mono text-xs">{row.order_no ?? "—"}</TableCell>
<TableCell className="font-mono text-xs">{row.draw_no ?? "—"}</TableCell>
<TableCell className="text-xs">{playCodeLabel(row.play_code)}</TableCell>
<TableCell className="font-mono text-xs">{row.original_number ?? "—"}</TableCell>
<TableCell className="text-center text-xs tabular-nums">
{row.actual_deduct_amount_formatted}
</TableCell>
<TableCell className="text-xs">
<AdminStatusBadge status={row.status}>
{ticketStatusText(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="max-w-[14rem] text-xs text-muted-foreground">
{row.fail_reason_text ?? row.fail_reason_code ?? "—"}
</TableCell>
<TableCell className="text-center text-xs tabular-nums">
{row.jackpot_win_amount_minor > 0
? `${row.win_amount_formatted} + ${row.jackpot_win_amount_formatted}`
: row.win_amount_formatted}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.placed_at ? formatDt(row.placed_at) : "—"}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.updated_at ? formatDt(row.updated_at) : "—"}
</TableCell>
</TableRow>
))}
{!ticketsLoading && tickets.length === 0 ? (
<AdminTableNoResourceRow colSpan={10} className="text-muted-foreground" />
) : null}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="player-detail-tickets"
total={ticketTotal}
page={ticketPage}
lastPage={ticketLastPage}
perPage={ticketPerPage}
loading={ticketsLoading}
onPerPageChange={(n) => {
setTicketPerPage(n);
setTicketPage(1);
}}
onPageChange={setTicketPage}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="wallet" className="mt-0">
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<CardTitle className="admin-list-title">
{isCreditPlayer ? t("tabCreditLedger") : t("tabWalletTxns")}
</CardTitle>
</CardHeader>
<CardContent className="admin-list-content">
{isCreditPlayer ? (
<PlayerCreditLedgerTab
playerId={player.id}
siteCode={player.site_code}
currencyCode={player.default_currency}
/>
) : (
<>
{txnsErr ? (
<p className="mb-3 text-sm text-destructive">{txnsErr}</p>
) : null}
<div className="admin-table-shell">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("txnNo", { ns: "wallet" })}</TableHead>
<TableHead>{t("bizType", { ns: "wallet" })}</TableHead>
<TableHead className="text-center">{t("txnAmount")}</TableHead>
<TableHead className="text-center">{t("balanceAfterTxn")}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead>{t("createdAt")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{txnsLoading && txns.length === 0 ? <AdminTableLoadingRow colSpan={6} /> : null}
{txns.map((row) => (
<TableRow key={row.id}>
<TableCell className="font-mono text-xs">{row.txn_no}</TableCell>
<TableCell className="text-xs">{row.biz_type}</TableCell>
<TableCell className="text-center text-xs">
<AdminTableMoney>
{row.amount_formatted ?? formatAdminMinorUnits(row.amount, player.default_currency)}
</AdminTableMoney>
</TableCell>
<TableCell className="text-center text-xs">
<AdminTableMoney>
{formatAdminMinorUnits(row.balance_after, player.default_currency)}
</AdminTableMoney>
</TableCell>
<TableCell className="text-xs">
<AdminStatusBadge status={row.status}>
{walletStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
</TableCell>
</TableRow>
))}
{!txnsLoading && txns.length === 0 && !txnsErr ? (
<AdminTableNoResourceRow colSpan={6} className="text-muted-foreground" />
) : null}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="player-detail-txns"
total={txnTotal}
page={txnPage}
lastPage={txnLastPage}
perPage={txnPerPage}
loading={txnsLoading}
onPerPageChange={(n) => {
setTxnPerPage(n);
setTxnPage(1);
}}
onPageChange={setTxnPage}
/>
</>
)}
</CardContent>
</Card>
</TabsContent>
{showTransferTab ? (
<TabsContent value="transfers" className="mt-0">
<Card className="admin-list-card">
<CardHeader className="admin-list-header">
<CardTitle className="admin-list-title">{t("tabTransferOrders")}</CardTitle>
</CardHeader>
<CardContent className="admin-list-content">
{transfersErr ? (
<p className="mb-3 text-sm text-destructive">{transfersErr}</p>
) : null}
<div className="admin-table-shell">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("localTransferNo", { ns: "wallet" })}</TableHead>
<TableHead>{t("direction", { ns: "wallet" })}</TableHead>
<TableHead className="text-center">{t("amount", { ns: "wallet" })}</TableHead>
<TableHead>{t("status")}</TableHead>
<TableHead>{t("requestTime", { ns: "wallet" })}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transfersLoading && transfers.length === 0 ? (
<AdminTableLoadingRow colSpan={5} />
) : null}
{transfers.map((row) => (
<TableRow key={row.id}>
<TableCell className="font-mono text-xs">{row.transfer_no}</TableCell>
<TableCell className="text-xs">{row.direction}</TableCell>
<TableCell className="text-center text-xs tabular-nums">
{formatAdminMinorUnits(row.amount, row.currency_code)}
</TableCell>
<TableCell className="text-xs">
<AdminStatusBadge status={row.status}>
{walletStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
</TableCell>
</TableRow>
))}
{!transfersLoading && transfers.length === 0 && !transfersErr ? (
<AdminTableNoResourceRow colSpan={5} className="text-muted-foreground" />
) : null}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="player-detail-transfers"
total={transferTotal}
page={transferPage}
lastPage={transferLastPage}
perPage={transferPerPage}
loading={transfersLoading}
onPerPageChange={(n) => {
setTransferPerPage(n);
setTransferPage(1);
}}
onPageChange={setTransferPage}
/>
</CardContent>
</Card>
</TabsContent>
) : null}
</Tabs>
</div>
);
}