"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 ( {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 [ticketsErr, setTicketsErr] = useState(null); 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 [txnsErr, setTxnsErr] = useState(null); 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 [transfersErr, setTransfersErr] = useState(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 ; } const isCreditPlayer = player ? isCreditFundingPlayer(player) : false; const showTransferTab = player ? playerShowsTransferOrders(player) : false; if (playerErr || !player) { return (
{t("backToList")} {playerErr ? (

{playerErr}

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

{playerDisplayName(player)}

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

{t("tabOverview")} {t("tabTickets")} {isCreditPlayer ? t("tabCreditLedger") : t("tabWalletTxns")} {showTransferTab ? ( {t("tabTransferOrders")} ) : null} {t("profileSection")}
{player.site_code} {player.site_player_id} {player.id} {player.username ?? "—"} {player.nickname ?? "—"}
{player.default_currency} {playerFundingModeLabel(player, t)} {playerAuthSourceLabel(player, t)} {player.risk_tags && player.risk_tags.length > 0 ? player.risk_tags.join(", ") : "—"} {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}
{isCreditPlayer ? t("creditSection") : t("walletsSection")} {isCreditPlayer ? (

{t("creditLimit")}

{player.credit_limit != null ? formatPlayerCreditAmount(player.credit_limit, player.default_currency) : "—"}

{t("availableCredit")}

{player.available_credit != null ? formatPlayerCreditAmount(player.available_credit, player.default_currency) : "—"}

{t("usedCredit")}

{player.used_credit != null ? formatPlayerCreditAmount(player.used_credit, player.default_currency) : "—"}
) : player.wallets.length === 0 ? ( ) : (
{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("ticketTableHint")}

{ticketsErr ? (

{ticketsErr}

) : null}
{t("ticketNo", { ns: "tickets" })} {t("orderNo", { ns: "tickets" })} {t("drawNo", { ns: "tickets" })} {t("playCode", { ns: "tickets" })} {t("number", { ns: "tickets" })} {t("actualDeduct", { ns: "tickets" })} {t("status", { ns: "tickets" })} {t("failReason", { ns: "tickets" })} {t("winAmount", { ns: "tickets" })} {t("placedAt", { ns: "tickets" })} {t("updatedAt", { ns: "tickets" })} {ticketsLoading && tickets.length === 0 ? ( ) : null} {tickets.map((row) => ( {row.ticket_no} {row.order_no ?? "—"} {row.draw_no ?? "—"} {playCodeLabel(row.play_code)} {row.original_number ?? "—"} {row.actual_deduct_amount_formatted} {ticketStatusText(row.status, t)} {row.fail_reason_text ?? row.fail_reason_code ?? "—"} {row.jackpot_win_amount_minor > 0 ? `${row.win_amount_formatted} + ${row.jackpot_win_amount_formatted}` : row.win_amount_formatted} {row.placed_at ? formatDt(row.placed_at) : "—"} {row.updated_at ? formatDt(row.updated_at) : "—"} ))} {!ticketsLoading && tickets.length === 0 ? ( ) : null}
{ setTicketPerPage(n); setTicketPage(1); }} onPageChange={setTicketPage} />
{isCreditPlayer ? t("tabCreditLedger") : t("tabWalletTxns")} {isCreditPlayer ? ( ) : ( <> {txnsErr ? (

{txnsErr}

) : null}
{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} {row.amount_formatted ?? 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 && !txnsErr ? ( ) : null}
{ setTxnPerPage(n); setTxnPage(1); }} onPageChange={setTxnPage} /> )}
{showTransferTab ? ( {t("tabTransferOrders")} {transfersErr ? (

{transfersErr}

) : null}
{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 && !transfersErr ? ( ) : null}
{ setTransferPerPage(n); setTransferPage(1); }} onPageChange={setTransferPage} />
) : null}
); }