feat(api, i18n): add agent_node_id to various admin queries and enhance multi-language support

Introduced the agent_node_id field in AdminDrawListQuery, AdminPlayerListQuery, AdminSettlementBatchListQuery, TicketItemsListQuery, and TransferOrderListQuery to improve filtering capabilities. Updated the admin-breadcrumb and admin-sidebar components to include new translations for agent-related terms in English, Nepali, and Chinese, enhancing the overall user experience and multi-language support across the admin interface.
This commit is contained in:
2026-06-02 14:37:08 +08:00
parent a4e7a2d228
commit b15e377187
105 changed files with 5305 additions and 1596 deletions

View File

@@ -1,8 +1,10 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { Copy, RotateCcw, Wrench } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import { toast } from "sonner";
import {
@@ -15,6 +17,8 @@ import {
} from "@/api/admin-wallet";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminAgentFilter } from "@/components/admin/admin-agent-filter";
import { AdminAgentIdentityCells, AdminAgentIdentityHeads } from "@/components/admin/admin-agent-columns";
import { AdminPlayerIdentityCells, AdminPlayerIdentityHeads } from "@/components/admin/admin-player-identity-columns";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
@@ -24,6 +28,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state";
import {
Select,
SelectContent,
@@ -125,6 +130,7 @@ function statusLabelT(status: string, t: (key: string) => string): string {
}
type TransferFilters = {
agentNodeId: number | undefined;
playerId: string;
playerAccount: string;
transferNo: string;
@@ -136,6 +142,7 @@ type TransferFilters = {
};
const emptyTransferFilters: TransferFilters = {
agentNodeId: undefined,
playerId: "",
playerAccount: "",
transferNo: "",
@@ -147,6 +154,7 @@ const emptyTransferFilters: TransferFilters = {
};
type TxnFilters = {
agentNodeId: number | undefined;
playerId: string;
playerAccount: string;
txnNo: string;
@@ -159,6 +167,7 @@ type TxnFilters = {
};
const emptyTxnFilters: TxnFilters = {
agentNodeId: undefined,
playerId: "",
playerAccount: "",
txnNo: "",
@@ -306,6 +315,7 @@ function TransferOrderRowActions({
export function TransferOrdersPanel(): React.ReactElement {
const { t } = useTranslation(["wallet", "common"]);
const tRef = useTranslationRef(["wallet", "common"]);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const profile = useAdminProfile();
const canWriteWallet = adminHasAnyPermission(profile?.permissions, [...PRD_WALLET_WRITE_ANY]);
@@ -386,21 +396,20 @@ export function TransferOrdersPanel(): React.ReactElement {
created_from: applied.createdFrom.trim() || undefined,
created_to: applied.createdTo.trim() || undefined,
status: applied.statusCsv.trim() || undefined,
agent_node_id: applied.agentNodeId,
});
setData(d);
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : t("loadFailed"));
setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage, applied, t]);
}, [page, perPage, applied]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
useAsyncEffect(() => {
void load();
}, [page, perPage, applied]);
const runSearch = () => {
setApplied({ ...draft });
@@ -421,6 +430,11 @@ export function TransferOrdersPanel(): React.ReactElement {
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<AdminAgentFilter
id="transfer-agent-filter"
value={draft.agentNodeId}
onChange={(id) => setDraft((d) => ({ ...d, agentNodeId: id }))}
/>
<div className="grid gap-1.5">
<Label htmlFor="to-transfer-no">{t("localTransferNo")}</Label>
<Input
@@ -531,11 +545,7 @@ export function TransferOrdersPanel(): React.ReactElement {
</div>
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
{loading && !data ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
) : null}
{data ? (
{(loading && !data) || data ? (
<>
<div className="rounded-md border">
<Table id="wallet-transfer-orders-table" className="table-fixed">
@@ -543,6 +553,7 @@ export function TransferOrdersPanel(): React.ReactElement {
<TableRow>
<TableHead className="min-w-0 max-w-[14rem]">{t("localTransferNo")}</TableHead>
<TableHead className="min-w-0 max-w-[12rem]">{t("externalRefNo")}</TableHead>
<AdminAgentIdentityHeads />
<AdminPlayerIdentityHeads />
<TableHead className="w-14">{t("direction")}</TableHead>
<TableHead className="whitespace-nowrap">{t("amount")}</TableHead>
@@ -554,9 +565,11 @@ export function TransferOrdersPanel(): React.ReactElement {
</TableRow>
</TableHeader>
<TableBody>
{data.items.length === 0 ? (
{loading && !data ? (
<AdminTableLoadingRow colSpan={13} />
) : !data || data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={12} className="text-muted-foreground">
<TableCell colSpan={13} className="text-muted-foreground">
{t("states.noData", { ns: "common" })}
</TableCell>
</TableRow>
@@ -569,6 +582,7 @@ export function TransferOrdersPanel(): React.ReactElement {
<TableCell className="min-w-0 max-w-[12rem] align-top whitespace-normal">
<CellMonoId value={row.external_ref_no} copyHint={t("copyExternalRefNo")} />
</TableCell>
<AdminAgentIdentityCells row={row} />
<AdminPlayerIdentityCells row={row} />
<TableCell>{row.direction}</TableCell>
<TableCell className="tabular-nums">
@@ -605,19 +619,21 @@ export function TransferOrdersPanel(): React.ReactElement {
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="wallet-transfer-orders-per-page"
total={data.total}
page={page}
lastPage={Math.max(1, Math.ceil(data.total / Math.max(1, data.per_page)))}
perPage={perPage}
loading={loading}
onPerPageChange={(next) => {
setPerPage(next);
setPage(1);
}}
onPageChange={setPage}
/>
{data ? (
<AdminListPaginationFooter
selectId="wallet-transfer-orders-per-page"
total={data.total}
page={page}
lastPage={Math.max(1, Math.ceil(data.total / Math.max(1, data.per_page)))}
perPage={perPage}
loading={loading}
onPerPageChange={(next) => {
setPerPage(next);
setPage(1);
}}
onPageChange={setPage}
/>
) : null}
</>
) : null}
</CardContent>
@@ -629,6 +645,7 @@ export function TransferOrdersPanel(): React.ReactElement {
export function WalletTxnsPanel(): React.ReactElement {
const { t } = useTranslation(["wallet", "common"]);
const tRef = useTranslationRef(["wallet", "common"]);
const exportLabels = useExportLabels("walletTransactions");
const formatTs = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminWalletTxnListData | null>(null);
@@ -660,21 +677,20 @@ export function WalletTxnsPanel(): React.ReactElement {
created_to: applied.createdTo.trim() || undefined,
biz_type: applied.bizType.trim() || undefined,
status: applied.statusCsv.trim() || undefined,
agent_node_id: applied.agentNodeId,
});
setData(d);
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : t("loadFailed"));
setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed"));
setData(null);
} finally {
setLoading(false);
}
}, [page, perPage, applied, t]);
}, [page, perPage, applied]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
useAsyncEffect(() => {
void load();
}, [page, perPage, applied]);
const runSearch = () => {
setApplied({ ...draft });
@@ -694,6 +710,11 @@ export function WalletTxnsPanel(): React.ReactElement {
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<AdminAgentFilter
id="wallet-txn-agent-filter"
value={draft.agentNodeId}
onChange={(id) => setDraft((d) => ({ ...d, agentNodeId: id }))}
/>
<div className="grid gap-1.5">
<Label htmlFor="tx-no">{t("txnNo")}</Label>
<Input
@@ -835,11 +856,7 @@ export function WalletTxnsPanel(): React.ReactElement {
</div>
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
{loading && !data ? (
<p className="text-sm text-muted-foreground">{t("states.loading", { ns: "common" })}</p>
) : null}
{data ? (
{(loading && !data) || data ? (
<>
<div className="rounded-md border">
<Table id="wallet-transactions-table" className="table-fixed">
@@ -847,6 +864,7 @@ export function WalletTxnsPanel(): React.ReactElement {
<TableRow>
<TableHead className="min-w-0 max-w-[14rem]">{t("txnNo")}</TableHead>
<TableHead className="min-w-0 max-w-[12rem]">{t("externalRefNo")}</TableHead>
<AdminAgentIdentityHeads />
<AdminPlayerIdentityHeads />
<TableHead className="whitespace-nowrap">{t("type")}</TableHead>
<TableHead className="whitespace-nowrap">{t("amount")}</TableHead>
@@ -856,9 +874,11 @@ export function WalletTxnsPanel(): React.ReactElement {
</TableRow>
</TableHeader>
<TableBody>
{data.items.length === 0 ? (
{loading && !data ? (
<AdminTableLoadingRow colSpan={11} />
) : !data || data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="text-muted-foreground">
<TableCell colSpan={11} className="text-muted-foreground">
{t("states.noData", { ns: "common" })}
</TableCell>
</TableRow>
@@ -871,6 +891,7 @@ export function WalletTxnsPanel(): React.ReactElement {
<TableCell className="min-w-0 max-w-[12rem] align-top whitespace-normal">
<CellMonoId value={row.external_ref_no} copyHint={t("copyExternalTxnRefNo")} />
</TableCell>
<AdminAgentIdentityCells row={row} />
<AdminPlayerIdentityCells row={row} />
<TableCell className="min-w-0 text-xs">{row.biz_type}</TableCell>
<TableCell className="tabular-nums text-xs">
@@ -891,19 +912,21 @@ export function WalletTxnsPanel(): React.ReactElement {
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId="wallet-transactions-per-page"
total={data.total}
page={page}
lastPage={Math.max(1, Math.ceil(data.total / Math.max(1, data.per_page)))}
perPage={perPage}
loading={loading}
onPerPageChange={(next) => {
setPerPage(next);
setPage(1);
}}
onPageChange={setPage}
/>
{data ? (
<AdminListPaginationFooter
selectId="wallet-transactions-per-page"
total={data.total}
page={page}
lastPage={Math.max(1, Math.ceil(data.total / Math.max(1, data.per_page)))}
perPage={perPage}
loading={loading}
onPerPageChange={(next) => {
setPerPage(next);
setPage(1);
}}
onPageChange={setPage}
/>
) : null}
</>
) : null}
</CardContent>
@@ -913,6 +936,7 @@ export function WalletTxnsPanel(): React.ReactElement {
export function PlayerWalletPanel(): React.ReactElement {
const { t } = useTranslation(["wallet", "common"]);
const tRef = useTranslationRef(["wallet", "common"]);
const exportLabels = useExportLabels("playerWallets");
useAdminCurrencyCatalog();
const [playerId, setPlayerId] = useState("");
@@ -923,7 +947,7 @@ export function PlayerWalletPanel(): React.ReactElement {
const query = useCallback(async () => {
const id = Number(playerId.trim());
if (Number.isNaN(id) || id < 1) {
setErr(t("invalidPlayerId"));
setErr(tRef.current("invalidPlayerId"));
setResult(null);
return;
}
@@ -933,12 +957,12 @@ export function PlayerWalletPanel(): React.ReactElement {
const d = await getAdminPlayerWallets(id);
setResult(d);
} catch (e) {
setErr(e instanceof LotteryApiBizError ? e.message : t("queryFailed"));
setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("queryFailed"));
setResult(null);
} finally {
setLoading(false);
}
}, [playerId, t]);
}, [playerId]);
return (
<Card>