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) ? ( <>