feat(i18n, agents): enhance multi-language support with new agent-related translations
Added new translations for agent management features in English, Nepali, and Chinese, including terms related to sub-agents, player management, and validation messages. This update improves the user experience by ensuring consistent terminology across different languages in the agents console.
This commit is contained in:
@@ -37,6 +37,11 @@
|
||||
"loadFailed": "Failed to load agent tree",
|
||||
"siteLabel": "Site",
|
||||
"createChild": "Add child agent",
|
||||
"viewDownline": "View sub-agents and players",
|
||||
"downlineDialogTitle": "{{name}} — sub-agents and players",
|
||||
"downlineAgentsSection": "Sub-agents",
|
||||
"downlinePlayersSection": "Direct players",
|
||||
"downlineNoAgents": "No sub-agents yet",
|
||||
"editNode": "Edit node",
|
||||
"deleteNode": "Delete node",
|
||||
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
||||
@@ -59,6 +64,12 @@
|
||||
"deleteSuccess": "Deleted agent {{name}}",
|
||||
"saveFailed": "Save failed",
|
||||
"codeRequired": "Code and name are required",
|
||||
"nameRequired": "Agent name is required",
|
||||
"usernameRequired": "Login name is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordMinLength": "Password must be at least 8 characters",
|
||||
"bindAccountPasswordRequired": "This agent has no login account yet. Enter an initial password to create one.",
|
||||
"bindAccountHint": "No login account is bound yet. Saving will create and bind one automatically.",
|
||||
"modelGuide": "Agent layer controls data scope and delegation ceiling. Account permissions are assigned through roles.",
|
||||
"pageGuide": "Manage the agent tree, agent roles, agent accounts, and delegation ceilings here. Platform accounts and platform roles stay in platform governance pages.",
|
||||
"summary": {
|
||||
@@ -83,7 +94,17 @@
|
||||
"canCreateChildAgent": "Allow creating sub-agents",
|
||||
"cycleDaily": "Daily",
|
||||
"cycleWeekly": "Weekly",
|
||||
"cycleMonthly": "Monthly"
|
||||
"cycleMonthly": "Monthly",
|
||||
"loading": "Loading share and credit settings…",
|
||||
"loadFailed": "Failed to load share and credit settings. Close and try again.",
|
||||
"loadingBlocked": "Share and credit settings are still loading. Please wait before saving.",
|
||||
"validation": {
|
||||
"shareRange": "Share rate must be between 0 and 100",
|
||||
"creditInvalid": "Credit limit cannot be negative",
|
||||
"rebateLimitRange": "Rebate ceiling must be between 0 and 1 (e.g. 0.005 = 0.5%)",
|
||||
"defaultRebateRange": "Default player rebate must be between 0 and 1",
|
||||
"defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling"
|
||||
}
|
||||
},
|
||||
"settlementBills": {
|
||||
"title": "Agent bills",
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"loadFailed": "Failed to load agent tree",
|
||||
"siteLabel": "Site",
|
||||
"createChild": "Add child agent",
|
||||
"viewDownline": "चाइल्ड एजेन्ट र खेलाडी हेर्नुहोस्",
|
||||
"downlineDialogTitle": "{{name}} — चाइल्ड एजेन्ट र खेलाडी",
|
||||
"downlineAgentsSection": "चाइल्ड एजेन्ट",
|
||||
"downlinePlayersSection": "प्रत्यक्ष खेलाडी",
|
||||
"downlineNoAgents": "कुनै चाइल्ड एजेन्ट छैन",
|
||||
"editNode": "Edit node",
|
||||
"deleteNode": "Delete node",
|
||||
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
||||
@@ -59,6 +64,12 @@
|
||||
"deleteSuccess": "Deleted agent {{name}}",
|
||||
"saveFailed": "Save failed",
|
||||
"codeRequired": "Code and name are required",
|
||||
"nameRequired": "Agent name is required",
|
||||
"usernameRequired": "Login name is required",
|
||||
"passwordRequired": "Password is required",
|
||||
"passwordMinLength": "Password must be at least 8 characters",
|
||||
"bindAccountPasswordRequired": "This agent has no login account yet. Enter an initial password.",
|
||||
"bindAccountHint": "No login account is bound yet. Saving will create one.",
|
||||
"modelGuide": "एजेन्ट तहले डाटा स्कोप र delegation ceiling नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।",
|
||||
"pageGuide": "यहाँ एजेन्ट ट्री, एजेन्ट भूमिका, एजेन्ट खाता र delegation ceiling व्यवस्थापन गरिन्छ। प्लेटफर्म खाता र प्लेटफर्म भूमिका अलग पृष्ठमा राखिन्छ।",
|
||||
"summary": {
|
||||
@@ -83,7 +94,17 @@
|
||||
"canCreateChildAgent": "अधीनस्थ एजेन्ट सिर्जना अनुमति",
|
||||
"cycleDaily": "दैनिक",
|
||||
"cycleWeekly": "साप्ताहिक",
|
||||
"cycleMonthly": "मासिक"
|
||||
"cycleMonthly": "मासिक",
|
||||
"loading": "Loading share and credit settings…",
|
||||
"loadFailed": "Failed to load share and credit settings.",
|
||||
"loadingBlocked": "Share and credit settings are still loading.",
|
||||
"validation": {
|
||||
"shareRange": "Share rate must be between 0 and 100",
|
||||
"creditInvalid": "Credit limit cannot be negative",
|
||||
"rebateLimitRange": "Rebate ceiling must be between 0 and 1",
|
||||
"defaultRebateRange": "Default player rebate must be between 0 and 1",
|
||||
"defaultExceedsLimit": "Default player rebate cannot exceed the rebate ceiling"
|
||||
}
|
||||
},
|
||||
"settlementBills": {
|
||||
"title": "एजेन्ट बिल",
|
||||
|
||||
@@ -37,6 +37,11 @@
|
||||
"loadFailed": "加载代理列表失败",
|
||||
"siteLabel": "站点",
|
||||
"createChild": "添加下级代理",
|
||||
"viewDownline": "查看下级代理和玩家",
|
||||
"downlineDialogTitle": "{{name}} — 下级代理与玩家",
|
||||
"downlineAgentsSection": "下级代理",
|
||||
"downlinePlayersSection": "直属玩家",
|
||||
"downlineNoAgents": "暂无下级代理",
|
||||
"editNode": "编辑代理",
|
||||
"deleteNode": "删除代理",
|
||||
"deleteNodeConfirm": "删除后不可恢复,请确认该代理无下级、无账号、无角色绑定。",
|
||||
@@ -59,6 +64,12 @@
|
||||
"deleteSuccess": "已删除代理 {{name}}",
|
||||
"saveFailed": "保存失败",
|
||||
"codeRequired": "请填写代理名称和登录名",
|
||||
"nameRequired": "请填写代理名称",
|
||||
"usernameRequired": "请填写登录名",
|
||||
"passwordRequired": "请填写密码",
|
||||
"passwordMinLength": "密码至少 8 位",
|
||||
"bindAccountPasswordRequired": "该代理尚未绑定登录账号,请填写初始密码以创建账号",
|
||||
"bindAccountHint": "该代理尚无登录账号,保存时将自动创建并绑定。",
|
||||
"modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。",
|
||||
"pageGuide": "这里统一管理代理树、代理角色、代理账号与下放上限。平台账号和平台角色请到各自的平台治理页面维护。",
|
||||
"summary": {
|
||||
@@ -83,7 +94,17 @@
|
||||
"canCreateChildAgent": "允许创建下级代理",
|
||||
"cycleDaily": "日结",
|
||||
"cycleWeekly": "周结",
|
||||
"cycleMonthly": "月结"
|
||||
"cycleMonthly": "月结",
|
||||
"loading": "正在加载占成与授信…",
|
||||
"loadFailed": "加载占成与授信失败,请关闭后重试",
|
||||
"loadingBlocked": "占成与授信尚未加载完成,请稍候再保存",
|
||||
"validation": {
|
||||
"shareRange": "占成比例须在 0–100 之间",
|
||||
"creditInvalid": "授信额度不能为负数",
|
||||
"rebateLimitRange": "回水上限须在 0–1 之间(如 0.005 表示 0.5%)",
|
||||
"defaultRebateRange": "默认玩家回水须在 0–1 之间",
|
||||
"defaultExceedsLimit": "默认玩家回水不能超过回水上限"
|
||||
}
|
||||
},
|
||||
"settlementBills": {
|
||||
"title": "代理账单",
|
||||
|
||||
13
src/lib/agent-settlement-cycle.ts
Normal file
13
src/lib/agent-settlement-cycle.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const AGENT_SETTLEMENT_CYCLES = ["daily", "weekly", "monthly"] as const;
|
||||
|
||||
export type AgentSettlementCycle = (typeof AGENT_SETTLEMENT_CYCLES)[number];
|
||||
|
||||
export function normalizeAgentSettlementCycle(value: unknown): AgentSettlementCycle {
|
||||
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
|
||||
if (AGENT_SETTLEMENT_CYCLES.includes(normalized as AgentSettlementCycle)) {
|
||||
return normalized as AgentSettlementCycle;
|
||||
}
|
||||
|
||||
return "weekly";
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { Eye, Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
PRD_USERS_MANAGE,
|
||||
} from "@/lib/admin-prd";
|
||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
||||
import { normalizeAgentSettlementCycle } from "@/lib/agent-settlement-cycle";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import type { AgentNodeRow } from "@/types/api/admin-agent";
|
||||
@@ -91,18 +92,21 @@ export function AgentsConsole(): React.ReactElement {
|
||||
const profile = useAdminProfile();
|
||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
||||
|
||||
const canManageNode = adminHasAnyPermission(profile?.permissions, [PRD_AGENT_MANAGE]);
|
||||
const canViewAgents =
|
||||
profile?.is_super_admin === true ||
|
||||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENTS_ACCESS_ANY]);
|
||||
const canManageProfile = adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_AGENT_PROFILE_MANAGE,
|
||||
PRD_AGENT_MANAGE,
|
||||
]);
|
||||
const canProvision =
|
||||
profile?.is_super_admin === true ||
|
||||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]);
|
||||
const isSuperAdmin = profile?.is_super_admin === true;
|
||||
const canManageNode =
|
||||
isSuperAdmin || adminHasAnyPermission(profile?.permissions, [PRD_AGENT_MANAGE]);
|
||||
const canViewAgents =
|
||||
isSuperAdmin ||
|
||||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENTS_ACCESS_ANY]);
|
||||
const canManageProfile =
|
||||
isSuperAdmin ||
|
||||
adminHasAnyPermission(profile?.permissions, [
|
||||
PRD_AGENT_PROFILE_MANAGE,
|
||||
PRD_AGENT_MANAGE,
|
||||
]);
|
||||
const canProvision =
|
||||
isSuperAdmin ||
|
||||
adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_LINE_PROVISION_ACCESS_ANY]);
|
||||
const canViewSiteList = adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_SITES_ACCESS_ANY]);
|
||||
const canSwitchSite = isSuperAdmin || adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_ACCESS_ANY]);
|
||||
|
||||
@@ -137,10 +141,15 @@ export function AgentsConsole(): React.ReactElement {
|
||||
const [profileExtraRebate, setProfileExtraRebate] = useState(false);
|
||||
const [profileCanCreateChild, setProfileCanCreateChild] = useState(false);
|
||||
const [profileCanCreatePlayer, setProfileCanCreatePlayer] = useState(true);
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [profileLoaded, setProfileLoaded] = useState(true);
|
||||
const [editingNodeNeedsPrimaryAccount, setEditingNodeNeedsPrimaryAccount] = useState(false);
|
||||
const [downlineDialogNode, setDownlineDialogNode] = useState<AgentNodeRow | null>(null);
|
||||
|
||||
const boundAgent = profile?.agent ?? null;
|
||||
const canCreateChildAgent =
|
||||
isSuperAdmin || boundAgent?.can_create_child_agent !== false;
|
||||
const canCreateChildForNode = (_node: AgentNodeRow) => canManageNode && canCreateChildAgent;
|
||||
const canViewPlayersTab =
|
||||
isSuperAdmin ||
|
||||
(boundAgent?.can_create_player !== false &&
|
||||
@@ -162,12 +171,51 @@ export function AgentsConsole(): React.ReactElement {
|
||||
credit_limit: Number.parseInt(profileCreditLimit, 10) || 0,
|
||||
rebate_limit: Number.parseFloat(profileRebateLimit) || 0,
|
||||
default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0,
|
||||
settlement_cycle: profileSettlementCycle,
|
||||
settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle),
|
||||
can_grant_extra_rebate: profileExtraRebate,
|
||||
can_create_child_agent: profileCanCreateChild,
|
||||
can_create_player: profileCanCreatePlayer,
|
||||
});
|
||||
|
||||
const validateProfileFields = (): string | null => {
|
||||
const shareRate = Number.parseFloat(profileShareRate);
|
||||
const creditLimit = Number.parseInt(profileCreditLimit, 10);
|
||||
const rebateLimit = Number.parseFloat(profileRebateLimit);
|
||||
const defaultRebate = Number.parseFloat(profileDefaultRebate);
|
||||
|
||||
if (Number.isNaN(shareRate) || shareRate < 0 || shareRate > 100) {
|
||||
return t("profile.validation.shareRange", {
|
||||
defaultValue: "占成比例须在 0–100 之间",
|
||||
});
|
||||
}
|
||||
|
||||
if (Number.isNaN(creditLimit) || creditLimit < 0) {
|
||||
return t("profile.validation.creditInvalid", {
|
||||
defaultValue: "授信额度不能为负数",
|
||||
});
|
||||
}
|
||||
|
||||
if (Number.isNaN(rebateLimit) || rebateLimit < 0 || rebateLimit > 1) {
|
||||
return t("profile.validation.rebateLimitRange", {
|
||||
defaultValue: "回水上限须在 0–1 之间(如 0.005 表示 0.5%)",
|
||||
});
|
||||
}
|
||||
|
||||
if (Number.isNaN(defaultRebate) || defaultRebate < 0 || defaultRebate > 1) {
|
||||
return t("profile.validation.defaultRebateRange", {
|
||||
defaultValue: "默认玩家回水须在 0–1 之间",
|
||||
});
|
||||
}
|
||||
|
||||
if (rebateLimit > 0 && defaultRebate > rebateLimit) {
|
||||
return t("profile.validation.defaultExceedsLimit", {
|
||||
defaultValue: "默认玩家回水不能超过回水上限",
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const flatNodes = useMemo(() => flattenTree(tree), [tree]);
|
||||
const parentNameMap = useMemo(
|
||||
() => new Map<number, string>(flatNodes.map((node) => [node.id, node.name])),
|
||||
@@ -221,6 +269,13 @@ export function AgentsConsole(): React.ReactElement {
|
||||
const start = (currentPage - 1) * perPage;
|
||||
return filteredRows.slice(start, start + perPage);
|
||||
}, [currentPage, filteredRows, perPage]);
|
||||
const downlineChildAgents = useMemo(() => {
|
||||
if (downlineDialogNode === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flatNodes.filter((node) => node.parent_id === downlineDialogNode.id);
|
||||
}, [downlineDialogNode, flatNodes]);
|
||||
|
||||
const loadTree = useCallback(async (siteId?: number | null) => {
|
||||
setLoading(true);
|
||||
@@ -300,6 +355,9 @@ export function AgentsConsole(): React.ReactElement {
|
||||
setNodeUsername("");
|
||||
setNodePassword("");
|
||||
resetProfileForm("create");
|
||||
setProfileLoading(false);
|
||||
setProfileLoaded(true);
|
||||
setEditingNodeNeedsPrimaryAccount(false);
|
||||
setNodeDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -310,23 +368,40 @@ export function AgentsConsole(): React.ReactElement {
|
||||
setNodeName(node.name);
|
||||
setNodeStatus(node.status);
|
||||
setNodeUsername(node.username ?? "");
|
||||
setEditingNodeNeedsPrimaryAccount((node.username ?? "").trim() === "");
|
||||
setNodePassword("");
|
||||
resetProfileForm("edit");
|
||||
setNodeDialogOpen(true);
|
||||
if (canManageProfile) {
|
||||
void getAgentNodeProfile(node.id)
|
||||
.then((p) => {
|
||||
setProfileShareRate(String(p.total_share_rate ?? 0));
|
||||
setProfileCreditLimit(String(p.credit_limit ?? 0));
|
||||
setProfileRebateLimit(String(p.rebate_limit ?? 0));
|
||||
setProfileDefaultRebate(String(p.default_player_rebate ?? 0));
|
||||
setProfileSettlementCycle(p.settlement_cycle ?? "weekly");
|
||||
setProfileExtraRebate(Boolean(p.can_grant_extra_rebate));
|
||||
setProfileCanCreateChild(Boolean(p.can_create_child_agent));
|
||||
setProfileCanCreatePlayer(p.can_create_player !== false);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
if (!canManageProfile) {
|
||||
setProfileLoading(false);
|
||||
setProfileLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileLoading(true);
|
||||
setProfileLoaded(false);
|
||||
void getAgentNodeProfile(node.id)
|
||||
.then((p) => {
|
||||
setProfileShareRate(String(p.total_share_rate ?? 0));
|
||||
setProfileCreditLimit(String(p.credit_limit ?? 0));
|
||||
setProfileRebateLimit(String(p.rebate_limit ?? 0));
|
||||
setProfileDefaultRebate(String(p.default_player_rebate ?? 0));
|
||||
setProfileSettlementCycle(normalizeAgentSettlementCycle(p.settlement_cycle));
|
||||
setProfileExtraRebate(Boolean(p.can_grant_extra_rebate));
|
||||
setProfileCanCreateChild(Boolean(p.can_create_child_agent));
|
||||
setProfileCanCreatePlayer(p.can_create_player !== false);
|
||||
setProfileLoaded(true);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
t("profile.loadFailed", { defaultValue: "加载占成与授信失败,请关闭后重试" }),
|
||||
);
|
||||
setProfileLoaded(false);
|
||||
})
|
||||
.finally(() => {
|
||||
setProfileLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const renderRowActions = (node: AgentNodeRow) => {
|
||||
@@ -349,9 +424,15 @@ export function AgentsConsole(): React.ReactElement {
|
||||
key: "create-child",
|
||||
label: t("createChild", { defaultValue: "添加下级代理" }),
|
||||
icon: Plus,
|
||||
hidden: !canManageNode || !canCreateChildAgent,
|
||||
hidden: !canCreateChildForNode(node),
|
||||
onClick: () => openCreateChildForNode(node),
|
||||
},
|
||||
{
|
||||
key: "view-downline",
|
||||
label: t("viewDownline", { defaultValue: "查看下级代理和玩家" }),
|
||||
icon: Eye,
|
||||
onClick: () => setDownlineDialogNode(node),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: t("deleteNode", { defaultValue: "删除代理" }),
|
||||
@@ -384,8 +465,13 @@ export function AgentsConsole(): React.ReactElement {
|
||||
};
|
||||
|
||||
const saveNode = async () => {
|
||||
if (!nodeName.trim() || !nodeUsername.trim()) {
|
||||
toast.error(t("codeRequired", { defaultValue: "请填写代理名称和登录名" }));
|
||||
if (!nodeName.trim()) {
|
||||
toast.error(t("nameRequired", { defaultValue: "请填写代理名称" }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nodeUsername.trim()) {
|
||||
toast.error(t("usernameRequired", { defaultValue: "请填写登录名" }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -394,7 +480,38 @@ export function AgentsConsole(): React.ReactElement {
|
||||
return;
|
||||
}
|
||||
if (!nodePassword.trim()) {
|
||||
toast.error(t("users.password", { defaultValue: "密码" }));
|
||||
toast.error(t("passwordRequired", { defaultValue: "请填写密码" }));
|
||||
return;
|
||||
}
|
||||
if (nodePassword.trim().length < 8) {
|
||||
toast.error(t("passwordMinLength", { defaultValue: "密码至少 8 位" }));
|
||||
return;
|
||||
}
|
||||
} else if (nodePassword.trim() && nodePassword.trim().length < 8) {
|
||||
toast.error(t("passwordMinLength", { defaultValue: "密码至少 8 位" }));
|
||||
return;
|
||||
} else if (nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount && !nodePassword.trim()) {
|
||||
toast.error(
|
||||
t("bindAccountPasswordRequired", {
|
||||
defaultValue: "该代理尚未绑定登录账号,请填写初始密码以创建账号",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (canManageProfile) {
|
||||
if (nodeDialogMode === "edit" && !profileLoaded) {
|
||||
toast.error(
|
||||
t("profile.loadingBlocked", {
|
||||
defaultValue: "占成与授信尚未加载完成,请稍候再保存",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const profileError = validateProfileFields();
|
||||
if (profileError !== null) {
|
||||
toast.error(profileError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -415,7 +532,9 @@ export function AgentsConsole(): React.ReactElement {
|
||||
await putAgentNode(editingNodeId, {
|
||||
name: nodeName.trim(),
|
||||
username: nodeUsername.trim(),
|
||||
password: nodePassword.trim() || undefined,
|
||||
password: editingNodeNeedsPrimaryAccount
|
||||
? nodePassword.trim()
|
||||
: nodePassword.trim() || undefined,
|
||||
status: nodeStatus,
|
||||
});
|
||||
if (canManageProfile) {
|
||||
@@ -426,6 +545,14 @@ export function AgentsConsole(): React.ReactElement {
|
||||
|
||||
setNodeDialogOpen(false);
|
||||
await loadTree(adminSiteId);
|
||||
if (nodeDialogMode === "create" && targetParentId !== null && downlineDialogNode?.id === targetParentId) {
|
||||
const refreshedParent = flattenTree(
|
||||
(await getAgentTree(adminSiteId ?? undefined)).tree,
|
||||
).find((node) => node.id === targetParentId);
|
||||
if (refreshedParent) {
|
||||
setDownlineDialogNode(refreshedParent);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
|
||||
} finally {
|
||||
@@ -634,6 +761,104 @@ export function AgentsConsole(): React.ReactElement {
|
||||
) : null}
|
||||
</AdminPageCard>
|
||||
|
||||
<Dialog
|
||||
open={downlineDialogNode !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDownlineDialogNode(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="flex max-h-[min(90vh,52rem)] max-w-4xl flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{downlineDialogNode
|
||||
? t("downlineDialogTitle", {
|
||||
name: downlineDialogNode.name,
|
||||
defaultValue: "{{name}} — 下级代理与玩家",
|
||||
})
|
||||
: t("viewDownline", { defaultValue: "查看下级代理和玩家" })}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-6 overflow-y-auto pr-1">
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("downlineAgentsSection", { defaultValue: "下级代理" })}
|
||||
</h3>
|
||||
{downlineDialogNode && canCreateChildForNode(downlineDialogNode) ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => openCreateChildForNode(downlineDialogNode)}
|
||||
>
|
||||
<Plus className="size-4" aria-hidden />
|
||||
{t("createChild", { defaultValue: "添加下级代理" })}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="admin-table-shell">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("name", { defaultValue: "名称" })}</TableHead>
|
||||
<TableHead>{t("code", { defaultValue: "编码" })}</TableHead>
|
||||
<TableHead>{t("users.username", { defaultValue: "登录名" })}</TableHead>
|
||||
<TableHead className="w-20">{t("childrenCount", { defaultValue: "直属下级" })}</TableHead>
|
||||
<TableHead className="w-24">{t("status", { defaultValue: "状态" })}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{downlineChildAgents.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-muted-foreground">
|
||||
{t("downlineNoAgents", { defaultValue: "暂无下级代理" })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
downlineChildAgents.map((child) => (
|
||||
<TableRow key={child.id}>
|
||||
<TableCell className="font-medium">{child.name}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{child.code}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{child.username ?? "—"}</TableCell>
|
||||
<TableCell>{child.children?.length ?? 0}</TableCell>
|
||||
<TableCell>
|
||||
<AdminStatusBadge tone={resolveRoleStatusTone(child.status)}>
|
||||
{child.status === 1
|
||||
? t("common:status.enabled", { defaultValue: "Enabled" })
|
||||
: t("common:status.disabled", { defaultValue: "Disabled" })}
|
||||
</AdminStatusBadge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{canViewPlayersTab && downlineDialogNode ? (
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("downlinePlayersSection", { defaultValue: "直属玩家" })}
|
||||
</h3>
|
||||
<AgentsPlayersPanel
|
||||
siteCode={activeSiteCode}
|
||||
agentNodeId={downlineDialogNode.id}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setDownlineDialogNode(null)}>
|
||||
{t("common:actions.close", { defaultValue: "关闭" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={nodeDialogOpen} onOpenChange={setNodeDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -668,17 +893,24 @@ export function AgentsConsole(): React.ReactElement {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-password">
|
||||
{nodeDialogMode === "create"
|
||||
{nodeDialogMode === "create" || editingNodeNeedsPrimaryAccount
|
||||
? t("users.password", { defaultValue: "密码" })
|
||||
: t("resetPassword", { defaultValue: "重置密码" })}
|
||||
</Label>
|
||||
{nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bindAccountHint", {
|
||||
defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。",
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
<Input
|
||||
id="agent-password"
|
||||
type="password"
|
||||
value={nodePassword}
|
||||
onChange={(e) => setNodePassword(e.target.value)}
|
||||
placeholder={
|
||||
nodeDialogMode === "edit"
|
||||
nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount
|
||||
? t("passwordOptionalHint")
|
||||
: t("passwordPlaceholder")
|
||||
}
|
||||
@@ -696,7 +928,12 @@ export function AgentsConsole(): React.ReactElement {
|
||||
<p className="text-sm font-medium">
|
||||
{t("profile.section", { defaultValue: "占成与授信" })}
|
||||
</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{profileLoading ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("profile.loading", { defaultValue: "正在加载占成与授信…" })}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={cn("grid gap-3 sm:grid-cols-2", profileLoading ? "pointer-events-none opacity-50" : "")}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agent-share-rate">
|
||||
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||||
@@ -813,7 +1050,11 @@ export function AgentsConsole(): React.ReactElement {
|
||||
<Button type="button" variant="outline" onClick={() => setNodeDialogOpen(false)}>
|
||||
{t("common:actions.cancel", { defaultValue: "取消" })}
|
||||
</Button>
|
||||
<Button type="button" disabled={nodeSaving} onClick={() => void saveNode()}>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={nodeSaving || (nodeDialogMode === "edit" && canManageProfile && profileLoading)}
|
||||
onClick={() => void saveNode()}
|
||||
>
|
||||
{t("common:actions.save", { defaultValue: "保存" })}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user