"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 { 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 { formatAdminMinorUnits } from "@/lib/money"; 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 ( {playerStatusLabel(status, t)} ); } 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 (
{label}
{children}
); } 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(null); const [playerLoading, setPlayerLoading] = useState(true); const [playerErr, setPlayerErr] = useState(null); const [ticketPage, setTicketPage] = useState(1); const [ticketPerPage, setTicketPerPage] = useState(10); const [tickets, setTickets] = useState([]); const [ticketTotal, setTicketTotal] = useState(0); const [ticketLastPage, setTicketLastPage] = useState(1); const [ticketsLoading, setTicketsLoading] = useState(false); const [txnPage, setTxnPage] = useState(1); const [txnPerPage, setTxnPerPage] = useState(10); const [txns, setTxns] = useState([]); const [txnTotal, setTxnTotal] = useState(0); const [txnLastPage, setTxnLastPage] = useState(1); const [txnsLoading, setTxnsLoading] = useState(false); const [transferPage, setTransferPage] = useState(1); const [transferPerPage, setTransferPerPage] = useState(10); const [transfers, setTransfers] = useState([]); const [transferTotal, setTransferTotal] = useState(0); const [transferLastPage, setTransferLastPage] = useState(1); const [transfersLoading, setTransfersLoading] = useState(false); 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]); const loadTickets = useCallback(async () => { setTicketsLoading(true); 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 { setTickets([]); setTicketTotal(0); setTicketLastPage(1); } finally { setTicketsLoading(false); } }, [playerId, ticketPage, ticketPerPage]); const loadTxns = useCallback(async () => { setTxnsLoading(true); 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 { setTxns([]); setTxnTotal(0); setTxnLastPage(1); } finally { setTxnsLoading(false); } }, [playerId, txnPage, txnPerPage]); const loadTransfers = useCallback(async () => { setTransfersLoading(true); 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 { setTransfers([]); setTransferTotal(0); setTransferLastPage(1); } finally { setTransfersLoading(false); } }, [playerId, transferPage, transferPerPage]); useAsyncEffect(() => { void loadPlayer(); }, [loadPlayer]); useAsyncEffect(() => { if (!player) return; void loadTickets(); }, [player, loadTickets]); useAsyncEffect(() => { if (!player) return; void loadTxns(); }, [player, loadTxns]); useAsyncEffect(() => { if (!player) return; void loadTransfers(); }, [player, loadTransfers]); if (playerLoading && !player) { return ; } if (playerErr || !player) { return (
{t("backToList")}

{playerErr ?? t("states.noData", { ns: "common" })}

); } return (
{t("backToList")}

{playerDisplayName(player)}

{t("detailSubtitle", { site: player.site_code, sitePlayerId: player.site_player_id, playerId: player.id, })}

{t("tabOverview")} {t("tabTickets")} {t("tabWalletTxns")} {t("tabTransferOrders")} {t("profileSection")}
{player.site_code} {player.site_player_id} {player.id} {player.username ?? "—"} {player.nickname ?? "—"}
{player.default_currency} {player.last_login_at ? formatDt(player.last_login_at) : "—"} {formatDt(player.created_at)} {player.agent_name ?? player.agent_code ?? "—"} {player.agent_code && player.agent_name ? ( ({player.agent_code}) ) : null}
{t("walletsSection")} {player.wallets.length === 0 ? (

{t("states.noData", { ns: "common" })}

) : (
{player.wallets.map((w: AdminPlayerWalletRow) => (

{w.wallet_type} · {w.currency_code}

{t("balance")}{" "} {formatAdminMinorUnits(w.balance, w.currency_code)}

{t("available")}{" "} {formatAdminMinorUnits(w.available_balance, w.currency_code)} {w.frozen_balance > 0 ? ( <> {" · "} {t("frozen", { defaultValue: "冻结" })}{" "} {formatAdminMinorUnits(w.frozen_balance, w.currency_code)} ) : null}

))}
)}
{t("tabTickets")}
{t("ticketNo", { ns: "tickets" })} {t("drawNo", { ns: "tickets" })} {t("playCode", { ns: "tickets" })} {t("number", { ns: "tickets" })} {t("actualDeduct", { ns: "tickets" })} {t("status", { ns: "tickets" })} {t("placedAt", { ns: "tickets" })} {ticketsLoading && tickets.length === 0 ? ( ) : null} {tickets.map((row) => ( {row.ticket_no} {row.draw_no ?? "—"} {playCodeLabel(row.play_code)} {row.original_number ?? "—"} {row.actual_deduct_amount_formatted} {ticketStatusText(row.status, t)} {row.placed_at ? formatDt(row.placed_at) : "—"} ))} {!ticketsLoading && tickets.length === 0 ? ( {t("states.noData", { ns: "common" })} ) : null}
{ setTicketPerPage(n); setTicketPage(1); }} onPageChange={setTicketPage} />
{t("tabWalletTxns")}
{t("txnNo", { ns: "wallet" })} {t("bizType", { ns: "wallet" })} {t("txnAmount")} {t("balanceAfterTxn")} {t("status")} {t("createdAt")} {txnsLoading && txns.length === 0 ? : null} {txns.map((row) => ( {row.txn_no} {row.biz_type} {formatAdminMinorUnits(row.amount, player.default_currency)} {formatAdminMinorUnits(row.balance_after, player.default_currency)} {walletStatusLabel(row.status, t)} {row.created_at ? formatDt(row.created_at) : "—"} ))} {!txnsLoading && txns.length === 0 ? ( {t("states.noData", { ns: "common" })} ) : null}
{ setTxnPerPage(n); setTxnPage(1); }} onPageChange={setTxnPage} />
{t("tabTransferOrders")}
{t("localTransferNo", { ns: "wallet" })} {t("direction", { ns: "wallet" })} {t("amount", { ns: "wallet" })} {t("status")} {t("requestTime", { ns: "wallet" })} {transfersLoading && transfers.length === 0 ? ( ) : null} {transfers.map((row) => ( {row.transfer_no} {row.direction} {formatAdminMinorUnits(row.amount, row.currency_code)} {walletStatusLabel(row.status, t)} {row.created_at ? formatDt(row.created_at) : "—"} ))} {!transfersLoading && transfers.length === 0 ? ( {t("states.noData", { ns: "common" })} ) : null}
{ setTransferPerPage(n); setTransferPage(1); }} onPageChange={setTransferPage} />
); }