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",
|
"loadFailed": "Failed to load agent tree",
|
||||||
"siteLabel": "Site",
|
"siteLabel": "Site",
|
||||||
"createChild": "Add child agent",
|
"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",
|
"editNode": "Edit node",
|
||||||
"deleteNode": "Delete node",
|
"deleteNode": "Delete node",
|
||||||
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
||||||
@@ -59,6 +64,12 @@
|
|||||||
"deleteSuccess": "Deleted agent {{name}}",
|
"deleteSuccess": "Deleted agent {{name}}",
|
||||||
"saveFailed": "Save failed",
|
"saveFailed": "Save failed",
|
||||||
"codeRequired": "Code and name are required",
|
"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.",
|
"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.",
|
"pageGuide": "Manage the agent tree, agent roles, agent accounts, and delegation ceilings here. Platform accounts and platform roles stay in platform governance pages.",
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -83,7 +94,17 @@
|
|||||||
"canCreateChildAgent": "Allow creating sub-agents",
|
"canCreateChildAgent": "Allow creating sub-agents",
|
||||||
"cycleDaily": "Daily",
|
"cycleDaily": "Daily",
|
||||||
"cycleWeekly": "Weekly",
|
"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": {
|
"settlementBills": {
|
||||||
"title": "Agent bills",
|
"title": "Agent bills",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"loadFailed": "Failed to load agent tree",
|
"loadFailed": "Failed to load agent tree",
|
||||||
"siteLabel": "Site",
|
"siteLabel": "Site",
|
||||||
"createChild": "Add child agent",
|
"createChild": "Add child agent",
|
||||||
|
"viewDownline": "चाइल्ड एजेन्ट र खेलाडी हेर्नुहोस्",
|
||||||
|
"downlineDialogTitle": "{{name}} — चाइल्ड एजेन्ट र खेलाडी",
|
||||||
|
"downlineAgentsSection": "चाइल्ड एजेन्ट",
|
||||||
|
"downlinePlayersSection": "प्रत्यक्ष खेलाडी",
|
||||||
|
"downlineNoAgents": "कुनै चाइल्ड एजेन्ट छैन",
|
||||||
"editNode": "Edit node",
|
"editNode": "Edit node",
|
||||||
"deleteNode": "Delete node",
|
"deleteNode": "Delete node",
|
||||||
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
"deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.",
|
||||||
@@ -59,6 +64,12 @@
|
|||||||
"deleteSuccess": "Deleted agent {{name}}",
|
"deleteSuccess": "Deleted agent {{name}}",
|
||||||
"saveFailed": "Save failed",
|
"saveFailed": "Save failed",
|
||||||
"codeRequired": "Code and name are required",
|
"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 नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।",
|
"modelGuide": "एजेन्ट तहले डाटा स्कोप र delegation ceiling नियन्त्रण गर्छ; खाताको अनुमति भूमिका मार्फत बाँडिन्छ।",
|
||||||
"pageGuide": "यहाँ एजेन्ट ट्री, एजेन्ट भूमिका, एजेन्ट खाता र delegation ceiling व्यवस्थापन गरिन्छ। प्लेटफर्म खाता र प्लेटफर्म भूमिका अलग पृष्ठमा राखिन्छ।",
|
"pageGuide": "यहाँ एजेन्ट ट्री, एजेन्ट भूमिका, एजेन्ट खाता र delegation ceiling व्यवस्थापन गरिन्छ। प्लेटफर्म खाता र प्लेटफर्म भूमिका अलग पृष्ठमा राखिन्छ।",
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -83,7 +94,17 @@
|
|||||||
"canCreateChildAgent": "अधीनस्थ एजेन्ट सिर्जना अनुमति",
|
"canCreateChildAgent": "अधीनस्थ एजेन्ट सिर्जना अनुमति",
|
||||||
"cycleDaily": "दैनिक",
|
"cycleDaily": "दैनिक",
|
||||||
"cycleWeekly": "साप्ताहिक",
|
"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": {
|
"settlementBills": {
|
||||||
"title": "एजेन्ट बिल",
|
"title": "एजेन्ट बिल",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"loadFailed": "加载代理列表失败",
|
"loadFailed": "加载代理列表失败",
|
||||||
"siteLabel": "站点",
|
"siteLabel": "站点",
|
||||||
"createChild": "添加下级代理",
|
"createChild": "添加下级代理",
|
||||||
|
"viewDownline": "查看下级代理和玩家",
|
||||||
|
"downlineDialogTitle": "{{name}} — 下级代理与玩家",
|
||||||
|
"downlineAgentsSection": "下级代理",
|
||||||
|
"downlinePlayersSection": "直属玩家",
|
||||||
|
"downlineNoAgents": "暂无下级代理",
|
||||||
"editNode": "编辑代理",
|
"editNode": "编辑代理",
|
||||||
"deleteNode": "删除代理",
|
"deleteNode": "删除代理",
|
||||||
"deleteNodeConfirm": "删除后不可恢复,请确认该代理无下级、无账号、无角色绑定。",
|
"deleteNodeConfirm": "删除后不可恢复,请确认该代理无下级、无账号、无角色绑定。",
|
||||||
@@ -59,6 +64,12 @@
|
|||||||
"deleteSuccess": "已删除代理 {{name}}",
|
"deleteSuccess": "已删除代理 {{name}}",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
"codeRequired": "请填写代理名称和登录名",
|
"codeRequired": "请填写代理名称和登录名",
|
||||||
|
"nameRequired": "请填写代理名称",
|
||||||
|
"usernameRequired": "请填写登录名",
|
||||||
|
"passwordRequired": "请填写密码",
|
||||||
|
"passwordMinLength": "密码至少 8 位",
|
||||||
|
"bindAccountPasswordRequired": "该代理尚未绑定登录账号,请填写初始密码以创建账号",
|
||||||
|
"bindAccountHint": "该代理尚无登录账号,保存时将自动创建并绑定。",
|
||||||
"modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。",
|
"modelGuide": "代理层负责数据范围(Scope)与授权上限(Ceiling),账号权限请通过角色分配。",
|
||||||
"pageGuide": "这里统一管理代理树、代理角色、代理账号与下放上限。平台账号和平台角色请到各自的平台治理页面维护。",
|
"pageGuide": "这里统一管理代理树、代理角色、代理账号与下放上限。平台账号和平台角色请到各自的平台治理页面维护。",
|
||||||
"summary": {
|
"summary": {
|
||||||
@@ -83,7 +94,17 @@
|
|||||||
"canCreateChildAgent": "允许创建下级代理",
|
"canCreateChildAgent": "允许创建下级代理",
|
||||||
"cycleDaily": "日结",
|
"cycleDaily": "日结",
|
||||||
"cycleWeekly": "周结",
|
"cycleWeekly": "周结",
|
||||||
"cycleMonthly": "月结"
|
"cycleMonthly": "月结",
|
||||||
|
"loading": "正在加载占成与授信…",
|
||||||
|
"loadFailed": "加载占成与授信失败,请关闭后重试",
|
||||||
|
"loadingBlocked": "占成与授信尚未加载完成,请稍候再保存",
|
||||||
|
"validation": {
|
||||||
|
"shareRange": "占成比例须在 0–100 之间",
|
||||||
|
"creditInvalid": "授信额度不能为负数",
|
||||||
|
"rebateLimitRange": "回水上限须在 0–1 之间(如 0.005 表示 0.5%)",
|
||||||
|
"defaultRebateRange": "默认玩家回水须在 0–1 之间",
|
||||||
|
"defaultExceedsLimit": "默认玩家回水不能超过回水上限"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settlementBills": {
|
"settlementBills": {
|
||||||
"title": "代理账单",
|
"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";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
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 { useCallback, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
PRD_USERS_MANAGE,
|
PRD_USERS_MANAGE,
|
||||||
} from "@/lib/admin-prd";
|
} from "@/lib/admin-prd";
|
||||||
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
|
||||||
|
import { normalizeAgentSettlementCycle } from "@/lib/agent-settlement-cycle";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAdminProfile } from "@/stores/admin-session";
|
import { useAdminProfile } from "@/stores/admin-session";
|
||||||
import type { AgentNodeRow } from "@/types/api/admin-agent";
|
import type { AgentNodeRow } from "@/types/api/admin-agent";
|
||||||
@@ -91,18 +92,21 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
const profile = useAdminProfile();
|
const profile = useAdminProfile();
|
||||||
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
|
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 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 canViewSiteList = adminHasAnyPermission(profile?.permissions, [...PRD_AGENT_SITES_ACCESS_ANY]);
|
||||||
const canSwitchSite = isSuperAdmin || adminHasAnyPermission(profile?.permissions, [...PRD_INTEGRATION_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 [profileExtraRebate, setProfileExtraRebate] = useState(false);
|
||||||
const [profileCanCreateChild, setProfileCanCreateChild] = useState(false);
|
const [profileCanCreateChild, setProfileCanCreateChild] = useState(false);
|
||||||
const [profileCanCreatePlayer, setProfileCanCreatePlayer] = useState(true);
|
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 boundAgent = profile?.agent ?? null;
|
||||||
const canCreateChildAgent =
|
const canCreateChildAgent =
|
||||||
isSuperAdmin || boundAgent?.can_create_child_agent !== false;
|
isSuperAdmin || boundAgent?.can_create_child_agent !== false;
|
||||||
|
const canCreateChildForNode = (_node: AgentNodeRow) => canManageNode && canCreateChildAgent;
|
||||||
const canViewPlayersTab =
|
const canViewPlayersTab =
|
||||||
isSuperAdmin ||
|
isSuperAdmin ||
|
||||||
(boundAgent?.can_create_player !== false &&
|
(boundAgent?.can_create_player !== false &&
|
||||||
@@ -162,12 +171,51 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
credit_limit: Number.parseInt(profileCreditLimit, 10) || 0,
|
credit_limit: Number.parseInt(profileCreditLimit, 10) || 0,
|
||||||
rebate_limit: Number.parseFloat(profileRebateLimit) || 0,
|
rebate_limit: Number.parseFloat(profileRebateLimit) || 0,
|
||||||
default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0,
|
default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0,
|
||||||
settlement_cycle: profileSettlementCycle,
|
settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle),
|
||||||
can_grant_extra_rebate: profileExtraRebate,
|
can_grant_extra_rebate: profileExtraRebate,
|
||||||
can_create_child_agent: profileCanCreateChild,
|
can_create_child_agent: profileCanCreateChild,
|
||||||
can_create_player: profileCanCreatePlayer,
|
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 flatNodes = useMemo(() => flattenTree(tree), [tree]);
|
||||||
const parentNameMap = useMemo(
|
const parentNameMap = useMemo(
|
||||||
() => new Map<number, string>(flatNodes.map((node) => [node.id, node.name])),
|
() => 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;
|
const start = (currentPage - 1) * perPage;
|
||||||
return filteredRows.slice(start, start + perPage);
|
return filteredRows.slice(start, start + perPage);
|
||||||
}, [currentPage, filteredRows, 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) => {
|
const loadTree = useCallback(async (siteId?: number | null) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -300,6 +355,9 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
setNodeUsername("");
|
setNodeUsername("");
|
||||||
setNodePassword("");
|
setNodePassword("");
|
||||||
resetProfileForm("create");
|
resetProfileForm("create");
|
||||||
|
setProfileLoading(false);
|
||||||
|
setProfileLoaded(true);
|
||||||
|
setEditingNodeNeedsPrimaryAccount(false);
|
||||||
setNodeDialogOpen(true);
|
setNodeDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,23 +368,40 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
setNodeName(node.name);
|
setNodeName(node.name);
|
||||||
setNodeStatus(node.status);
|
setNodeStatus(node.status);
|
||||||
setNodeUsername(node.username ?? "");
|
setNodeUsername(node.username ?? "");
|
||||||
|
setEditingNodeNeedsPrimaryAccount((node.username ?? "").trim() === "");
|
||||||
setNodePassword("");
|
setNodePassword("");
|
||||||
resetProfileForm("edit");
|
resetProfileForm("edit");
|
||||||
setNodeDialogOpen(true);
|
setNodeDialogOpen(true);
|
||||||
if (canManageProfile) {
|
|
||||||
void getAgentNodeProfile(node.id)
|
if (!canManageProfile) {
|
||||||
.then((p) => {
|
setProfileLoading(false);
|
||||||
setProfileShareRate(String(p.total_share_rate ?? 0));
|
setProfileLoaded(true);
|
||||||
setProfileCreditLimit(String(p.credit_limit ?? 0));
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
const renderRowActions = (node: AgentNodeRow) => {
|
||||||
@@ -349,9 +424,15 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
key: "create-child",
|
key: "create-child",
|
||||||
label: t("createChild", { defaultValue: "添加下级代理" }),
|
label: t("createChild", { defaultValue: "添加下级代理" }),
|
||||||
icon: Plus,
|
icon: Plus,
|
||||||
hidden: !canManageNode || !canCreateChildAgent,
|
hidden: !canCreateChildForNode(node),
|
||||||
onClick: () => openCreateChildForNode(node),
|
onClick: () => openCreateChildForNode(node),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "view-downline",
|
||||||
|
label: t("viewDownline", { defaultValue: "查看下级代理和玩家" }),
|
||||||
|
icon: Eye,
|
||||||
|
onClick: () => setDownlineDialogNode(node),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
label: t("deleteNode", { defaultValue: "删除代理" }),
|
label: t("deleteNode", { defaultValue: "删除代理" }),
|
||||||
@@ -384,8 +465,13 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveNode = async () => {
|
const saveNode = async () => {
|
||||||
if (!nodeName.trim() || !nodeUsername.trim()) {
|
if (!nodeName.trim()) {
|
||||||
toast.error(t("codeRequired", { defaultValue: "请填写代理名称和登录名" }));
|
toast.error(t("nameRequired", { defaultValue: "请填写代理名称" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeUsername.trim()) {
|
||||||
|
toast.error(t("usernameRequired", { defaultValue: "请填写登录名" }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +480,38 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!nodePassword.trim()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,7 +532,9 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
await putAgentNode(editingNodeId, {
|
await putAgentNode(editingNodeId, {
|
||||||
name: nodeName.trim(),
|
name: nodeName.trim(),
|
||||||
username: nodeUsername.trim(),
|
username: nodeUsername.trim(),
|
||||||
password: nodePassword.trim() || undefined,
|
password: editingNodeNeedsPrimaryAccount
|
||||||
|
? nodePassword.trim()
|
||||||
|
: nodePassword.trim() || undefined,
|
||||||
status: nodeStatus,
|
status: nodeStatus,
|
||||||
});
|
});
|
||||||
if (canManageProfile) {
|
if (canManageProfile) {
|
||||||
@@ -426,6 +545,14 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
|
|
||||||
setNodeDialogOpen(false);
|
setNodeDialogOpen(false);
|
||||||
await loadTree(adminSiteId);
|
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) {
|
} catch (e) {
|
||||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
|
toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -634,6 +761,104 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
) : null}
|
) : null}
|
||||||
</AdminPageCard>
|
</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}>
|
<Dialog open={nodeDialogOpen} onOpenChange={setNodeDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -668,17 +893,24 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="agent-password">
|
<Label htmlFor="agent-password">
|
||||||
{nodeDialogMode === "create"
|
{nodeDialogMode === "create" || editingNodeNeedsPrimaryAccount
|
||||||
? t("users.password", { defaultValue: "密码" })
|
? t("users.password", { defaultValue: "密码" })
|
||||||
: t("resetPassword", { defaultValue: "重置密码" })}
|
: t("resetPassword", { defaultValue: "重置密码" })}
|
||||||
</Label>
|
</Label>
|
||||||
|
{nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("bindAccountHint", {
|
||||||
|
defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<Input
|
<Input
|
||||||
id="agent-password"
|
id="agent-password"
|
||||||
type="password"
|
type="password"
|
||||||
value={nodePassword}
|
value={nodePassword}
|
||||||
onChange={(e) => setNodePassword(e.target.value)}
|
onChange={(e) => setNodePassword(e.target.value)}
|
||||||
placeholder={
|
placeholder={
|
||||||
nodeDialogMode === "edit"
|
nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount
|
||||||
? t("passwordOptionalHint")
|
? t("passwordOptionalHint")
|
||||||
: t("passwordPlaceholder")
|
: t("passwordPlaceholder")
|
||||||
}
|
}
|
||||||
@@ -696,7 +928,12 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{t("profile.section", { defaultValue: "占成与授信" })}
|
{t("profile.section", { defaultValue: "占成与授信" })}
|
||||||
</p>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="agent-share-rate">
|
<Label htmlFor="agent-share-rate">
|
||||||
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
{t("profile.totalShareRate", { defaultValue: "占成比例 (%)" })}
|
||||||
@@ -813,7 +1050,11 @@ export function AgentsConsole(): React.ReactElement {
|
|||||||
<Button type="button" variant="outline" onClick={() => setNodeDialogOpen(false)}>
|
<Button type="button" variant="outline" onClick={() => setNodeDialogOpen(false)}>
|
||||||
{t("common:actions.cancel", { defaultValue: "取消" })}
|
{t("common:actions.cancel", { defaultValue: "取消" })}
|
||||||
</Button>
|
</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: "保存" })}
|
{t("common:actions.save", { defaultValue: "保存" })}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user