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 (
-
- )
-}
diff --git a/web/components/customer-link-or-create-dialog.tsx b/web/components/customer-link-or-create-dialog.tsx
index a3e2b7d..f6d4392 100644
--- a/web/components/customer-link-or-create-dialog.tsx
+++ b/web/components/customer-link-or-create-dialog.tsx
@@ -15,8 +15,6 @@ export type CustomerLinkOrCreateDialogProps = {
onOpenChange: (open: boolean) => void
/** 传入时会话侧:关联已有或新建后绑定该会话 */
conversationId?: number | null
- /** 传入时工单侧:关联已有或新建后绑定该工单 */
- ticketId?: number | null
/** 绑定成功或仅新建成功后的回调 */
onSuccess?: () => void | Promise
}
@@ -27,7 +25,6 @@ export function CustomerLinkOrCreateDialog({
open,
onOpenChange,
conversationId,
- ticketId,
onSuccess,
}: CustomerLinkOrCreateDialogProps) {
const [searchText, setSearchText] = useState("")
@@ -73,7 +70,7 @@ export function CustomerLinkOrCreateDialog({
}
const handleLinkExisting = async (customer: AdminCustomer) => {
- if (!conversationId && !ticketId) {
+ if (!conversationId) {
toast.success(`已选择客户:${customer.name || `#${customer.id}`}`)
onOpenChange(false)
await onSuccess?.()
@@ -86,9 +83,6 @@ export function CustomerLinkOrCreateDialog({
conversationId,
customerId: customer.id,
})
- } else if (ticketId) {
- toast.error("轻量工单暂不支持在此关联客户")
- return
}
toast.success("已关联客户")
onOpenChange(false)
@@ -110,8 +104,6 @@ export function CustomerLinkOrCreateDialog({
customerId: created.id,
})
toast.success("已创建客户并关联当前会话")
- } else if (ticketId) {
- toast.success("已创建客户")
} else {
toast.success("已创建客户")
}
@@ -129,15 +121,11 @@ export function CustomerLinkOrCreateDialog({
先搜索已有客户;
{conversationId
? "选中即可关联当前会话。"
- : ticketId
- ? "选中即可关联当前工单。"
- : "未接入上下文时仅创建或定位客户。"}
+ : "未接入上下文时仅创建或定位客户。"}
若无结果,可填写下方新客户
{conversationId
? ",保存后将自动关联会话。"
- : ticketId
- ? ",保存后将自动关联工单。"
- : "。"}
+ : "。"}
>
)
@@ -164,8 +152,6 @@ export function CustomerLinkOrCreateDialog({
? "提交中…"
: conversationId
? "创建并关联会话"
- : ticketId
- ? "创建并关联工单"
: "创建客户"}
) : null}
@@ -230,8 +216,6 @@ export function CustomerLinkOrCreateDialog({
? "处理中…"
: conversationId
? "关联"
- : ticketId
- ? "关联"
: "选用"}
diff --git a/web/lib/api/ticket.ts b/web/lib/api/ticket.ts
index 6b5a95c..b46b0f8 100644
--- a/web/lib/api/ticket.ts
+++ b/web/lib/api/ticket.ts
@@ -206,21 +206,11 @@ export function changeTicketStatus(payload: {
})
}
-export function fetchTicketProgresses(query: {
- ticketId: number
- page?: number
- limit?: number
-}) {
- return request>(
- `/api/dashboard/ticket/progress/list${toQueryString(query)}`,
- )
-}
-
export function createTicketProgress(payload: {
ticketId: number
content: string
}) {
- return request("/api/dashboard/ticket/progress/create", {
+ return request("/api/dashboard/ticket/add_progress", {
method: "POST",
body: JSON.stringify(payload),
})