From d6c7d2c361ed696e9b364345b88fd4ec549b1e76 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 17 Jun 2026 15:23:46 +0800 Subject: [PATCH] feat(agents, players): enhance agent console and players panel functionality Updated the AgentsConsole to improve node ID handling from URL parameters, ensuring better validation and selection logic. Enhanced the AgentsPlayersPanel by introducing new permissions for managing bills and refining the conditions for displaying bill actions based on user roles. Improved overall code clarity and maintainability through refactoring and added checks for selected bills. --- AGENTS.md | 1 + package.json | 2 +- src/modules/agents/agents-console.tsx | 69 ++++++++++++++------- src/modules/agents/agents-players-panel.tsx | 20 ++++-- 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22bcaf1..a6d784c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ This version has breaking changes — APIs, conventions, and file structure may ## Learned User Preferences +- 排障/上线评估时用 MCP(postgres)直查库验证数据与权限,勿仅凭代码推断生产态。 - 占成/授信/回水/上限等数值字段用 `AdminNumericStepper`(± 步进 + 可手输),勿单独裸 `input[type=number]`。 - 对外文档(接入 + 后台运营手册)禁用 RBAC slug(如 `prd.settlement.agent.manage`);对客户称「贵司」;排版忌 AI 感,正文对比度与字号可读优先。 - 文档 i18n:`useTranslation` 须显式 `ns`;`returnObjects` 列表用 `Array.isArray` 守卫;避免节名与表头 key 冲突(如 `billStatus`)。 diff --git a/package.json b/package.json index fdacb8b..b8f67c7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --port 3801", + "dev": "NEXT_DISABLE_MEM_OVERRIDE=1 NODE_OPTIONS='--max-old-space-size=3072' next dev --port 3801", "build": "next build", "start": "next start --port 3801", "lint": "eslint" diff --git a/src/modules/agents/agents-console.tsx b/src/modules/agents/agents-console.tsx index 48f6e09..cc04983 100644 --- a/src/modules/agents/agents-console.tsx +++ b/src/modules/agents/agents-console.tsx @@ -160,7 +160,10 @@ export function AgentsConsole(): React.ReactElement { isSuperAdmin || adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]); const [rootProfile, setRootProfile] = useState(null); - const selectedNodeIdFromUrl = Number.parseInt(searchParams.get("agent_node_id") ?? "", 10); + const selectedNodeIdFromUrl = Number.parseInt( + searchParams.get("agent_node_id") ?? searchParams.get("node") ?? "", + 10, + ); const resetProfileForm = (mode: "create" | "edit" = "create") => { setProfileShareRate("0"); @@ -417,14 +420,31 @@ export function AgentsConsole(): React.ReactElement { ]); useEffect(() => { - if (!Number.isInteger(selectedNodeIdFromUrl) || selectedNodeIdFromUrl <= 0) { + if (visibleAgentRows.length === 0) { + setSelectedNodeId(null); return; } - if (!flatNodes.some((node) => node.id === selectedNodeIdFromUrl)) { - return; - } - setSelectedNodeId(selectedNodeIdFromUrl); - }, [flatNodes, selectedNodeIdFromUrl]); + + const urlId = + Number.isInteger(selectedNodeIdFromUrl) && selectedNodeIdFromUrl > 0 + ? selectedNodeIdFromUrl + : null; + const urlValid = + urlId !== null && visibleAgentRows.some((row) => row.id === urlId); + + setSelectedNodeId((current) => { + if (urlValid) { + return urlId; + } + if ( + current !== null && + visibleAgentRows.some((row) => row.id === current) + ) { + return current; + } + return visibleAgentRows[0]?.id ?? null; + }); + }, [visibleAgentRows, selectedNodeIdFromUrl]); useAsyncEffect(() => { if (selectedNode === null) { @@ -433,16 +453,34 @@ export function AgentsConsole(): React.ReactElement { return; } + const nodeId = selectedNode.id; + let cancelled = false; setSelectedProfileLoading(true); - void getAgentNodeProfile(selectedNode.id) + + void getAgentNodeProfile(nodeId) .then((row) => { + if (cancelled) { + return; + } setSelectedProfile(row); if (!nodeDialogOpen) { applyProfileRowToForm(row); } }) - .catch(() => setSelectedProfile(null)) - .finally(() => setSelectedProfileLoading(false)); + .catch(() => { + if (!cancelled) { + setSelectedProfile(null); + } + }) + .finally(() => { + if (!cancelled) { + setSelectedProfileLoading(false); + } + }); + + return () => { + cancelled = true; + }; }, [selectedNode?.id, nodeDialogOpen]); useAsyncEffect(() => { @@ -532,17 +570,6 @@ export function AgentsConsole(): React.ReactElement { selectedProfileLoading, ]); - useAsyncEffect(() => { - if (visibleAgentRows.length === 0) { - setSelectedNodeId(null); - return; - } - - if (selectedNodeId === null || !visibleAgentRows.some((row) => row.id === selectedNodeId)) { - setSelectedNodeId(visibleAgentRows[0]?.id ?? null); - } - }, [visibleAgentRows, selectedNodeId]); - useEffect(() => { setDetailTab("overview"); }, [selectedNodeId]); diff --git a/src/modules/agents/agents-players-panel.tsx b/src/modules/agents/agents-players-panel.tsx index 1e212a3..ff3c5fb 100644 --- a/src/modules/agents/agents-players-panel.tsx +++ b/src/modules/agents/agents-players-panel.tsx @@ -66,8 +66,9 @@ import { validateNativePlayerUsername, } from "@/lib/admin-input-validation"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; -import { PRD_USERS_MANAGE } from "@/lib/admin-prd"; +import { PRD_SETTLEMENT_AGENT_MANAGE, PRD_USERS_MANAGE } from "@/lib/admin-prd"; import { isSiteAdminOperator } from "@/lib/admin-session-variants"; +import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { useAdminProfile } from "@/stores/admin-session"; import { LotteryApiBizError } from "@/types/api/errors"; @@ -182,6 +183,10 @@ export function AgentsPlayersPanel({ (profileAllowsCreate && adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE])); const canManagePlayerRows = canCreatePlayer; + const canOperateBills = + isSuperAdmin || + adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]); + const canFinanceAdjustments = canOperateBills && boundAgent === null; const effectiveAgentId = useMemo(() => { if (agentNodeId !== null) { @@ -597,6 +602,10 @@ export function AgentsPlayersPanel({ () => billingBills.find((bill) => bill.id === selectedBillId) ?? null, [billingBills, selectedBillId], ); + const canOperateSelectedBill = + selectedBill !== null && + canOperateBills && + settlementBillOperableByBoundAgent(selectedBill, boundAgent); const billingCurrency = billingPlayer?.default_currency ?? "NPR"; function resetBillingForm(): void { @@ -1163,13 +1172,13 @@ export function AgentsPlayersPanel({ - {selectedBill.status === "pending_confirm" ? ( + {canOperateSelectedBill && selectedBill.status === "pending_confirm" ? ( ) : null} - {selectedBill.status !== "pending_confirm" && Number(selectedBill.unpaid_amount ?? 0) > 0 ? ( + {canOperateSelectedBill && selectedBill.status !== "pending_confirm" && Number(selectedBill.unpaid_amount ?? 0) > 0 ? (
@@ -1207,7 +1216,10 @@ export function AgentsPlayersPanel({ {t("agents:settlementBills.paid", { defaultValue: "登记收付" })} - {boundAgent === null ? ( + {canFinanceAdjustments && + ["confirmed", "partial_paid", "overdue"].includes(selectedBill.status) && + Number(selectedBill.unpaid_amount ?? 0) > 0 && + !["adjustment", "reversal", "bad_debt"].includes(selectedBill.bill_type) ? ( <>