feat(agents, players): enhance agent console and players panel functionality
Some checks failed
lotteryadmin CI / build (push) Has been cancelled

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.
This commit is contained in:
2026-06-17 15:23:46 +08:00
parent 7224d044ae
commit d6c7d2c361
4 changed files with 66 additions and 26 deletions

View File

@@ -31,6 +31,7 @@ This version has breaking changes — APIs, conventions, and file structure may
## Learned User Preferences ## Learned User Preferences
- 排障/上线评估时用 MCPpostgres直查库验证数据与权限勿仅凭代码推断生产态。
- 占成/授信/回水/上限等数值字段用 `AdminNumericStepper`(± 步进 + 可手输),勿单独裸 `input[type=number]` - 占成/授信/回水/上限等数值字段用 `AdminNumericStepper`(± 步进 + 可手输),勿单独裸 `input[type=number]`
- 对外文档(接入 + 后台运营手册)禁用 RBAC slug`prd.settlement.agent.manage`);对客户称「贵司」;排版忌 AI 感,正文对比度与字号可读优先。 - 对外文档(接入 + 后台运营手册)禁用 RBAC slug`prd.settlement.agent.manage`);对客户称「贵司」;排版忌 AI 感,正文对比度与字号可读优先。
- 文档 i18n`useTranslation` 须显式 `ns``returnObjects` 列表用 `Array.isArray` 守卫;避免节名与表头 key 冲突(如 `billStatus`)。 - 文档 i18n`useTranslation` 须显式 `ns``returnObjects` 列表用 `Array.isArray` 守卫;避免节名与表头 key 冲突(如 `billStatus`)。

View File

@@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "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", "build": "next build",
"start": "next start --port 3801", "start": "next start --port 3801",
"lint": "eslint" "lint": "eslint"

View File

