feat(ticket): add customer linking functionality to tickets

This commit is contained in:
mlogclub
2026-05-03 12:40:16 +08:00
parent ac0f25f10c
commit dba175b0fa
7 changed files with 169 additions and 23 deletions
@@ -6,6 +6,7 @@ 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 { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
@@ -58,6 +59,10 @@ function metadataValue(value?: string | number | null) {
return String(value)
}
function getTicketCustomerId(ticket?: TicketDetail["ticket"] | null) {
return Number(ticket?.customer?.id || ticket?.customerId || 0)
}
export function TicketDetailDialog({
ticketId,
open,
@@ -74,6 +79,7 @@ export function TicketDetailDialog({
const [editSaving, setEditSaving] = useState(false)
const [customerEditOpen, setCustomerEditOpen] = useState(false)
const [customerEditSaving, setCustomerEditSaving] = useState(false)
const [customerLinkOpen, setCustomerLinkOpen] = useState(false)
const loadSeqRef = useRef(0)
const dialogSeqRef = useRef(0)
const currentTicketIdRef = useRef<number | null>(null)
@@ -124,6 +130,7 @@ export function TicketDetailDialog({
setAssignOpen(false)
setEditOpen(false)
setCustomerEditOpen(false)
setCustomerLinkOpen(false)
setProgressContent("")
}, [open, ticketId])
@@ -245,7 +252,8 @@ export function TicketDetailDialog({
}
async function handleUpdateCustomer(payload: CustomerFormSavePayload) {
if (!ticket?.id || !ticket.customerId) {
const activeCustomerId = getTicketCustomerId(ticket)
if (!ticket?.id || activeCustomerId <= 0) {
toast.error("当前工单未关联客户")
return
}
@@ -253,7 +261,6 @@ export function TicketDetailDialog({
return
}
const activeTicketId = ticket.id
const activeCustomerId = ticket.customerId
const activeDialogSeq = dialogSeqRef.current
setCustomerEditSaving(true)
try {
@@ -280,7 +287,21 @@ export function TicketDetailDialog({
}
}
async function handleCustomerLinked() {
const activeTicketId = ticket?.id
const activeDialogSeq = dialogSeqRef.current
if (!activeTicketId || !isCurrentOperation(activeTicketId, activeDialogSeq)) {
return
}
await loadDetail(activeTicketId, activeDialogSeq)
if (!isCurrentOperation(activeTicketId, activeDialogSeq)) {
return
}
onChanged()
}
const ticket = detail?.ticket
const customerId = getTicketCustomerId(ticket)
return (
<>
@@ -378,18 +399,22 @@ export function TicketDetailDialog({
<section className="space-y-3 rounded-md border p-3 text-sm">
<div className="flex items-center justify-between gap-2">
<div className="font-medium text-muted-foreground"></div>
{ticket.customerId > 0 ? (
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1 px-2 text-xs"
onClick={() => setCustomerEditOpen(true)}
>
<PencilIcon className="size-3.5" />
</Button>
) : null}
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1 px-2 text-xs"
onClick={() => {
if (customerId > 0) {
setCustomerEditOpen(true)
return
}
setCustomerLinkOpen(true)
}}
>
<PencilIcon className="size-3.5" />
{customerId > 0 ? "编辑" : "关联或创建"}
</Button>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<MetadataItem label="客户" value={ticket.customer?.name || ticket.customerId} />
@@ -477,9 +502,15 @@ export function TicketDetailDialog({
open={customerEditOpen}
onOpenChange={setCustomerEditOpen}
saving={customerEditSaving}
itemId={ticket?.customerId ? ticket.customerId : null}
itemId={customerId > 0 ? customerId : null}
onSave={handleUpdateCustomer}
/>
<CustomerLinkOrCreateDialog
open={customerLinkOpen}
onOpenChange={setCustomerLinkOpen}
ticketId={ticket?.id ?? null}
onSuccess={handleCustomerLinked}
/>
</>
)
}
@@ -9,12 +9,15 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { linkConversationToCustomer } from "@/lib/api/agent"
import { fetchCustomers, saveCustomerProfile, type AdminCustomer } from "@/lib/api/customer"
import { linkTicketToCustomer } from "@/lib/api/ticket"
export type CustomerLinkOrCreateDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
/** 传入时会话侧:关联已有或新建后绑定该会话 */
conversationId?: number | null
/** 传入时工单侧:关联已有或新建后绑定该工单 */
ticketId?: number | null
/** 绑定成功或仅新建成功后的回调 */
onSuccess?: () => void | Promise<void>
}
@@ -25,6 +28,7 @@ export function CustomerLinkOrCreateDialog({
open,
onOpenChange,
conversationId,
ticketId,
onSuccess,
}: CustomerLinkOrCreateDialogProps) {
const [searchText, setSearchText] = useState("")
@@ -70,7 +74,7 @@ export function CustomerLinkOrCreateDialog({
}
const handleLinkExisting = async (customer: AdminCustomer) => {
if (!conversationId) {
if (!conversationId && !ticketId) {
toast.success(`已选择客户:${customer.name || `#${customer.id}`}`)
onOpenChange(false)
await onSuccess?.()
@@ -84,6 +88,12 @@ export function CustomerLinkOrCreateDialog({
customerId: customer.id,
})
}
if (ticketId) {
await linkTicketToCustomer({
ticketId,
customerId: customer.id,
})
}
toast.success("已关联客户")
onOpenChange(false)
await onSuccess?.()
@@ -103,7 +113,15 @@ export function CustomerLinkOrCreateDialog({
conversationId,
customerId: created.id,
})
toast.success("已创建客户并关联当前会话")
}
if (ticketId) {
await linkTicketToCustomer({
ticketId,
customerId: created.id,
})
}
if (conversationId || ticketId) {
toast.success(conversationId ? "已创建客户并关联当前会话" : "已创建客户并关联当前工单")
} else {
toast.success("已创建客户")
}
@@ -119,12 +137,12 @@ export function CustomerLinkOrCreateDialog({
const description = (
<>
{conversationId
? "选中即可关联当前会话"
{conversationId || ticketId
? `选中即可关联当前${conversationId ? "会话" : "工单"}`
: "未接入上下文时仅创建或定位客户。"}
{conversationId
? ",保存后将自动关联会话"
{conversationId || ticketId
? `,保存后将自动关联${conversationId ? "会话" : "工单"}`
: "。"}
</>
)
+10
View File
@@ -175,6 +175,16 @@ export function updateTicket(payload: UpdateTicketPayload) {
})
}
export function linkTicketToCustomer(payload: {
ticketId: number
customerId: number
}) {
return request<void>("/api/dashboard/ticket/link_customer", {
method: "POST",
body: JSON.stringify(payload),
})
}
export function assignTicket(payload: {
ticketId: number
toUserId: number