"use client"; import Link from "next/link"; import { Building2Icon, Link2Icon, MailIcon, PencilIcon, PhoneIcon, UserRoundIcon, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { type CustomerFormSavePayload } from "@/components/customer-form"; import { CustomerFormDialog } from "@/components/customer-form-dialog"; import { CustomerLinkOrCreateDialog } from "@/components/customer-link-or-create-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Field, FieldContent, FieldLabel, } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import type { AgentConversation } from "@/lib/api/agent"; import { type TagTree, fetchTagsAll } from "@/lib/api/admin"; import { updateCompany, type AdminCompany } from "@/lib/api/company"; import { fetchTickets, type TicketItem } from "@/lib/api/ticket"; import { fetchCustomer, saveCustomerProfile, type AdminCustomer, } from "@/lib/api/customer"; import { fetchCustomerContacts, type AdminCustomerContact, } from "@/lib/api/customer-contact"; import { ContactType, ContactTypeLabels, Gender, GenderLabels, } from "@/lib/generated/enums"; import { useAgentConversationsStore } from "@/lib/stores/agent-conversations"; import { cn, formatDateTime } from "@/lib/utils"; import { ConversationTagBadges, ConversationTagPicker, } from "./conversation-tag-picker"; import { TicketPriorityBadge } from "../../tickets/_components/ticket-priority-badge"; import { TicketStatusBadge } from "../../tickets/_components/ticket-status-badge"; function contactTypeLabel(contactType: ContactType | string) { return ContactTypeLabels[contactType as ContactType] ?? contactType; } function ContactTypeIcon({ contactType }: { contactType: ContactType | string }) { const cls = "size-3.5 shrink-0 text-muted-foreground"; switch (contactType) { case ContactType.Mobile: return ; case ContactType.Email: return ; default: return ; } } function DetailRow({ label, value, valueClassName, }: { label: string; value: string; valueClassName?: string; }) { const empty = !value.trim(); return (
{label} {empty ? "—" : value}
); } function SectionHeading({ children, action, }: { children: React.ReactNode; action?: React.ReactNode; }) { return (

{children}

{action}
); } function UnlinkedCustomerEmpty({ conversation }: { conversation: AgentConversation }) { const [linkDialogOpen, setLinkDialogOpen] = useState(false); const loadConversations = useAgentConversationsStore((s) => s.loadConversations); return (

尚未关联 CRM 客户

当前会话未绑定客户主档。绑定后可在此维护公司与联系方式。

void loadConversations()} />
); } function MissingCustomerEmpty({ conversation }: { conversation: AgentConversation }) { const [linkDialogOpen, setLinkDialogOpen] = useState(false); const loadConversations = useAgentConversationsStore((s) => s.loadConversations); return (

客户已删除或不存在

当前会话绑定的客户主档已不可用。你可以重新关联已有客户,或直接新建一个客户并绑定到当前会话。

会话归属
void loadConversations()} />
); } type ConversationInfoPanelProps = { conversation: AgentConversation | null; className?: string; variant?: "default" | "embedded"; }; export function ConversationInfoPanel({ conversation, className, variant = "default", }: ConversationInfoPanelProps) { const embedded = variant === "embedded"; return (

会话信息

{!conversation ? (

{embedded ? "请选择会话以查看会话信息" : "请选择左侧会话以查看会话信息"}

) : (
)}
); } function ConversationTagSection({ conversation, }: { conversation: AgentConversation; }) { const setConversationTags = useAgentConversationsStore( (state) => state.setConversationTags, ); const [availableTags, setAvailableTags] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; async function loadTags() { setLoading(true); try { const data = await fetchTagsAll(); if (!cancelled) { setAvailableTags(Array.isArray(data) ? data : []); } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "加载标签失败"); } } finally { if (!cancelled) { setLoading(false); } } } void loadTags(); return () => { cancelled = true; }; }, []); return (
{ setConversationTags(conversation.id, tags); }} /> } > 会话标签 {!conversation.tags || conversation.tags.length === 0 ? (

暂未设置会话标签

) : null}
); } function CustomerBody({ conversation }: { conversation: AgentConversation }) { const customerId = conversation.customerId ?? 0; if (customerId <= 0) { return (
); } return ; } type CustomerLinkedBodyProps = { conversation: AgentConversation; customerId: number; }; function CustomerLinkedBody({ conversation, customerId }: CustomerLinkedBodyProps) { const [loading, setLoading] = useState(true); const [customer, setCustomer] = useState(null); const [contacts, setContacts] = useState([]); const [customerEditOpen, setCustomerEditOpen] = useState(false); const [customerEditSaving, setCustomerEditSaving] = useState(false); const [companyEditOpen, setCompanyEditOpen] = useState(false); const load = useCallback(async () => { setLoading(true); try { const c = await fetchCustomer(customerId); setCustomer(c); if (!c) { setContacts([]); return; } const list = await fetchCustomerContacts(customerId); setContacts(Array.isArray(list) ? list : []); } catch (e) { const msg = e instanceof Error ? e.message : "加载客户信息失败"; toast.error(msg); setCustomer(null); setContacts([]); } finally { setLoading(false); } }, [customerId]); useEffect(() => { void load(); }, [load]); const isProfileEmpty = customer && !customer.name.trim() && !customer.primaryMobile.trim() && !customer.primaryEmail.trim() && customer.companyId === 0 && !customer.remark.trim(); if (loading && !customer) { return (

加载客户信息…

); } if (!customer) { return (
); } const displayName = customer.name.trim() || "未填写姓名"; const company = customer.company ?? null; const genderLabel = customer.gender === Gender.Male || customer.gender === Gender.Female ? GenderLabels[customer.gender as Gender] ?? String(customer.gender) : null; return (
{isProfileEmpty ? (
客户主档已关联,但基础信息尚未填写。请点击「编辑」补全资料。
) : null}

{displayName} {genderLabel ? ( {" "} · {genderLabel} ) : null}

{contacts.length === 0 ? (

暂无联系方式

) : (
    {contacts.map((row) => { const tags: string[] = []; if (row.isPrimary) { tags.push("主"); } if (row.isVerified) { tags.push("已验证"); } return (
  • {row.contactValue} {contactTypeLabel(row.contactType)} {tags.length > 0 ? ( {tags.join(" · ")} ) : null}

    {row.remark ? (

    {row.remark}

    ) : null}
  • ); })}
)}
{customer.companyId > 0 ? (
{company ? (

{company.name}

{company.code ? (

{company.code}

) : null}
) : (

公司信息加载失败或公司已删除。

)}
) : null} { if (customerEditSaving) { return; } setCustomerEditSaving(true); try { await saveCustomerProfile({ ...payload, id: customer.id }); toast.success("已保存"); void load(); setCustomerEditOpen(false); } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败"); } finally { setCustomerEditSaving(false); } }} /> {company ? ( { void load(); }} /> ) : null}
); } function RelatedTicketsSection({ conversation }: { conversation: AgentConversation }) { const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let cancelled = false; async function loadTickets() { setLoading(true); try { const data = await fetchTickets({ conversationId: conversation.id, page: 1, limit: 5, }); if (!cancelled) { setTickets(Array.isArray(data.results) ? data.results : []); } } catch (error) { if (!cancelled) { toast.error(error instanceof Error ? error.message : "加载关联工单失败"); } } finally { if (!cancelled) { setLoading(false); } } } void loadTickets(); return () => { cancelled = true; }; }, [conversation.id]); return (
关联工单 {loading ? (

加载工单中…

) : tickets.length > 0 ? (
{tickets.map((ticket) => (
{ticket.title}
{ticket.ticketNo}
{ticket.updatedAt ? formatDateTime(ticket.updatedAt) : "—"}
))}
) : (

当前会话暂无关联工单

)}
); } type CompanyEditDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; company: AdminCompany; onSaved: () => void; }; function CompanyEditDialog({ open, onOpenChange, company, onSaved, }: CompanyEditDialogProps) { const [name, setName] = useState(""); const [code, setCode] = useState(""); const [remark, setRemark] = useState(""); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) { return; } setName(company.name); setCode(company.code); setRemark(company.remark); }, [open, company]); const handleSubmit = async () => { const trimmedName = name.trim(); if (!trimmedName) { toast.error("公司名称不能为空"); return; } setSaving(true); try { await updateCompany({ id: company.id, name: trimmedName, code: code.trim(), remark: remark.trim(), }); toast.success("已保存"); onSaved(); onOpenChange(false); } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } }; return ( 编辑公司
公司名称 setName(e.target.value)} /> 公司编码 setCode(e.target.value)} /> 备注