import { useEffect, useState, type ReactNode } from "react"; import { Search, Plus, ChevronRight, Building, Calendar, DollarSign, Activity, X, Clock, FileText, User, Tag, Link as LinkIcon } from "lucide-react"; import { motion, AnimatePresence } from "motion/react"; import { createOpportunity, createOpportunityFollowUp, getOpportunityOverview, updateOpportunity, type CreateOpportunityFollowUpPayload, type CreateOpportunityPayload, type OpportunityFollowUp, type OpportunityItem } from "@/lib/auth"; const stageOptions = ["全部", "初步沟通", "方案交流", "招投标", "商务谈判", "已成交"] as const; const defaultForm: CreateOpportunityPayload = { opportunityName: "", customerName: "", amount: 0, expectedCloseDate: "", confidencePct: 50, stage: "初步沟通", opportunityType: "新建", productType: "VDI云桌面", source: "主动开发", pushedToOms: false, description: "", }; function toDateTimeLocalValue(date = new Date()) { const timezoneOffset = date.getTimezoneOffset() * 60000; return new Date(date.getTime() - timezoneOffset).toISOString().slice(0, 16); } const defaultFollowUpForm: CreateOpportunityFollowUpPayload = { followUpType: "电话沟通", content: "", nextAction: "", followUpTime: toDateTimeLocalValue(), }; function formatAmount(value?: number) { if (value === undefined || value === null || Number.isNaN(Number(value))) { return "0"; } return new Intl.NumberFormat("zh-CN").format(Number(value)); } function toFormFromItem(item: OpportunityItem): CreateOpportunityPayload { return { opportunityName: item.name || "", customerName: item.client || "", amount: item.amount || 0, expectedCloseDate: item.date || "", confidencePct: item.confidence ?? 50, stage: item.stage || "初步沟通", opportunityType: item.type || "新建", productType: item.product || "VDI云桌面", source: item.source || "主动开发", pushedToOms: Boolean(item.pushedToOms), description: item.notes || "", }; } function ModalShell({ title, subtitle, onClose, children, footer, }: { title: string; subtitle: string; onClose: () => void; children: ReactNode; footer: ReactNode; }) { return ( <>

{title}

{subtitle}

{children}
{footer}
); } export default function Opportunities() { const [filter, setFilter] = useState<(typeof stageOptions)[number]>("全部"); const [keyword, setKeyword] = useState(""); const [selectedItem, setSelectedItem] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [followUpOpen, setFollowUpOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const [followUpError, setFollowUpError] = useState(""); const [items, setItems] = useState([]); const [form, setForm] = useState(defaultForm); const [followUpForm, setFollowUpForm] = useState(defaultFollowUpForm); const hasForegroundModal = createOpen || editOpen || followUpOpen; useEffect(() => { let cancelled = false; async function load() { try { const data = await getOpportunityOverview(keyword, filter); if (!cancelled) { setItems(data.items ?? []); setSelectedItem(null); } } catch { if (!cancelled) { setItems([]); setSelectedItem(null); } } } void load(); return () => { cancelled = true; }; }, [keyword, filter]); const followUpRecords: OpportunityFollowUp[] = selectedItem?.followUps ?? []; const getConfidenceColor = (score: number) => { if (score >= 80) return "text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20"; if (score >= 50) return "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20"; return "text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20"; }; const handleChange = (key: K, value: CreateOpportunityPayload[K]) => { setForm((current) => ({ ...current, [key]: value })); }; const handleFollowUpChange = (key: K, value: CreateOpportunityFollowUpPayload[K]) => { setFollowUpForm((current) => ({ ...current, [key]: value })); }; const handleOpenCreate = () => { setError(""); setForm(defaultForm); setCreateOpen(true); }; const resetCreateState = () => { setCreateOpen(false); setEditOpen(false); setSubmitting(false); setError(""); setForm(defaultForm); }; const resetFollowUpState = () => { setFollowUpOpen(false); setFollowUpError(""); setFollowUpForm({ ...defaultFollowUpForm, followUpTime: toDateTimeLocalValue(), }); }; const reload = async (preferredSelectedId?: number) => { const data = await getOpportunityOverview(keyword, filter); const nextItems = data.items ?? []; setItems(nextItems); if (preferredSelectedId) { setSelectedItem(nextItems.find((item) => item.id === preferredSelectedId) ?? null); } }; const handleCreateSubmit = async () => { if (submitting) { return; } setSubmitting(true); setError(""); try { await createOpportunity(form); await reload(); resetCreateState(); } catch (createError) { setError(createError instanceof Error ? createError.message : "新增商机失败"); setSubmitting(false); } }; const handleOpenEdit = () => { if (!selectedItem) { return; } setError(""); setForm(toFormFromItem(selectedItem)); setEditOpen(true); }; const handleEditSubmit = async () => { if (!selectedItem || submitting) { return; } setSubmitting(true); setError(""); try { await updateOpportunity(selectedItem.id, form); await reload(selectedItem.id); resetCreateState(); } catch (updateError) { setError(updateError instanceof Error ? updateError.message : "编辑商机失败"); setSubmitting(false); } }; const handleOpenFollowUp = () => { if (!selectedItem) { return; } setFollowUpError(""); setFollowUpForm({ followUpType: "电话沟通", content: "", nextAction: "", followUpTime: toDateTimeLocalValue(), }); setFollowUpOpen(true); }; const handleFollowUpSubmit = async () => { if (!selectedItem || submitting) { return; } setSubmitting(true); setFollowUpError(""); try { await createOpportunityFollowUp(selectedItem.id, { ...followUpForm, nextAction: followUpForm.nextAction || undefined, followUpTime: new Date(followUpForm.followUpTime).toISOString(), }); await reload(); resetFollowUpState(); setSelectedItem(null); setSubmitting(false); } catch (submitError) { setFollowUpError(submitError instanceof Error ? submitError.message : "新增跟进失败"); setSubmitting(false); } }; const renderEmpty = () => (
暂无商机数据,先新增一条试试。
); return (

商机储备

setKeyword(event.target.value)} className="w-full rounded-xl border border-slate-200 bg-white py-2.5 pl-10 pr-4 text-sm text-slate-900 outline-none transition-all placeholder:text-slate-400 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 dark:border-slate-800 dark:bg-slate-900/50 dark:text-white dark:placeholder:text-slate-500" />
{stageOptions.map((stage) => ( ))}
{items.length > 0 ? ( items.map((opp, i) => ( setSelectedItem(opp)} className="group relative cursor-pointer rounded-2xl border border-slate-100 bg-white p-5 shadow-sm backdrop-blur-sm transition-all hover:border-violet-100 hover:shadow-md dark:border-slate-800 dark:bg-slate-900/50 dark:hover:border-violet-900/50" >
{opp.code || `#${opp.id}`} {opp.pushedToOms ? ( 已推OMS ) : null}

{opp.name || "未命名商机"}

{opp.confidence ?? 0}% 把握度
{opp.client || "未命名客户"}
¥{formatAmount(opp.amount)}
{opp.date || "未设置"}
{opp.stage || "初步沟通"}
类型: {opp.type || "新建"}
)) ) : renderEmpty()}
{(createOpen || editOpen) && (
)} >