"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 (
)
}