import { useEffect, useState, type ReactNode } from "react"; import { Search, Plus, MapPin, Building2, User, Phone, X, Clock, FileText, Calendar, ChevronRight } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import { useLocation } from "react-router-dom"; import { createChannelExpansion, createSalesExpansion, getExpansionMeta, getExpansionOverview, updateChannelExpansion, updateSalesExpansion, type ChannelExpansionContact, type ChannelExpansionItem, type CreateChannelExpansionPayload, type CreateSalesExpansionPayload, type ExpansionDictOption, type ExpansionFollowUp, type SalesExpansionItem, } from "@/lib/auth"; import { AdaptiveSelect } from "@/components/AdaptiveSelect"; type ExpansionItem = SalesExpansionItem | ChannelExpansionItem; type ExpansionTab = "sales" | "channel"; const detailBadgeClass = "crm-pill crm-pill-violet inline-flex items-center gap-0.5 border border-violet-100 transition-all hover:border-violet-200 hover:bg-violet-100 dark:border-violet-500/20 dark:hover:border-violet-500/30 dark:hover:bg-violet-500/15"; function createEmptyChannelContact(): ChannelExpansionContact { return { name: "", mobile: "", title: "", }; } const defaultSalesForm: CreateSalesExpansionPayload = { employeeNo: "", candidateName: "", officeName: "", mobile: "", industry: "", title: "", intentLevel: "medium", hasDesktopExp: false, employmentStatus: "active", }; const defaultChannelForm: CreateChannelExpansionPayload = { channelCode: "", channelName: "", province: "", officeAddress: "", channelIndustry: "", contactEstablishedDate: "", intentLevel: "medium", hasDesktopExp: false, channelAttribute: "", internalAttribute: "", stage: "initial_contact", remark: "", contacts: [createEmptyChannelContact()], }; function ModalShell({ title, subtitle, onClose, children, footer, }: { title: string; subtitle: string; onClose: () => void; children: ReactNode; footer: ReactNode; }) { return ( <>

{title}

{subtitle}

{children}
{footer}
); } function DetailItem({ label, value, icon, className = "", }: { label: string; value: ReactNode; icon?: ReactNode; className?: string; }) { return (

{icon} {label}

{value}
); } export default function Expansion() { const location = useLocation(); const [activeTab, setActiveTab] = useState("sales"); const [selectedItem, setSelectedItem] = useState(null); const [keyword, setKeyword] = useState(""); const [salesData, setSalesData] = useState([]); const [channelData, setChannelData] = useState([]); const [officeOptions, setOfficeOptions] = useState([]); const [industryOptions, setIndustryOptions] = useState([]); const [channelAttributeOptions, setChannelAttributeOptions] = useState([]); const [internalAttributeOptions, setInternalAttributeOptions] = useState([]); const [nextChannelCode, setNextChannelCode] = useState(""); const [refreshTick, setRefreshTick] = useState(0); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [createError, setCreateError] = useState(""); const [editError, setEditError] = useState(""); const [salesDetailTab, setSalesDetailTab] = useState<"projects" | "followups">("projects"); const [channelDetailTab, setChannelDetailTab] = useState<"projects" | "contacts" | "followups">("projects"); const [salesForm, setSalesForm] = useState(defaultSalesForm); const [channelForm, setChannelForm] = useState(defaultChannelForm); const [editSalesForm, setEditSalesForm] = useState(defaultSalesForm); const [editChannelForm, setEditChannelForm] = useState(defaultChannelForm); const hasForegroundModal = createOpen || editOpen; useEffect(() => { const requestedTab = (location.state as { tab?: ExpansionTab } | null)?.tab; if (requestedTab === "sales" || requestedTab === "channel") { setActiveTab(requestedTab); } }, [location.state]); useEffect(() => { let cancelled = false; async function loadMeta() { try { const data = await getExpansionMeta(); if (!cancelled) { setOfficeOptions(data.officeOptions ?? []); setIndustryOptions(data.industryOptions ?? []); setChannelAttributeOptions(data.channelAttributeOptions ?? []); setInternalAttributeOptions(data.internalAttributeOptions ?? []); setNextChannelCode(data.nextChannelCode ?? ""); } } catch { if (!cancelled) { setOfficeOptions([]); setIndustryOptions([]); setChannelAttributeOptions([]); setInternalAttributeOptions([]); setNextChannelCode(""); } } } void loadMeta(); return () => { cancelled = true; }; }, []); useEffect(() => { let cancelled = false; async function loadExpansionData() { try { const data = await getExpansionOverview(keyword); if (cancelled) { return; } setSalesData(data.salesItems ?? []); setChannelData(data.channelItems ?? []); setSelectedItem(null); } catch { if (!cancelled) { setSalesData([]); setChannelData([]); setSelectedItem(null); } } } void loadExpansionData(); return () => { cancelled = true; }; }, [keyword, refreshTick]); const followUpRecords: ExpansionFollowUp[] = selectedItem?.followUps ?? []; useEffect(() => { if (selectedItem?.type === "sales") { setSalesDetailTab("projects"); } else if (selectedItem?.type === "channel") { setChannelDetailTab("projects"); } }, [selectedItem]); const handleSalesChange = (key: K, value: CreateSalesExpansionPayload[K]) => { setSalesForm((current) => ({ ...current, [key]: value })); }; const handleChannelChange = (key: K, value: CreateChannelExpansionPayload[K]) => { setChannelForm((current) => ({ ...current, [key]: value })); }; const handleEditSalesChange = (key: K, value: CreateSalesExpansionPayload[K]) => { setEditSalesForm((current) => ({ ...current, [key]: value })); }; const handleEditChannelChange = (key: K, value: CreateChannelExpansionPayload[K]) => { setEditChannelForm((current) => ({ ...current, [key]: value })); }; const handleChannelContactChange = (index: number, key: keyof ChannelExpansionContact, value: string, isEdit = false) => { const setter = isEdit ? setEditChannelForm : setChannelForm; setter((current) => { const nextContacts = [...(current.contacts ?? [])]; const target = { ...(nextContacts[index] ?? createEmptyChannelContact()), [key]: value }; nextContacts[index] = target; return { ...current, contacts: nextContacts }; }); }; const addChannelContact = (isEdit = false) => { const setter = isEdit ? setEditChannelForm : setChannelForm; setter((current) => ({ ...current, contacts: [...(current.contacts ?? []), createEmptyChannelContact()], })); }; const removeChannelContact = (index: number, isEdit = false) => { const setter = isEdit ? setEditChannelForm : setChannelForm; setter((current) => { const currentContacts = current.contacts ?? []; const nextContacts = currentContacts.filter((_, contactIndex) => contactIndex !== index); return { ...current, contacts: nextContacts.length > 0 ? nextContacts : [createEmptyChannelContact()], }; }); }; const resetCreateState = () => { setCreateOpen(false); setCreateError(""); setSalesForm(defaultSalesForm); setChannelForm(defaultChannelForm); }; const resetEditState = () => { setEditOpen(false); setEditError(""); setEditSalesForm(defaultSalesForm); setEditChannelForm(defaultChannelForm); }; const handleOpenCreate = () => { setCreateError(""); setCreateOpen(true); }; const handleOpenEdit = () => { if (!selectedItem) { return; } setEditError(""); if (selectedItem.type === "sales") { setEditSalesForm({ employeeNo: selectedItem.employeeNo === "无" ? "" : selectedItem.employeeNo ?? "", candidateName: selectedItem.name ?? "", officeName: selectedItem.officeCode ?? "", mobile: selectedItem.phone === "无" ? "" : selectedItem.phone ?? "", targetDept: selectedItem.dept === "无" ? "" : selectedItem.dept ?? selectedItem.targetDept ?? "", industry: selectedItem.industryCode ?? "", title: selectedItem.title === "无" ? "" : selectedItem.title ?? "", intentLevel: selectedItem.intentLevel ?? "medium", hasDesktopExp: Boolean(selectedItem.hasExp), employmentStatus: selectedItem.active ? "active" : "left", }); } else { setEditChannelForm({ channelCode: selectedItem.channelCode ?? "", channelName: selectedItem.name ?? "", province: selectedItem.province === "无" ? "" : selectedItem.province ?? "", officeAddress: selectedItem.officeAddress === "无" ? "" : selectedItem.officeAddress ?? "", channelIndustry: selectedItem.channelIndustry === "无" ? "" : selectedItem.channelIndustry ?? "", annualRevenue: selectedItem.annualRevenue ? Number(selectedItem.annualRevenue) : undefined, staffSize: selectedItem.size ?? undefined, contactEstablishedDate: selectedItem.establishedDate === "无" ? "" : selectedItem.establishedDate ?? "", intentLevel: selectedItem.intentLevel ?? "medium", hasDesktopExp: Boolean(selectedItem.hasDesktopExp), channelAttribute: selectedItem.channelAttribute === "无" ? "" : selectedItem.channelAttribute ?? "", internalAttribute: selectedItem.internalAttribute === "无" ? "" : selectedItem.internalAttribute ?? "", stage: selectedItem.stageCode ?? "initial_contact", remark: selectedItem.notes === "无" ? "" : selectedItem.notes ?? "", contacts: (selectedItem.contacts?.length ?? 0) > 0 ? selectedItem.contacts?.map((contact) => ({ name: contact.name === "无" ? "" : contact.name ?? "", mobile: contact.mobile === "无" ? "" : contact.mobile ?? "", title: contact.title === "无" ? "" : contact.title ?? "", })) : [createEmptyChannelContact()], }); } setEditOpen(true); }; const handleCreateSubmit = async () => { if (submitting) { return; } setSubmitting(true); setCreateError(""); try { if (activeTab === "sales") { await createSalesExpansion({ ...salesForm, targetDept: salesForm.targetDept?.trim() || undefined, }); } else { await createChannelExpansion({ ...channelForm, annualRevenue: channelForm.annualRevenue || undefined, staffSize: channelForm.staffSize || undefined, }); } resetCreateState(); setRefreshTick((current) => current + 1); } catch (error) { setCreateError(error instanceof Error ? error.message : "新增失败"); } finally { setSubmitting(false); } }; const handleEditSubmit = async () => { if (!selectedItem || submitting) { return; } setSubmitting(true); setEditError(""); try { if (selectedItem.type === "sales") { await updateSalesExpansion(selectedItem.id, { ...editSalesForm, targetDept: editSalesForm.targetDept?.trim() || undefined, }); } else { await updateChannelExpansion(selectedItem.id, { ...editChannelForm, annualRevenue: editChannelForm.annualRevenue || undefined, staffSize: editChannelForm.staffSize || undefined, }); } resetEditState(); setSelectedItem(null); setRefreshTick((current) => current + 1); } catch (error) { setEditError(error instanceof Error ? error.message : "编辑失败"); } finally { setSubmitting(false); } }; const renderEmpty = () => (
暂无
); const renderFollowUpTimeline = () => { if (followUpRecords.length <= 0) { return (
暂无跟进记录
); } return (
{followUpRecords.map((record) => { const summary = getExpansionFollowUpSummary(record); return (

拜访时间

{summary.visitStartTime}

沟通内容

{summary.evaluationContent}

后续规划

{summary.nextPlan}

跟进人: {record.user || "无"}{record.date || "无"}

)})}
); }; const handleTabChange = (tab: ExpansionTab) => { setActiveTab(tab); setSelectedItem(null); }; const renderSalesForm = ( form: CreateSalesExpansionPayload, onChange: (key: K, value: CreateSalesExpansionPayload[K]) => void, ) => (
); const renderChannelForm = ( form: CreateChannelExpansionPayload, onChange: (key: K, value: CreateChannelExpansionPayload[K]) => void, isEdit = false, ) => (
人员信息
{(form.contacts ?? []).map((contact, index) => (
handleChannelContactChange(index, "name", e.target.value, isEdit)} placeholder="人员姓名" className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" /> handleChannelContactChange(index, "mobile", e.target.value, isEdit)} placeholder="联系电话" className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" /> handleChannelContactChange(index, "title", e.target.value, isEdit)} placeholder="职位" className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm outline-none focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50" />
))}