feat(admin, i18n): enhance admin dashboard and user management with new features and translations
Added the ability to filter admin dashboard data by site code and agent node ID, improving data retrieval capabilities. Introduced new functions for fetching dashboard data based on these parameters. Updated the admin users and roles management components to reflect these changes. Enhanced multi-language support by adding new translations for agent management and permission levels in English, Nepali, and Chinese, ensuring a consistent user experience across the admin interface.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Copy, RotateCcw, Wrench } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
||||
@@ -17,7 +18,6 @@ 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";
|
||||
@@ -121,7 +121,7 @@ function statusLabelT(status: string, t: (key: string) => string): string {
|
||||
case "reversed":
|
||||
return t("statusReversed");
|
||||
case "manually_processed":
|
||||
return t("statusManuallyProcessed");
|
||||
return t("statusCaseClosed");
|
||||
case "posted":
|
||||
return t("statusPosted");
|
||||
default:
|
||||
@@ -130,7 +130,6 @@ function statusLabelT(status: string, t: (key: string) => string): string {
|
||||
}
|
||||
|
||||
type TransferFilters = {
|
||||
agentNodeId: number | undefined;
|
||||
playerId: string;
|
||||
playerAccount: string;
|
||||
transferNo: string;
|
||||
@@ -142,7 +141,6 @@ type TransferFilters = {
|
||||
};
|
||||
|
||||
const emptyTransferFilters: TransferFilters = {
|
||||
agentNodeId: undefined,
|
||||
playerId: "",
|
||||
playerAccount: "",
|
||||
transferNo: "",
|
||||
@@ -154,7 +152,6 @@ const emptyTransferFilters: TransferFilters = {
|
||||
};
|
||||
|
||||
type TxnFilters = {
|
||||
agentNodeId: number | undefined;
|
||||
playerId: string;
|
||||
playerAccount: string;
|
||||
txnNo: string;
|
||||
@@ -167,7 +164,6 @@ type TxnFilters = {
|
||||
};
|
||||
|
||||
const emptyTxnFilters: TxnFilters = {
|
||||
agentNodeId: undefined,
|
||||
playerId: "",
|
||||
playerAccount: "",
|
||||
txnNo: "",
|
||||
@@ -203,7 +199,7 @@ const TRANSFER_ORDER_STATUS_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: "failed", label: "statusFailed" },
|
||||
{ value: "pending_reconcile", label: "statusPendingReconcile" },
|
||||
{ value: "reversed", label: "statusReversed" },
|
||||
{ value: "manually_processed", label: "statusManuallyProcessed" },
|
||||
{ value: "manually_processed", label: "statusCaseClosed" },
|
||||
];
|
||||
|
||||
/** Base UI 的 SelectValue 会直接显示 `value`,需把哨兵转成「不限」、其余转成选项文案 */
|
||||
@@ -251,6 +247,7 @@ function canManuallyProcessTransferOrder(
|
||||
row: {
|
||||
direction?: string;
|
||||
status: string;
|
||||
fail_reason?: string | null;
|
||||
can_manually_process?: boolean;
|
||||
},
|
||||
canWriteWallet: boolean,
|
||||
@@ -259,7 +256,8 @@ function canManuallyProcessTransferOrder(
|
||||
canWriteWallet &&
|
||||
(row.can_manually_process ??
|
||||
(["processing", "failed", "pending_reconcile"].includes(row.status) &&
|
||||
!(row.direction === "out" && row.status === "pending_reconcile")))
|
||||
!(row.direction === "out" && row.status === "pending_reconcile") &&
|
||||
row.fail_reason !== "lottery_credit_failed"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,7 +293,7 @@ function TransferOrderRowActions({
|
||||
},
|
||||
{
|
||||
key: "manual",
|
||||
label: t("manualProcess"),
|
||||
label: t("markCaseClosed"),
|
||||
icon: Wrench,
|
||||
hidden: !canManuallyProcessTransferOrder(row, canWriteWallet),
|
||||
onClick: () => onManualProcess(row.transfer_no),
|
||||
@@ -362,10 +360,10 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
|
||||
const handleManuallyProcess = (transferNo: string) =>
|
||||
requestConfirm({
|
||||
title: t("confirm.manualProcessTitle"),
|
||||
description: t("confirm.manualProcessDescription", { transferNo }),
|
||||
title: t("confirm.markCaseClosedTitle"),
|
||||
description: t("confirm.markCaseClosedDescription", { transferNo }),
|
||||
onConfirm: () =>
|
||||
doAction(transferNo, () => manuallyProcessTransferOrder(transferNo), t("manualProcessSuccess")),
|
||||
doAction(transferNo, () => manuallyProcessTransferOrder(transferNo), t("markCaseClosedSuccess")),
|
||||
});
|
||||
|
||||
const handleCompleteCredit = (transferNo: string) =>
|
||||
@@ -396,7 +394,6 @@ 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) {
|
||||
@@ -430,11 +427,6 @@ 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
|
||||
@@ -561,7 +553,7 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
<TableHead className="min-w-0 max-w-[14rem]">{t("failReason")}</TableHead>
|
||||
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("requestTime")}</TableHead>
|
||||
<TableHead className="min-w-0 whitespace-normal leading-tight">{t("finishedTime")}</TableHead>
|
||||
<TableHead className="w-12 text-center">{t("actions")}</TableHead>
|
||||
<TableHead className="sticky right-0 z-20 bg-muted w-12 text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">{t("table.actions", { ns: "common" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -600,7 +592,7 @@ export function TransferOrdersPanel(): React.ReactElement {
|
||||
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
|
||||
{formatTs(row.finished_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center align-middle">
|
||||
<TableCell className="sticky right-0 z-10 bg-card text-center align-middle shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
|
||||
<div className="flex justify-center">
|
||||
<TransferOrderRowActions
|
||||
row={row}
|
||||
@@ -655,6 +647,8 @@ export function WalletTxnsPanel(): React.ReactElement {
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [draft, setDraft] = useState<TxnFilters>(emptyTxnFilters);
|
||||
const [applied, setApplied] = useState<TxnFilters>(emptyTxnFilters);
|
||||
const searchParams = useSearchParams();
|
||||
const playerIdFromUrl = (searchParams.get("player_id") ?? "").trim();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -677,7 +671,6 @@ 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) {
|
||||
@@ -692,6 +685,15 @@ export function WalletTxnsPanel(): React.ReactElement {
|
||||
void load();
|
||||
}, [page, perPage, applied]);
|
||||
|
||||
useAsyncEffect(() => {
|
||||
if (!playerIdFromUrl) {
|
||||
return;
|
||||
}
|
||||
setDraft((d) => ({ ...d, playerId: playerIdFromUrl }));
|
||||
setApplied((d) => ({ ...d, playerId: playerIdFromUrl }));
|
||||
setPage(1);
|
||||
}, [playerIdFromUrl]);
|
||||
|
||||
const runSearch = () => {
|
||||
setApplied({ ...draft });
|
||||
setPage(1);
|
||||
@@ -710,11 +712,6 @@ 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
|
||||
|
||||
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { PRD_WALLET_PLAYER_ACCESS_ANY } from "@/lib/admin-prd";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
|
||||
@@ -17,6 +18,7 @@ const RECONCILE_PERMS = [
|
||||
const tabs: { href: string; label: string; requiredAny: readonly string[] }[] = [
|
||||
{ href: "/admin/wallet/transactions", label: "subnavTransactions", requiredAny: RECONCILE_PERMS },
|
||||
{ href: "/admin/wallet/transfer-orders", label: "subnavTransferOrders", requiredAny: RECONCILE_PERMS },
|
||||
{ href: "/admin/wallet/player", label: "subnavPlayerWallet", requiredAny: PRD_WALLET_PLAYER_ACCESS_ANY },
|
||||
];
|
||||
|
||||
export function WalletSubnav(): React.ReactElement {
|
||||
|
||||
Reference in New Issue
Block a user