feat(agents, players): enhance agent console and players panel functionality
Some checks failed
lotteryadmin CI / build (push) Has been cancelled
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:
@@ -31,6 +31,7 @@ This version has breaking changes — APIs, conventions, and file structure may
|
|||||||
|
|
||||||
## Learned User Preferences
|
## Learned User Preferences
|
||||||
|
|
||||||
|
- 排障/上线评估时用 MCP(postgres)直查库验证数据与权限,勿仅凭代码推断生产态。
|
||||||
- 占成/授信/回水/上限等数值字段用 `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`)。
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user