diff --git a/internal/controllers/dashboard/ticket_controller.go b/internal/controllers/dashboard/ticket_controller.go index 5630b34..421250a 100644 --- a/internal/controllers/dashboard/ticket_controller.go +++ b/internal/controllers/dashboard/ticket_controller.go @@ -62,7 +62,8 @@ func (c *TicketController) AnySummary() *web.JsonResult { if err != nil { return web.JsonError(err) } - return web.JsonData(builders.BuildTicketSummary(services.TicketService.GetSummary(operator))) + staleHours, _ := params.GetInt(c.Ctx, "staleHours") + return web.JsonData(builders.BuildTicketSummary(services.TicketService.GetSummary(operator, staleHours))) } func (c *TicketController) AnyView_list() *web.JsonResult { diff --git a/web/app/dashboard/conversations/_components/conversation-info-panel.tsx b/web/app/dashboard/conversations/_components/conversation-info-panel.tsx index 99279b8..37d9bc8 100644 --- a/web/app/dashboard/conversations/_components/conversation-info-panel.tsx +++ b/web/app/dashboard/conversations/_components/conversation-info-panel.tsx @@ -1,6 +1,4 @@ "use client"; - -import Link from "next/link"; import { Building2Icon, Link2Icon, @@ -626,10 +624,9 @@ function RelatedTicketsSection({ conversation }: { conversation: AgentConversati ) : tickets.length > 0 ? (
{tickets.map((ticket) => ( -
@@ -647,7 +644,7 @@ function RelatedTicketsSection({ conversation }: { conversation: AgentConversati {ticket.updatedAt ? formatDateTime(ticket.updatedAt) : "—"}
- +
))}
) : ( diff --git a/web/app/dashboard/tickets/_components/ticket-customer-panel.tsx b/web/app/dashboard/tickets/_components/ticket-customer-panel.tsx deleted file mode 100644 index cb58a39..0000000 --- a/web/app/dashboard/tickets/_components/ticket-customer-panel.tsx +++ /dev/null @@ -1,529 +0,0 @@ -"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)} - /> - - - - 备注 - -