"use client" 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 { updateCompany, type AdminCompany } from "@/lib/api/company" import { fetchCustomer, saveCustomerProfile, type AdminCustomer, } from "@/lib/api/customer" import { fetchCustomerContacts, type AdminCustomerContact, } from "@/lib/api/customer-contact" import { Gender, GenderLabels, ContactType, ContactTypeLabels } from "@/lib/generated/enums" import { cn, formatDateTime } from "@/lib/utils" 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({ ticketId, onSuccess, }: { ticketId: number onSuccess: () => void | Promise }) { const [linkDialogOpen, setLinkDialogOpen] = useState(false) return (

尚未关联 CRM 客户

当前工单未绑定客户主档。绑定后可在此查看客户资料、公司信息与联系方式。

) } function MissingCustomerEmpty({ ticketId, onSuccess, }: { ticketId: number onSuccess: () => void | Promise }) { const [linkDialogOpen, setLinkDialogOpen] = useState(false) return (

客户已删除或不存在

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

) } type TicketCustomerPanelProps = { ticketId: number customerId?: number onRefresh: () => void | Promise } type TicketLinkedCustomerPanelProps = { ticketId: number customerId: number onRefresh: () => void | Promise } export function TicketCustomerPanel({ ticketId, customerId = 0, onRefresh, }: TicketCustomerPanelProps) { if (customerId <= 0) { return } return ( ) } function TicketLinkedCustomerPanel({ ticketId, customerId, onRefresh, }: TicketLinkedCustomerPanelProps) { const linkedCustomerId = customerId 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(linkedCustomerId) setCustomer(c) if (!c) { setContacts([]) return } const list = await fetchCustomerContacts(linkedCustomerId) setContacts(Array.isArray(list) ? list : []) } catch (error) { toast.error(error instanceof Error ? error.message : "加载客户信息失败") setCustomer(null) setContacts([]) } finally { setLoading(false) } }, [linkedCustomerId]) useEffect(() => { void load() }, [load]) 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 const isProfileEmpty = !customer.name.trim() && !customer.primaryMobile.trim() && !customer.primaryEmail.trim() && customer.companyId === 0 && !customer.remark.trim() return (
{isProfileEmpty ? (
客户主档已关联,但基础信息尚未填写。请点击「编辑」补全资料。
) : null}
setCustomerEditOpen(true)} > 编辑 } > 客户信息

{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}
  • ) })}
)}
setCompanyEditOpen(true)} > 编辑 ) : null } > 公司信息 {company ? (

{company.name}

{company.code ? (

{company.code}

) : null}
) : (

未关联公司。可通过编辑客户资料补充公司信息。

)}
{ if (customerEditSaving) { return } setCustomerEditSaving(true) try { await saveCustomerProfile({ ...payload, id: customer.id }) toast.success("已保存") await load() await onRefresh() setCustomerEditOpen(false) } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败") } finally { setCustomerEditSaving(false) } }} /> {company ? ( { await load() await onRefresh() }} /> ) : null}
) } type CompanyEditDialogProps = { open: boolean onOpenChange: (open: boolean) => void company: AdminCompany onSaved: () => void | Promise } 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("已保存") await onSaved() onOpenChange(false) } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败") } finally { setSaving(false) } } return ( 编辑公司
公司名称 setName(event.target.value)} /> 公司编码 setCode(event.target.value)} /> 备注