"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { deleteAgentNode, getAgentNodeProfile, getAgentTree, postAgentNode, putAgentNode, putAgentNodeProfile, } from "@/api/admin-agents"; import { AgentLineDetailPanel, type AgentDetailTab, } from "@/modules/agents/agent-line-detail-panel"; import { AgentLineSidebar } from "@/modules/agents/agent-line-sidebar"; import { AgentProfileFields } from "@/modules/agents/agent-profile-fields"; import { AdminLoadingState } from "@/components/admin/admin-loading-state"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useConfirmAction } from "@/hooks/use-confirm-action"; import { useTranslationRef } from "@/hooks/use-translation-ref"; import { percentUiToRatio, percentValueToUi, parsePercentUi, ratioToPercentUi, } from "@/lib/admin-rate-percent"; import { adminHasAnyPermission } from "@/lib/admin-permissions"; import { PRD_AGENT_MANAGE, PRD_AGENT_PROFILE_MANAGE, PRD_AGENTS_ACCESS_ANY, PRD_USERS_MANAGE, } from "@/lib/admin-prd"; import { normalizeAgentSettlementCycle } from "@/lib/agent-settlement-cycle"; import { useAdminSiteCodeOptions } from "@/hooks/use-admin-site-code-options"; import { useAdminProfile } from "@/stores/admin-session"; import { useAgentManagementSiteStore } from "@/stores/agent-management-site"; import type { AgentNodeRow, AgentParentCaps, AgentProfileRow } from "@/types/api/admin-agent"; import { LotteryApiBizError } from "@/types/api/errors"; function parseRiskTagsInput(text: string): string[] { return Array.from( new Set( text .split(/[,,\s]+/) .map((tag) => tag.trim()) .filter((tag) => tag.length > 0), ), ); } function flattenTree(nodes: AgentNodeRow[]): AgentNodeRow[] { const out: AgentNodeRow[] = []; const walk = (list: AgentNodeRow[]) => { for (const node of list) { out.push(node); if (node.children?.length) { walk(node.children); } } }; walk(nodes); return out; } export function AgentsConsole(): React.ReactElement { const { t } = useTranslation(["agents", "common"]); const tRef = useTranslationRef(["agents", "common"]); const profile = useAdminProfile(); const { request: requestConfirm, ConfirmDialog } = useConfirmAction(); 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 { sites: siteOptions } = useAdminSiteCodeOptions(); const adminSiteId = useAgentManagementSiteStore((s) => s.adminSiteId); const setAdminSiteId = useAgentManagementSiteStore((s) => s.setAdminSiteId); const [tree, setTree] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [keyword, setKeyword] = useState(""); const [selectedNodeId, setSelectedNodeId] = useState(null); const [detailTab, setDetailTab] = useState("overview"); const [profileSaving, setProfileSaving] = useState(false); const [selectedProfile, setSelectedProfile] = useState(null); const [selectedProfileLoading, setSelectedProfileLoading] = useState(false); const [nodeDialogOpen, setNodeDialogOpen] = useState(false); const [nodeDialogMode, setNodeDialogMode] = useState<"create" | "edit">("create"); const [targetParentId, setTargetParentId] = useState(null); const [editingNodeId, setEditingNodeId] = useState(null); const [nodeName, setNodeName] = useState(""); const [nodeStatus, setNodeStatus] = useState(1); const [nodeUsername, setNodeUsername] = useState(""); const [nodePassword, setNodePassword] = useState(""); const [nodeSaving, setNodeSaving] = useState(false); const [profileShareRate, setProfileShareRate] = useState("0"); const [profileCreditLimit, setProfileCreditLimit] = useState("0"); const [profileRebateLimit, setProfileRebateLimit] = useState("0"); const [profileDefaultRebate, setProfileDefaultRebate] = useState("0"); const [profileSettlementCycle, setProfileSettlementCycle] = useState< "daily" | "weekly" | "monthly" >("weekly"); const [profileExtraRebate, setProfileExtraRebate] = useState(false); const [profileCanCreateChild, setProfileCanCreateChild] = useState(false); const [profileCanCreatePlayer, setProfileCanCreatePlayer] = useState(true); const [profileRiskTags, setProfileRiskTags] = useState(""); const [profileLoading, setProfileLoading] = useState(false); const [profileLoaded, setProfileLoaded] = useState(true); const [profileParentCaps, setProfileParentCaps] = useState(null); const [profileAvailableCredit, setProfileAvailableCredit] = useState(null); const [editingNodeNeedsPrimaryAccount, setEditingNodeNeedsPrimaryAccount] = useState(false); const boundAgent = profile?.agent ?? null; /** 登录账号是否可向子代理下放「允许创建下级」 */ const canCreateChildAgent = isSuperAdmin || boundAgent?.can_create_child_agent !== false; const hasUsersManagePermission = isSuperAdmin || adminHasAnyPermission(profile?.permissions, [PRD_USERS_MANAGE]); const [rootProfile, setRootProfile] = useState(null); const resetProfileForm = (mode: "create" | "edit" = "create") => { setProfileShareRate("0"); setProfileCreditLimit("0"); setProfileRebateLimit("0"); setProfileDefaultRebate("0"); setProfileSettlementCycle("weekly"); setProfileExtraRebate(false); setProfileCanCreateChild(mode === "create" ? false : false); setProfileCanCreatePlayer(true); setProfileRiskTags(""); setProfileParentCaps(null); setProfileAvailableCredit(null); }; const applyProfileRowToForm = (row: AgentProfileRow) => { setProfileShareRate(percentValueToUi(row.total_share_rate ?? 0)); setProfileCreditLimit(String(row.credit_limit ?? 0)); setProfileRebateLimit(ratioToPercentUi(row.rebate_limit ?? 0)); setProfileDefaultRebate(ratioToPercentUi(row.default_player_rebate ?? 0)); setProfileSettlementCycle(normalizeAgentSettlementCycle(row.settlement_cycle)); setProfileExtraRebate(Boolean(row.can_grant_extra_rebate)); setProfileCanCreateChild(Boolean(row.can_create_child_agent)); setProfileCanCreatePlayer(row.can_create_player !== false); setProfileParentCaps(row.parent_caps ?? null); setProfileAvailableCredit(row.available_credit ?? null); setProfileRiskTags((row.risk_tags ?? []).join(", ")); }; const profilePayload = () => ({ total_share_rate: Number.parseFloat(profileShareRate) || 0, credit_limit: Number.parseInt(profileCreditLimit, 10) || 0, rebate_limit: percentUiToRatio(profileRebateLimit), default_player_rebate: percentUiToRatio(profileDefaultRebate), settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle), can_grant_extra_rebate: profileExtraRebate, can_create_child_agent: profileCanCreateChild, can_create_player: profileCanCreatePlayer, risk_tags: parseRiskTagsInput(profileRiskTags), }); const validateProfileFields = (): string | null => { const shareRate = Number.parseFloat(profileShareRate); const creditLimit = Number.parseInt(profileCreditLimit, 10); const rebateLimit = parsePercentUi(profileRebateLimit); const defaultRebate = parsePercentUi(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 (rebateLimit === null || rebateLimit < 0 || rebateLimit > 100) { return t("profile.validation.rebateLimitRange", { defaultValue: "回水上限须在 0–100% 之间", }); } if (defaultRebate === null || defaultRebate < 0 || defaultRebate > 100) { return t("profile.validation.defaultRebateRange", { defaultValue: "默认玩家回水须在 0–100% 之间", }); } if (rebateLimit > 0 && defaultRebate > rebateLimit) { return t("profile.validation.defaultExceedsLimit", { defaultValue: "默认玩家回水不能超过回水上限", }); } return null; }; const flatNodes = useMemo(() => flattenTree(tree), [tree]); const parentNameMap = useMemo( () => new Map(flatNodes.map((node) => [node.id, node.name])), [flatNodes], ); const businessRows = useMemo(() => flatNodes.filter((node) => !node.is_root), [flatNodes]); const selectedSiteLabel = useMemo( () => siteOptions.find((site) => site.id === adminSiteId)?.name ?? null, [adminSiteId, siteOptions], ); const activeSiteCode = useMemo(() => { const fromAgent = boundAgent?.site_code?.trim(); if (fromAgent) { return fromAgent; } const fromSite = siteOptions.find((site) => site.id === adminSiteId)?.code?.trim(); if (fromSite) { return fromSite; } return flatNodes.find((node) => node.depth === 0)?.code?.trim() ?? ""; }, [adminSiteId, boundAgent?.site_code, flatNodes, siteOptions]); const rootNode = useMemo( () => flatNodes.find((node) => node.is_root || node.depth === 0) ?? null, [flatNodes], ); const selectedNode = useMemo( () => selectedNodeId !== null ? (flatNodes.find((node) => node.id === selectedNodeId) ?? null) : null, [flatNodes, selectedNodeId], ); const isOwnAgentNode = boundAgent !== null && selectedNodeId !== null && selectedNodeId === boundAgent.id; const canEditSelectedProfile = canManageProfile && selectedNode !== null && (isSuperAdmin || !isOwnAgentNode); const selectedChildAgents = useMemo(() => { if (selectedNode === null) { return []; } return flatNodes.filter((node) => node.parent_id === selectedNode.id); }, [flatNodes, selectedNode]); const childCountById = useMemo(() => { const counts = new Map(); for (const node of flatNodes) { if (node.parent_id === null) { continue; } counts.set(node.parent_id, (counts.get(node.parent_id) ?? 0) + 1); } return counts; }, [flatNodes]); const filteredRows = useMemo(() => { const normalized = keyword.trim().toLowerCase(); return businessRows.filter((node) => { if (normalized === "") { return true; } const parentName = node.parent_id !== null ? (parentNameMap.get(node.parent_id) ?? "") : ""; return [node.name, node.code, node.username ?? "", parentName] .join(" ") .toLowerCase() .includes(normalized); }); }, [businessRows, keyword, parentNameMap]); const loadTree = useCallback(async (siteId?: number | null) => { setLoading(true); setErr(null); try { const data = await getAgentTree(siteId ?? undefined); setTree(data.tree); setAdminSiteId(data.admin_site_id); } catch (e) { setErr(e instanceof LotteryApiBizError ? e.message : tRef.current("loadFailed")); setTree([]); } finally { setLoading(false); } }, [tRef]); useEffect(() => { if (!canViewAgents) { return; } if (adminSiteId === null) { if (profile?.agent?.admin_site_id) { setAdminSiteId(profile.agent.admin_site_id); return; } if (siteOptions.length > 0 && isSuperAdmin) { setAdminSiteId(siteOptions[0]?.id ?? null); } } }, [adminSiteId, canViewAgents, isSuperAdmin, profile?.agent?.admin_site_id, setAdminSiteId, siteOptions]); useAsyncEffect(() => { if (selectedNode === null) { setSelectedProfile(null); setSelectedProfileLoading(false); return; } setSelectedProfileLoading(true); void getAgentNodeProfile(selectedNode.id) .then((row) => { setSelectedProfile(row); if (!nodeDialogOpen) { applyProfileRowToForm(row); } }) .catch(() => setSelectedProfile(null)) .finally(() => setSelectedProfileLoading(false)); }, [selectedNode?.id, nodeDialogOpen]); useAsyncEffect(() => { if (rootNode === null) { setRootProfile(null); return; } void getAgentNodeProfile(rootNode.id) .then((p) => setRootProfile(p)) .catch(() => setRootProfile(null)); }, [rootNode?.id]); /** 仅上级/平台维护下级占成授信;代理查看自己时不展示配置 Tab */ const canShowProfileTab = canEditSelectedProfile; const canShowDownlineTab = useMemo( () => selectedNode !== null && !selectedProfileLoading && selectedProfile?.can_create_child_agent === true, [selectedNode, selectedProfile, selectedProfileLoading], ); const canShowPlayersTab = useMemo( () => selectedNode !== null && !selectedProfileLoading && selectedProfile?.can_create_player === true && hasUsersManagePermission, [hasUsersManagePermission, selectedNode, selectedProfile, selectedProfileLoading], ); const canCreateChildOnSelected = useMemo( () => canManageNode && selectedProfile?.can_create_child_agent === true, [canManageNode, selectedProfile?.can_create_child_agent], ); useEffect(() => { if (selectedProfileLoading || selectedNode === null) { return; } if (detailTab === "profile" && !canShowProfileTab) { setDetailTab("overview"); } else if (detailTab === "downline" && !canShowDownlineTab) { setDetailTab(canShowPlayersTab ? "players" : "overview"); } else if (detailTab === "players" && !canShowPlayersTab) { setDetailTab(canShowDownlineTab ? "downline" : "overview"); } }, [ canShowDownlineTab, canShowPlayersTab, canShowProfileTab, detailTab, selectedNode, selectedProfileLoading, ]); useAsyncEffect(() => { if (filteredRows.length === 0) { setSelectedNodeId(null); return; } if (selectedNodeId === null || !filteredRows.some((row) => row.id === selectedNodeId)) { setSelectedNodeId(filteredRows[0]?.id ?? null); } }, [filteredRows, selectedNodeId]); useEffect(() => { setDetailTab("overview"); }, [selectedNodeId]); useEffect(() => { if (isOwnAgentNode && detailTab === "profile") { setDetailTab("overview"); } }, [detailTab, isOwnAgentNode]); useAsyncEffect(() => { if (adminSiteId !== null) { void loadTree(adminSiteId); } }, [adminSiteId, loadTree]); const openCreateChildForNode = (node: AgentNodeRow) => { setNodeDialogMode("create"); setTargetParentId(node.id); setEditingNodeId(null); setNodeName(""); setNodeStatus(1); setNodeUsername(""); setNodePassword(""); resetProfileForm("create"); setProfileLoading(false); setProfileLoaded(true); setEditingNodeNeedsPrimaryAccount(false); setNodeDialogOpen(true); if (canManageProfile) { void getAgentNodeProfile(node.id) .then((p) => setProfileParentCaps(p.parent_caps ?? null)) .catch(() => setProfileParentCaps(null)); } }; const openEditForNode = (node: AgentNodeRow) => { setNodeDialogMode("edit"); setTargetParentId(node.parent_id); setEditingNodeId(node.id); setNodeName(node.name); setNodeStatus(node.status); setNodeUsername(node.username ?? ""); setEditingNodeNeedsPrimaryAccount((node.username ?? "").trim() === ""); setNodePassword(""); resetProfileForm("edit"); setNodeDialogOpen(true); if (!canManageProfile) { setProfileLoading(false); setProfileLoaded(true); return; } setProfileLoading(true); setProfileLoaded(false); void getAgentNodeProfile(node.id) .then((p) => { applyProfileRowToForm(p); setProfileLoaded(true); }) .catch(() => { toast.error( t("profile.loadFailed", { defaultValue: "加载占成与授信失败,请关闭后重试" }), ); setProfileLoaded(false); }) .finally(() => { setProfileLoading(false); }); }; const canDeleteNode = (node: AgentNodeRow): boolean => { const blockedByChildren = (node.children?.length ?? 0) > 0; const blockedBySelf = profile?.agent?.id === node.id; return canManageNode && !blockedByChildren && !blockedBySelf; }; const handleDeleteNode = (node: AgentNodeRow): void => { if (!canDeleteNode(node)) { return; } requestConfirm({ title: t("deleteNode", { defaultValue: "删除代理" }), description: t("deleteNodeConfirm", { defaultValue: "删除后将同时移除该代理的唯一登录账号,且不可恢复。", }), confirmLabel: t("deleteNode", { defaultValue: "删除代理" }), confirmVariant: "destructive", onConfirm: async () => { await deleteAgentNode(node.id); toast.success(t("deleteSuccess", { name: node.name })); if (selectedNodeId === node.id) { setSelectedNodeId(null); } await loadTree(adminSiteId); }, }); }; const saveInlineProfile = async (): Promise => { if (selectedNode === null || !canEditSelectedProfile) { return; } const validationError = validateProfileFields(); if (validationError) { toast.error(validationError); return; } setProfileSaving(true); try { const updated = await putAgentNodeProfile(selectedNode.id, profilePayload()); setSelectedProfile(updated); applyProfileRowToForm(updated); toast.success(t("profile.saveSuccess", { defaultValue: "占成与授信已保存" })); await loadTree(adminSiteId); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed")); } finally { setProfileSaving(false); } }; const inlineProfileFields = useMemo(() => { if (!canShowProfileTab) { return null; } return { disabled: !canEditSelectedProfile, loading: selectedProfileLoading, parentCaps: profileParentCaps, availableCredit: profileAvailableCredit, canCreateChildAgent, isSuperAdmin, shareRate: profileShareRate, onShareRateChange: setProfileShareRate, creditLimit: profileCreditLimit, onCreditLimitChange: setProfileCreditLimit, rebateLimit: profileRebateLimit, onRebateLimitChange: setProfileRebateLimit, defaultRebate: profileDefaultRebate, onDefaultRebateChange: setProfileDefaultRebate, settlementCycle: profileSettlementCycle, onSettlementCycleChange: setProfileSettlementCycle, extraRebate: profileExtraRebate, onExtraRebateChange: setProfileExtraRebate, canCreatePlayer: profileCanCreatePlayer, onCanCreatePlayerChange: setProfileCanCreatePlayer, canCreateChild: profileCanCreateChild, onCanCreateChildChange: setProfileCanCreateChild, riskTags: profileRiskTags, onRiskTagsChange: setProfileRiskTags, }; }, [ canCreateChildAgent, canEditSelectedProfile, canShowProfileTab, isSuperAdmin, profileAvailableCredit, profileCanCreateChild, profileCanCreatePlayer, profileCreditLimit, profileDefaultRebate, profileExtraRebate, profileParentCaps, profileRebateLimit, profileRiskTags, profileSettlementCycle, profileShareRate, selectedProfileLoading, ]); const showAgentSidebar = businessRows.length > 1; const openAddAgent = (): void => { const parent = selectedNode ?? rootNode; if (parent !== null) { openCreateChildForNode(parent); } }; const saveNode = async () => { if (!nodeName.trim()) { toast.error(t("nameRequired", { defaultValue: "请填写代理名称" })); return; } if (!nodeUsername.trim()) { toast.error(t("usernameRequired", { defaultValue: "请填写登录名" })); return; } if (nodeDialogMode === "create") { if (targetParentId === null) { return; } if (!nodePassword.trim()) { 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; } const includeProfileInDialog = canManageProfile && (nodeDialogMode === "create" || (editingNodeId !== null && boundAgent?.id !== editingNodeId)); if (includeProfileInDialog) { if (nodeDialogMode === "edit" && !profileLoaded) { toast.error( t("profile.loadingBlocked", { defaultValue: "占成与授信尚未加载完成,请稍候再保存", }), ); return; } const profileError = validateProfileFields(); if (profileError !== null) { toast.error(profileError); return; } } setNodeSaving(true); try { if (nodeDialogMode === "create" && targetParentId !== null) { await postAgentNode({ parent_id: targetParentId, name: nodeName.trim(), username: nodeUsername.trim(), password: nodePassword, status: nodeStatus, ...(canManageProfile ? profilePayload() : {}), }); toast.success(t("createSuccess", { name: nodeName.trim() })); } else if (nodeDialogMode === "edit" && editingNodeId !== null) { await putAgentNode(editingNodeId, { name: nodeName.trim(), username: nodeUsername.trim(), password: editingNodeNeedsPrimaryAccount ? nodePassword.trim() : nodePassword.trim() || undefined, status: nodeStatus, }); if (includeProfileInDialog) { await putAgentNodeProfile(editingNodeId, profilePayload()); } toast.success(t("updateSuccess", { name: nodeName.trim() })); } setNodeDialogOpen(false); await loadTree(adminSiteId); if (nodeDialogMode === "create" && targetParentId !== null) { const refreshed = flattenTree((await getAgentTree(adminSiteId ?? undefined)).tree); const created = refreshed.find( (node) => node.parent_id === targetParentId && node.name === nodeName.trim(), ); if (created) { setSelectedNodeId(created.id); } } else if (nodeDialogMode === "edit" && editingNodeId !== null) { setSelectedNodeId(editingNodeId); if (canManageProfile) { void getAgentNodeProfile(editingNodeId) .then((p) => { if (selectedNodeId === editingNodeId || selectedNodeId === null) { setSelectedProfile(p); } }) .catch(() => { /* 树已刷新,占成区可能短暂不可用 */ }); } } } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed")); } finally { setNodeSaving(false); } }; const addParent = selectedNode ?? rootNode; const parentProfileForAdd = useMemo(() => { if (addParent === null) { return null; } if (addParent.id === selectedNodeId && selectedProfile !== null) { return selectedProfile; } if (rootNode !== null && addParent.id === rootNode.id) { return rootProfile; } return null; }, [addParent, rootNode, rootProfile, selectedNodeId, selectedProfile]); if (!canViewAgents) { return (

{t("noAccess", { defaultValue: "您没有代理经营相关权限,请联系管理员开通。" })}

); } if (loading && tree.length === 0) { return ; } return (
{err ?

{err}

: null}
{showAgentSidebar ? ( { setKeyword(value); }} onSelect={(node) => { setSelectedNodeId(node.id); }} /> ) : null} openEditForNode(node)} onAddChild={() => selectedNode && openCreateChildForNode(selectedNode)} onEditCurrent={() => selectedNode && openEditForNode(selectedNode)} onDeleteChild={(node) => handleDeleteNode(node)} onSelectChild={(child) => { setSelectedNodeId(child.id); }} profileFields={inlineProfileFields} profileSaving={profileSaving} onSaveProfile={() => void saveInlineProfile()} />
{nodeDialogMode === "create" ? t("createChild", { defaultValue: "添加下级代理" }) : t("editNode", { defaultValue: "编辑代理" })}
setNodeName(e.target.value)} autoComplete="off" />
setNodeUsername(e.target.value)} autoComplete="off" />
{nodeDialogMode === "edit" && editingNodeNeedsPrimaryAccount ? (

{t("bindAccountHint", { defaultValue: "该代理尚无登录账号,保存时将自动创建并绑定。", })}

) : null} setNodePassword(e.target.value)} placeholder={ nodeDialogMode === "edit" && !editingNodeNeedsPrimaryAccount ? t("passwordOptionalHint") : t("passwordPlaceholder") } autoComplete="new-password" />
setNodeStatus(value ? 1 : 0)} />
{canManageProfile && (nodeDialogMode === "create" || (editingNodeId !== null && boundAgent?.id !== editingNodeId)) ? (

{t("profile.section", { defaultValue: "占成与授信" })}

) : null}
); }