"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}
/>
);
}