"use client" import { useCallback, useEffect, useRef, useState } from "react" import { MessageSquareTextIcon, RefreshCcwIcon, SendIcon, UserRoundIcon } from "lucide-react" import { toast } from "sonner" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Textarea } from "@/components/ui/textarea" import { changeTicketStatus, createTicketProgress, fetchTicketDetail, type CreateTicketPayload, type TicketDetail, type TicketStatus, type UpdateTicketPayload, updateTicket, } from "@/lib/api/ticket" import { cn, formatDateTime } from "@/lib/utils" import { EditDialog } from "./edit" import { TicketAssignDialog } from "./ticket-assign-dialog" import { TicketStatusBadge } from "./ticket-status-badge" type TicketDetailDialogProps = { ticketId: number | null open: boolean onOpenChange: (open: boolean) => void onChanged: () => void } const statusOptions: Array<{ value: TicketStatus; label: string }> = [ { value: "pending", label: "待处理" }, { value: "in_progress", label: "处理中" }, { value: "done", label: "已处理" }, ] function sourceLabel(source: string) { switch (source) { case "manual": return "手动创建" case "conversation": return "会话生成" default: return source || "-" } } function metadataValue(value?: string | number | null) { if (value === undefined || value === null || value === "") { return "-" } return String(value) } export function TicketDetailDialog({ ticketId, open, onOpenChange, onChanged, }: TicketDetailDialogProps) { const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(false) const [statusSaving, setStatusSaving] = useState(null) const [progressSaving, setProgressSaving] = useState(false) const [progressContent, setProgressContent] = useState("") const [assignOpen, setAssignOpen] = useState(false) const [editOpen, setEditOpen] = useState(false) const [editSaving, setEditSaving] = useState(false) const loadSeqRef = useRef(0) const loadDetail = useCallback(async () => { const seq = loadSeqRef.current + 1 loadSeqRef.current = seq if (!open || !ticketId) { setDetail(null) setLoading(false) return } setLoading(true) try { const data = await fetchTicketDetail(ticketId) if (loadSeqRef.current !== seq) { return } setDetail(data) } catch (error) { if (loadSeqRef.current !== seq) { return } toast.error(error instanceof Error ? error.message : "加载工单详情失败") } finally { if (loadSeqRef.current === seq) { setLoading(false) } } }, [open, ticketId]) useEffect(() => { void loadDetail() }, [loadDetail]) async function handleStatusChange(status: TicketStatus) { if (!detail || detail.ticket.status === status) { return } setStatusSaving(status) try { await changeTicketStatus({ ticketId: detail.ticket.id, status }) toast.success("工单状态已更新") await loadDetail() onChanged() } catch (error) { toast.error(error instanceof Error ? error.message : "更新工单状态失败") } finally { setStatusSaving(null) } } async function handleCreateProgress() { if (!detail) { return } const content = progressContent.trim() if (!content) { toast.error("请填写处理进展") return } setProgressSaving(true) try { await createTicketProgress({ ticketId: detail.ticket.id, content, }) toast.success("处理进展已记录") setProgressContent("") await loadDetail() onChanged() } catch (error) { toast.error(error instanceof Error ? error.message : "记录处理进展失败") } finally { setProgressSaving(false) } } async function handleAssigned() { await loadDetail() onChanged() } async function handleUpdateTicket(payload: CreateTicketPayload | UpdateTicketPayload) { if (!("ticketId" in payload) || payload.ticketId <= 0) { toast.error("请选择工单") return } setEditSaving(true) try { await updateTicket(payload) toast.success("工单已更新") setEditOpen(false) await loadDetail() onChanged() } catch (error) { toast.error(error instanceof Error ? error.message : "更新工单失败") } finally { setEditSaving(false) } } const ticket = detail?.ticket return ( <> {ticket?.title ?? "工单详情"} {ticket ? : null} {ticket ? (
{ticket.ticketNo} {sourceLabel(ticket.source)} 创建人:{metadataValue(ticket.createdByName || ticket.createdBy)}
) : null}
{loading && !ticket ? (
加载中...
) : ticket ? (
描述
{ticket.description || "暂无描述"}
状态
{statusOptions.map((option) => ( ))}
负责人
{ticket.currentAssigneeName || "未分配"}
标签
{ticket.tags && ticket.tags.length > 0 ? (
{ticket.tags.map((tag) => ( {tag.name} ))}
) : (
暂无标签
)}
处理进展