import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { AnimatePresence, motion } from "motion/react"; import { AlertTriangle, ArrowLeft, ArrowRightLeft, BriefcaseBusiness, Building2, CheckCircle2, Search, ShieldAlert, Users, X, } from "lucide-react"; import { useNavigate } from "react-router-dom"; import ActionDialog from "@/components/ActionDialog"; import { useIsMobileViewport } from "@/hooks/useIsMobileViewport"; import { executeOwnerTransfer, getCurrentUser, getStoredCurrentUserId, listOwnerTransferTargetUsers, previewOwnerTransfer, type AdminUserSummary, type OwnerTransferItem, type OwnerTransferPayload, type OwnerTransferPreview, } from "@/lib/auth"; import { cn } from "@/lib/utils"; type TransferTab = "opportunities" | "sales" | "channels"; type TransferKind = "opportunity" | "sales" | "channel"; type SelectionState = { opportunityIds: number[]; salesExpansionIds: number[]; channelExpansionIds: number[]; }; type TransferConfirmState = { title: string; description: string; payload: OwnerTransferPayload; successText: string; confirmText: string; sourceLabel: string; targetLabel: string; subjectLabel?: string; badges: string[]; }; const TAB_META: Array<{ key: TransferTab; label: string; icon: typeof BriefcaseBusiness; }> = [ { key: "opportunities", label: "商机", icon: BriefcaseBusiness }, { key: "sales", label: "拓展销售人员", icon: Users }, { key: "channels", label: "拓展渠道", icon: Building2 }, ]; function getActiveTenantId() { const rawValue = localStorage.getItem("activeTenantId"); const tenantId = Number(rawValue || 0); return Number.isFinite(tenantId) ? tenantId : 0; } function getDisplayUserName(user?: AdminUserSummary | null) { if (!user) { return "未选择"; } return `${user.displayName || user.username} (${user.username})`; } function filterTransferItems(items: OwnerTransferItem[], keyword: string) { const normalizedKeyword = keyword.trim().toLowerCase(); if (!normalizedKeyword) { return items; } return items.filter((item) => `${item.name || ""} ${item.code || ""}`.toLowerCase().includes(normalizedKeyword)); } function countLabel(value?: number) { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function createEmptySelectionState(): SelectionState { return { opportunityIds: [], salesExpansionIds: [], channelExpansionIds: [], }; } function getSelectionKey(kind: TransferKind) { if (kind === "sales") { return "salesExpansionIds"; } if (kind === "channel") { return "channelExpansionIds"; } return "opportunityIds"; } function getTransferTabKind(tab: TransferTab): TransferKind { if (tab === "sales") { return "sales"; } if (tab === "channels") { return "channel"; } return "opportunity"; } export default function OwnerTransfer({ embedded = false, onClose, onFooterChange, }: { embedded?: boolean; onClose?: () => void; onFooterChange?: (footer: ReactNode | null) => void; }) { const navigate = useNavigate(); const isMobileViewport = useIsMobileViewport(); const [users, setUsers] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [loadingUsers, setLoadingUsers] = useState(false); const [previewLoading, setPreviewLoading] = useState(false); const [executing, setExecuting] = useState(false); const [pageError, setPageError] = useState(""); const [successMessage, setSuccessMessage] = useState(""); const [preview, setPreview] = useState(null); const [fromUserId, setFromUserId] = useState(() => { const storedUserId = getStoredCurrentUserId(); return storedUserId ? String(storedUserId) : ""; }); const [toUserId, setToUserId] = useState(""); const [targetPickerOpen, setTargetPickerOpen] = useState(false); const [targetPickerKeyword, setTargetPickerKeyword] = useState(""); const [activeTab, setActiveTab] = useState("opportunities"); const [searchKeyword, setSearchKeyword] = useState(""); const [selection, setSelection] = useState(createEmptySelectionState); const [detailsExpanded, setDetailsExpanded] = useState(false); const [confirmState, setConfirmState] = useState(null); const activeTenantId = getActiveTenantId(); const tenantSelected = activeTenantId > 0; const numericFromUserId = fromUserId ? Number(fromUserId) : undefined; const numericToUserId = toUserId ? Number(toUserId) : undefined; const fromUser = useMemo( () => users.find((item) => item.userId === numericFromUserId) ?? currentUser, [currentUser, numericFromUserId, users], ); const toUser = useMemo( () => users.find((item) => item.userId === numericToUserId) ?? null, [numericToUserId, users], ); const selectedPreviewHasSalesConflict = Boolean(preview?.salesConflicts?.length); const loadCurrentUser = useCallback(async () => { try { const profile = await getCurrentUser(); const nextCurrentUser: AdminUserSummary = { userId: profile.userId, username: profile.username, displayName: profile.displayName || profile.username, status: profile.status, tenantId: profile.tenantId, }; setCurrentUser(nextCurrentUser); if (profile.userId) { setFromUserId(String(profile.userId)); } } catch (error) { setCurrentUser(null); setPageError(error instanceof Error ? error.message : "获取当前登录账号失败"); } }, []); const loadUsers = useCallback(async () => { if (!tenantSelected) { setUsers([]); return; } setLoadingUsers(true); setPageError(""); try { const response = await listOwnerTransferTargetUsers(); setUsers(response ?? []); } catch (error) { setUsers([]); setPageError(error instanceof Error ? error.message : "加载用户列表失败"); } finally { setLoadingUsers(false); } }, [activeTenantId, tenantSelected]); const loadPreview = useCallback(async (sourceUserId?: number, targetUserId?: number) => { if (!currentUser?.userId) { setPreview(null); return; } if ( !tenantSelected || !sourceUserId || !targetUserId || sourceUserId <= 0 || targetUserId <= 0 || sourceUserId === targetUserId || sourceUserId !== currentUser.userId ) { setPreview(null); return; } setPreviewLoading(true); setPageError(""); try { const response = await previewOwnerTransfer(sourceUserId, targetUserId); setPreview(response); } catch (error) { setPreview(null); setPageError(error instanceof Error ? error.message : "加载转移预检失败"); } finally { setPreviewLoading(false); } }, [currentUser?.userId, tenantSelected]); useEffect(() => { void loadCurrentUser(); }, [loadCurrentUser]); useEffect(() => { void loadUsers(); }, [loadUsers]); useEffect(() => { void loadPreview(numericFromUserId, numericToUserId); }, [loadPreview, numericFromUserId, numericToUserId]); useEffect(() => { setSearchKeyword(""); }, [activeTab, preview?.fromUserId, preview?.toUserId]); useEffect(() => { setSelection(createEmptySelectionState()); }, [preview?.fromUserId, preview?.toUserId]); useEffect(() => { if (!isMobileViewport) { setDetailsExpanded(true); return; } setDetailsExpanded(false); }, [isMobileViewport, preview?.fromUserId, preview?.toUserId]); const selectableUsers = useMemo( () => users.filter((user) => String(user.userId) !== fromUserId && user.status !== 0), [fromUserId, users], ); const filteredSelectableUsers = useMemo(() => { const normalizedKeyword = targetPickerKeyword.trim().toLowerCase(); if (!normalizedKeyword) { return selectableUsers; } return selectableUsers.filter((user) => { const haystack = `${user.displayName || ""} ${user.username || ""}`.toLowerCase(); return haystack.includes(normalizedKeyword); }); }, [selectableUsers, targetPickerKeyword]); const filteredItems = useMemo(() => { if (!preview) { return []; } if (activeTab === "sales") { return filterTransferItems(preview.salesExpansions || [], searchKeyword); } if (activeTab === "channels") { return filterTransferItems(preview.channelExpansions || [], searchKeyword); } return filterTransferItems(preview.opportunities || [], searchKeyword); }, [activeTab, preview, searchKeyword]); const selectedIdsForActiveTab = useMemo(() => { if (activeTab === "sales") { return selection.salesExpansionIds; } if (activeTab === "channels") { return selection.channelExpansionIds; } return selection.opportunityIds; }, [activeTab, selection.channelExpansionIds, selection.opportunityIds, selection.salesExpansionIds]); const selectableFilteredItems = useMemo(() => { if (activeTab === "sales") { return filteredItems.filter((item) => !item.conflict); } return filteredItems; }, [activeTab, filteredItems]); const totalSelectedCount = selection.opportunityIds.length + selection.salesExpansionIds.length + selection.channelExpansionIds.length; const activeTabSelectedCount = selectedIdsForActiveTab.length; const selectedOpportunityCount = selection.opportunityIds.length; const selectedSalesCount = selection.salesExpansionIds.length; const selectedChannelCount = selection.channelExpansionIds.length; const selectableFilteredIds = selectableFilteredItems.map((item) => item.id); const areAllFilteredItemsSelected = selectableFilteredIds.length > 0 && selectableFilteredIds.every((id) => selectedIdsForActiveTab.includes(id)); const activePreviewCount = activeTab === "sales" ? countLabel(preview?.salesExpansionCount) : activeTab === "channels" ? countLabel(preview?.channelExpansionCount) : countLabel(preview?.opportunityCount); const primaryActionLabel = executing ? "执行中..." : totalSelectedCount > 0 ? `转移已勾选 ${totalSelectedCount} 条` : "请选择转移数据"; const bottomActionSummary = totalSelectedCount > 0 ? `已勾选 ${totalSelectedCount} 条待转移数据` : "请先勾选需要转移的数据"; const runTransfer = useCallback(async (payload: OwnerTransferPayload, successText: string) => { setExecuting(true); setPageError(""); setSuccessMessage(""); try { const result = await executeOwnerTransfer(payload); setSuccessMessage( `${successText} 本次共转移:商机 ${countLabel(result.transferredOpportunityCount)} 条、拓展销售人员 ${countLabel(result.transferredSalesExpansionCount)} 条、拓展渠道 ${countLabel(result.transferredChannelExpansionCount)} 条。`, ); setSelection(createEmptySelectionState()); await loadUsers(); await loadPreview(payload.fromUserId, payload.toUserId); } catch (error) { setPageError(error instanceof Error ? error.message : "执行归属人转移失败"); } finally { setExecuting(false); } }, [loadPreview, loadUsers]); const confirmTransfer = useCallback(async () => { if (!confirmState) { return; } await runTransfer(confirmState.payload, confirmState.successText); setConfirmState(null); }, [confirmState, runTransfer]); const handleExecute = async () => { if (!numericFromUserId || !numericToUserId) { setPageError("请先选择原归属人和新归属人"); return; } if (numericFromUserId === numericToUserId) { setPageError("原归属人和新归属人不能相同"); return; } if (totalSelectedCount <= 0) { setPageError("请先勾选需要转移的数据"); return; } setConfirmState({ title: "确认执行转移?", description: "", payload: { fromUserId: numericFromUserId, toUserId: numericToUserId, transferOpportunities: selection.opportunityIds.length > 0, transferSalesExpansions: selection.salesExpansionIds.length > 0, transferChannelExpansions: selection.channelExpansionIds.length > 0, selection: { opportunityIds: selection.opportunityIds, salesExpansionIds: selection.salesExpansionIds, channelExpansionIds: selection.channelExpansionIds, }, }, successText: "已勾选数据转移完成。", confirmText: "确认转移", sourceLabel: getDisplayUserName(fromUser), targetLabel: getDisplayUserName(toUser), badges: [ selectedOpportunityCount > 0 ? `商机 ${selectedOpportunityCount} 条` : "", selectedSalesCount > 0 ? `拓展销售 ${selectedSalesCount} 条` : "", selectedChannelCount > 0 ? `拓展渠道 ${selectedChannelCount} 条` : "", ].filter(Boolean), }); }; const handleSingleTransfer = async (kind: TransferKind, item: OwnerTransferItem) => { if (!numericFromUserId || !numericToUserId) { setPageError("请先选择原归属人和新归属人"); return; } if (kind === "sales" && item.conflict) { setPageError("目标归属人下已存在相同工号的拓展销售人员,请先处理冲突后再试"); return; } const itemTypeLabel = kind === "sales" ? "拓展销售人员" : kind === "channel" ? "拓展渠道" : "商机"; setConfirmState({ title: "确认转移当前数据?", description: "", payload: { fromUserId: numericFromUserId, toUserId: numericToUserId, transferOpportunities: kind === "opportunity", transferSalesExpansions: kind === "sales", transferChannelExpansions: kind === "channel", selection: { opportunityIds: kind === "opportunity" ? [item.id] : [], salesExpansionIds: kind === "sales" ? [item.id] : [], channelExpansionIds: kind === "channel" ? [item.id] : [], }, }, successText: "单条归属人转移已完成。", confirmText: "确认单条转移", sourceLabel: getDisplayUserName(fromUser), targetLabel: getDisplayUserName(toUser), subjectLabel: `${itemTypeLabel} · ${item.name || "未命名对象"}`, badges: [itemTypeLabel], }); }; const toggleItemSelection = (kind: TransferKind, itemId: number) => { const selectionKey = getSelectionKey(kind); setSelection((current) => { const currentIds = current[selectionKey]; const nextIds = currentIds.includes(itemId) ? currentIds.filter((id) => id !== itemId) : [...currentIds, itemId]; return { ...current, [selectionKey]: nextIds, }; }); }; const handleToggleSelectAllFiltered = () => { const kind = getTransferTabKind(activeTab); const selectionKey = getSelectionKey(kind); setSelection((current) => { const currentIds = current[selectionKey]; const nextIds = areAllFilteredItemsSelected ? currentIds.filter((id) => !selectableFilteredIds.includes(id)) : Array.from(new Set([...currentIds, ...selectableFilteredIds])); return { ...current, [selectionKey]: nextIds, }; }); }; const handleClearActiveSelection = () => { const kind = getTransferTabKind(activeTab); const selectionKey = getSelectionKey(kind); setSelection((current) => ({ ...current, [selectionKey]: [], })); }; const embeddedFooter = useMemo(() => { if (!embedded) { return null; } return (

{toUser ? `转移给 ${toUser.displayName || toUser.username}` : "请选择新归属人"}

{bottomActionSummary}

); }, [ bottomActionSummary, embedded, executing, handleExecute, onClose, previewLoading, primaryActionLabel, tenantSelected, toUser, totalSelectedCount, ]); useEffect(() => { if (!embedded || !onFooterChange) { return; } onFooterChange(embeddedFooter); return () => onFooterChange(null); }, [embedded, embeddedFooter, onFooterChange]); return (
{!embedded ? (

项目归属转移

) : null} {!tenantSelected ? (
请先切换到具体租户后再操作归属人转移。
) : null} {pageError ? (
{pageError}
) : null} {successMessage ? (
{successMessage}
) : null}

转移设置

{selectedPreviewHasSalesConflict ? (
存在拓展销售人员工号冲突,请先处理冲突,或仅勾选下方可转移的数据后再执行。
) : null}

转移预检

{preview ? ( <> ) : null} {previewLoading ?

加载中...

: null}

从 {preview?.fromUserName || getDisplayUserName(fromUser)} 转移到 {preview?.toUserName || getDisplayUserName(toUser)} 的预检结果。

{!preview && !previewLoading ? (
选择原归属人和新归属人后,系统会显示当前可转移数量及冲突检查结果。
) : null} {preview ? ( <> {isMobileViewport ? (
{TAB_META.map((tab) => { const count = tab.key === "sales" ? countLabel(preview.salesExpansionCount) : tab.key === "channels" ? countLabel(preview.channelExpansionCount) : countLabel(preview.opportunityCount); return ( ); })}
) : (
{TAB_META.map((tab) => { const count = tab.key === "sales" ? countLabel(preview.salesExpansionCount) : tab.key === "channels" ? countLabel(preview.channelExpansionCount) : countLabel(preview.opportunityCount); return ( ); })}
)} {!isMobileViewport || detailsExpanded || isMobileViewport ? ( <>
setSearchKeyword(event.target.value)} placeholder="搜索名称或工号" className={cn( "crm-input-text w-full border border-slate-200 bg-white pl-10 outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50", isMobileViewport ? "crm-input-box" : "h-10 rounded-xl px-3 text-sm", )} />
{!isMobileViewport ?
: null}
{activeTab === "sales" && preview.salesConflicts.length > 0 ? (

拓展销售人员工号冲突

目标归属人名下已存在相同工号时,系统会阻止执行“拓展销售人员”转移。

{preview.salesConflicts.map((conflict) => (
工号 {conflict.employeeNo || "-"} · 人员姓名 {conflict.candidateName || "-"}
))}
) : null}
当前页签已选 {activeTabSelectedCount} 条,全部已选 {totalSelectedCount} 条。
{filteredItems.length > 0 ? filteredItems.map((item) => (
toggleItemSelection(getTransferTabKind(activeTab), item.id)} className="h-4 w-4 accent-violet-600 disabled:cursor-not-allowed" />

{item.name || "未命名对象"}

{activeTab === "sales" ? ( 工号 {item.code || "-"} ) : null} {item.conflict ? ( 有冲突 ) : ( 可转移 )}
)) : (
暂无数据
)}
) : null} ) : null}
{targetPickerOpen ? ( <>
setTargetPickerOpen(false)} />

选择新归属人

搜索并选择接收归属数据的账号

setTargetPickerKeyword(event.target.value)} placeholder="搜索姓名或账号" className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 pl-10 pr-24 text-sm text-slate-700 outline-none transition-colors focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-700 dark:bg-slate-800/70 dark:text-slate-100" /> {targetPickerKeyword ? ( ) : null}
{filteredSelectableUsers.length > 0 ? filteredSelectableUsers.map((user) => { const selected = String(user.userId) === toUserId; return ( ); }) : (
未找到匹配的归属人
)}
) : null} {confirmState ? ( } confirmText={confirmState.confirmText} loading={executing} onClose={() => setConfirmState(null)} onConfirm={confirmTransfer} >
{confirmState.subjectLabel ? (

转移对象

{confirmState.subjectLabel}

) : null}

原归属人

{confirmState.sourceLabel}

新归属人

{confirmState.targetLabel}

{confirmState.badges.length > 0 ? (
{confirmState.badges.map((badge) => ( {badge} ))}
) : null}
确认后将立即生效,请确保选择无误。
) : null}
{!embedded && !isMobileViewport ? (

{toUser ? `转移给 ${toUser.displayName || toUser.username}` : "请选择新归属人"}

{bottomActionSummary}

) : null} {!embedded && isMobileViewport ? ); }