feat(admin, players): enhance player management and admin interfaces
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
Updated the player management components to improve error handling and display for various operations, including ticket loading and transaction retrieval. Introduced new status badges for better visual representation of player statuses. Enhanced localization support for error messages and improved the overall user experience in the admin console. Refactored components for better maintainability and clarity.
This commit is contained in:
@@ -29,7 +29,9 @@ 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,
|
||||
@@ -115,6 +117,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
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);
|
||||
@@ -122,6 +125,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
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);
|
||||
@@ -129,6 +133,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
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);
|
||||
@@ -145,6 +150,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
|
||||
const loadTickets = useCallback(async () => {
|
||||
setTicketsLoading(true);
|
||||
setTicketsErr(null);
|
||||
try {
|
||||
const d = await getAdminPlayerTicketItems(playerId, {
|
||||
page: ticketPage,
|
||||
@@ -153,17 +159,19 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
setTickets(d.items);
|
||||
setTicketTotal(d.total);
|
||||
setTicketLastPage(Math.max(1, d.last_page));
|
||||
} catch {
|
||||
} catch (e) {
|
||||
setTickets([]);
|
||||
setTicketTotal(0);
|
||||
setTicketLastPage(1);
|
||||
setTicketsErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
|
||||
} finally {
|
||||
setTicketsLoading(false);
|
||||
}
|
||||
}, [playerId, ticketPage, ticketPerPage]);
|
||||
}, [playerId, ticketPage, ticketPerPage, tRef]);
|
||||
|
||||
const loadTxns = useCallback(async () => {
|
||||
setTxnsLoading(true);
|
||||
setTxnsErr(null);
|
||||
try {
|
||||
const d = await getAdminWalletTransactions({
|
||||
player_id: playerId,
|
||||
@@ -173,17 +181,19 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
setTxns(d.items);
|
||||
setTxnTotal(d.total);
|
||||
setTxnLastPage(Math.max(1, Math.ceil(d.total / d.per_page) || 1));
|
||||
} catch {
|
||||
} catch (e) {
|
||||
setTxns([]);
|
||||
setTxnTotal(0);
|
||||
setTxnLastPage(1);
|
||||
setTxnsErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
|
||||
} finally {
|
||||
setTxnsLoading(false);
|
||||
}
|
||||
}, [playerId, txnPage, txnPerPage]);
|
||||
}, [playerId, txnPage, txnPerPage, tRef]);
|
||||
|
||||
const loadTransfers = useCallback(async () => {
|
||||
setTransfersLoading(true);
|
||||
setTransfersErr(null);
|
||||
try {
|
||||
const d = await getAdminTransferOrders({
|
||||
player_id: playerId,
|
||||
@@ -193,14 +203,15 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
setTransfers(d.items);
|
||||
setTransferTotal(d.total);
|
||||
setTransferLastPage(Math.max(1, Math.ceil(d.total / d.per_page) || 1));
|
||||
} catch {
|
||||
} catch (e) {
|
||||
setTransfers([]);
|
||||
setTransferTotal(0);
|
||||
setTransferLastPage(1);
|
||||
setTransfersErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
|
||||
} finally {
|
||||
setTransfersLoading(false);
|
||||
}
|
||||
}, [playerId, transferPage, transferPerPage]);
|
||||
}, [playerId, transferPage, transferPerPage, tRef]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
void loadPlayer();
|
||||
@@ -212,7 +223,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
}, [player, loadTickets]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
if (!player) return;
|
||||
if (!player || isCreditFundingPlayer(player)) return;
|
||||
void loadTxns();
|
||||
}, [player, loadTxns]);
|
||||
|
||||
@@ -344,27 +355,27 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
<dl className="grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("creditLimit")}</p>
|
||||
<p className="mt-1 text-sm font-semibold tabular-nums">
|
||||
<AdminTableMoney className="mt-1 text-sm font-semibold">
|
||||
{player.credit_limit != null
|
||||
? formatPlayerCreditAmount(player.credit_limit, player.default_currency)
|
||||
: "—"}
|
||||
</p>
|
||||
</AdminTableMoney>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("availableCredit")}</p>
|
||||
<p className="mt-1 text-sm font-semibold tabular-nums">
|
||||
<AdminTableMoney className="mt-1 text-sm font-semibold">
|
||||
{player.available_credit != null
|
||||
? formatPlayerCreditAmount(player.available_credit, player.default_currency)
|
||||
: "—"}
|
||||
</p>
|
||||
</AdminTableMoney>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">{t("usedCredit")}</p>
|
||||
<p className="mt-1 text-sm tabular-nums text-muted-foreground">
|
||||
<AdminTableMoney className="mt-1 text-sm text-muted-foreground">
|
||||
{player.used_credit != null
|
||||
? formatPlayerCreditAmount(player.used_credit, player.default_currency)
|
||||
: "—"}
|
||||
</p>
|
||||
</AdminTableMoney>
|
||||
</div>
|
||||
</dl>
|
||||
) : player.wallets.length === 0 ? (
|
||||
@@ -417,6 +428,9 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
</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>
|
||||
@@ -507,59 +521,76 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="admin-list-content">
|
||||
<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 tabular-nums">
|
||||
{formatAdminMinorUnits(row.amount, player.default_currency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-xs tabular-nums">
|
||||
{formatAdminMinorUnits(row.balance_after, player.default_currency)}
|
||||
</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 ? (
|
||||
<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}
|
||||
/>
|
||||
{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>
|
||||
@@ -571,6 +602,9 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
<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>
|
||||
@@ -603,7 +637,7 @@ export function PlayerDetailConsole({ playerId }: { playerId: number }) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{!transfersLoading && transfers.length === 0 ? (
|
||||
{!transfersLoading && transfers.length === 0 && !transfersErr ? (
|
||||
<AdminTableNoResourceRow colSpan={5} className="text-muted-foreground" />
|
||||
) : null}
|
||||
</TableBody>
|
||||
|
||||
Reference in New Issue
Block a user