@@ -160,7 +160,10 @@ export function AgentsConsole(): React.ReactElement {
isSuperAdmin || isSuperAdmin ||
adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]); adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]);
const [rootProfile, setRootProfile] = useState<AgentProfileRow | null>(null); const [rootProfile, setRootProfile] = useState<AgentProfileRow | null>(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") => { const resetProfileForm = (mode: "create" | "edit" = "create") => {
setProfileShareRate("0"); setProfileShareRate("0");
@@ -417,14 +420,31 @@ export function AgentsConsole(): React.ReactElement {
]); ]);
useEffect(() => { useEffect(() => {
if (!Number.isInteger(selectedNodeIdFromUrl) || selectedNodeIdFromUrl <= 0) { if (visibleAgentRows.length === 0) {
setSelectedNodeId(null);
return; return;
} }
if (!flatNodes.some((node) => node.id === selectedNodeIdFromUrl)) {
return; 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;
} }
setSelectedNodeId(selectedNodeIdFromUrl); if (
}, [flatNodes, selectedNodeIdFromUrl]); current !== null &&
visibleAgentRows.some((row) => row.id === current)
) {
return current;
}
return visibleAgentRows[0]?.id ?? null;
});
}, [visibleAgentRows, selectedNodeIdFromUrl]);
useAsyncEffect(() => { useAsyncEffect(() => {
if (selectedNode === null) { if (selectedNode === null) {
@@ -433,16 +453,34 @@ export function AgentsConsole(): React.ReactElement {
return; return;
} }
const nodeId = selectedNode.id;
let cancelled = false;
setSelectedProfileLoading(true); setSelectedProfileLoading(true);
void getAgentNodeProfile(selectedNode.id)
void getAgentNodeProfile(nodeId)
.then((row) => { .then((row) => {
if (cancelled) {
return;
}
setSelectedProfile(row); setSelectedProfile(row);
if (!nodeDialogOpen) { if (!nodeDialogOpen) {
applyProfileRowToForm(row); applyProfileRowToForm(row);
} }
}) })
.catch(() => setSelectedProfile(null)) .catch(() => {
.finally(() => setSelectedProfileLoading(false)); if (!cancelled) {
setSelectedProfile(null);
}
})
.finally(() => {
if (!cancelled) {
setSelectedProfileLoading(false);
}
});
return () => {
cancelled = true;
};
}, [selectedNode?.id, nodeDialogOpen]); }, [selectedNode?.id, nodeDialogOpen]);
useAsyncEffect(() => { useAsyncEffect(() => {
@@ -532,17 +570,6 @@ export function AgentsConsole(): React.ReactElement {
selectedProfileLoading, 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(() => { useEffect(() => {
setDetailTab("overview"); setDetailTab("overview");
}, [selectedNodeId]); }, [selectedNodeId]);

View File

@@ -66,8 +66,9 @@ import {
validateNativePlayerUsername, validateNativePlayerUsername,
} from "@/lib/admin-input-validation"; } from "@/lib/admin-input-validation";
import { adminHasAnyPermission } from "@/lib/admin-permissions"; 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 { isSiteAdminOperator } from "@/lib/admin-session-variants";
import { settlementBillOperableByBoundAgent } from "@/modules/settlement/settlement-bill-operable";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone"; import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { useAdminProfile } from "@/stores/admin-session"; import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors"; import { LotteryApiBizError } from "@/types/api/errors";
@@ -182,6 +183,10 @@ export function AgentsPlayersPanel({
(profileAllowsCreate && (profileAllowsCreate &&
adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE])); adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]));
const canManagePlayerRows = canCreatePlayer; const canManagePlayerRows = canCreatePlayer;
const canOperateBills =
isSuperAdmin ||
adminHasAnyPermission(profile?.permissions, [PRD_SETTLEMENT_AGENT_MANAGE]);
const canFinanceAdjustments = canOperateBills && boundAgent === null;
const effectiveAgentId = useMemo(() => { const effectiveAgentId = useMemo(() => {
if (agentNodeId !== null) { if (agentNodeId !== null) {
@@ -597,6 +602,10 @@ export function AgentsPlayersPanel({
() => billingBills.find((bill) => bill.id === selectedBillId) ?? null, () => billingBills.find((bill) => bill.id === selectedBillId) ?? null,
[billingBills, selectedBillId], [billingBills, selectedBillId],
); );
const canOperateSelectedBill =
selectedBill !== null &&
canOperateBills &&
settlementBillOperableByBoundAgent(selectedBill, boundAgent);
const billingCurrency = billingPlayer?.default_currency ?? "NPR"; const billingCurrency = billingPlayer?.default_currency ?? "NPR";
function resetBillingForm(): void { function resetBillingForm(): void {
@@ -1163,13 +1172,13 @@ export function AgentsPlayersPanel({
</div> </div>
</div> </div>
{selectedBill.status === "pending_confirm" ? ( {canOperateSelectedBill && selectedBill.status === "pending_confirm" ? (
<Button type="button" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestConfirmBillAction}> <Button type="button" className="w-full" disabled={billingBusy || confirmBusy} onClick={requestConfirmBillAction}>
{t("agents:settlementBills.confirm", { defaultValue: "确认账单" })} {t("agents:settlementBills.confirm", { defaultValue: "确认账单" })}
</Button> </Button>
) : null} ) : null}
{selectedBill.status !== "pending_confirm" && Number(selectedBill.unpaid_amount ?? 0) > 0 ? ( {canOperateSelectedBill && selectedBill.status !== "pending_confirm" && Number(selectedBill.unpaid_amount ?? 0) > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t("agents:settlementBills.paymentAmount", { defaultValue: "收付金额" })}</Label> <Label>{t("agents:settlementBills.paymentAmount", { defaultValue: "收付金额" })}</Label>
@@ -1207,7 +1216,10 @@ export function AgentsPlayersPanel({
{t("agents:settlementBills.paid", { defaultValue: "登记收付" })} {t("agents:settlementBills.paid", { defaultValue: "登记收付" })}
</Button> </Button>
{boundAgent === null ? ( {canFinanceAdjustments &&
["confirmed", "partial_paid", "overdue"].includes(selectedBill.status) &&
Number(selectedBill.unpaid_amount ?? 0) > 0 &&
!["adjustment", "reversal", "bad_debt"].includes(selectedBill.bill_type) ? (
<> <>
<div className="space-y-1 pt-2"> <div className="space-y-1 pt-2">
<Label>{t("agents:settlementBills.badDebtReason", { defaultValue: "核销原因" })}</Label> <Label>{t("agents:settlementBills.badDebtReason", { defaultValue: "核销原因" })}</Label>