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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user