"use client" import { CheckIcon, TagIcon } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import type { Resolver } from "react-hook-form" import { Controller, useForm } from "react-hook-form" import { z } from "zod/v4" import { OptionCombobox } from "@/components/option-combobox" import { ProjectDialog } from "@/components/project-dialog" import { RichTextEditor } from "@/components/rich-text-editor" import { isRichTextEmpty } from "@/components/safe-rich-html" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Field, FieldContent, FieldError, FieldGroup, FieldLabel, } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { fetchAgentProfilesAll, fetchTagsAll, type AdminAgentProfile, type TagTree, } from "@/lib/api/admin" import { fetchTicketDetail, type CreateTicketPayload, type TicketItem, type UpdateTicketPayload, } from "@/lib/api/ticket" type EditDialogProps = { open: boolean saving: boolean itemId: number | null initialValues?: Partial fixedConversationId?: number fixedCustomerId?: number titleOverride?: string descriptionOverride?: string onOpenChange: (open: boolean) => void onSubmit: (payload: CreateTicketPayload | UpdateTicketPayload) => Promise } const schema = z.object({ title: z.string().trim().min(1, "请输入工单标题"), description: z.string().refine((value) => !isRichTextEmpty(value), "请输入问题描述"), currentAssigneeId: z.coerce.number().int().min(0).optional(), tagIds: z.array(z.number().int().positive()).default([]), }) type EditForm = z.infer const editFormResolver = zodResolver(schema as never) as Resolver< z.input, undefined, z.output > const emptyForm: EditForm = { title: "", description: "", currentAssigneeId: 0, tagIds: [], } function buildForm(item: TicketItem | null): EditForm { if (!item) { return emptyForm } return { title: item.title ?? "", description: item.description ?? "", currentAssigneeId: item.currentAssigneeId ?? 0, tagIds: (item.tags ?? []).map((tag) => tag.id), } } function buildInitialForm(initialValues?: Partial): EditForm { return { title: initialValues?.title?.trim() ?? "", description: initialValues?.description ?? "", currentAssigneeId: initialValues?.currentAssigneeId ?? 0, tagIds: initialValues?.tagIds ?? [], } } function buildPayload(form: EditForm): CreateTicketPayload { const currentAssigneeId = form.currentAssigneeId ?? 0 return { title: form.title.trim(), description: form.description.trim(), currentAssigneeId, tagIds: form.tagIds, } } type FlatTagNode = TagTree & { depth: number path: string } function flattenTagTree(nodes: TagTree[], depth = 0, parentPath = ""): FlatTagNode[] { const result: FlatTagNode[] = [] nodes.forEach((item) => { const path = parentPath ? `${parentPath} / ${item.name}` : item.name result.push({ ...item, depth, path }) if (item.children.length > 0) { result.push(...flattenTagTree(item.children, depth + 1, path)) } }) return result } type TicketTagSelectorProps = { value?: number[] onChange: (value: number[]) => void availableTags: TagTree[] } function TicketTagSelector({ value, onChange, availableTags }: TicketTagSelectorProps) { const selectedValues = useMemo(() => value ?? [], [value]) const flatTags = useMemo(() => flattenTagTree(availableTags), [availableTags]) const selectedTagIDs = useMemo(() => new Set(selectedValues), [selectedValues]) const selectedTags = useMemo( () => flatTags.filter((tag) => selectedTagIDs.has(tag.id)), [flatTags, selectedTagIDs], ) function handleToggle(tagID: number) { if (selectedTagIDs.has(tagID)) { onChange(selectedValues.filter((item) => item !== tagID)) return } onChange(selectedValues.concat(tagID)) } return (
} > {selectedTags.length > 0 ? `已选择 ${selectedTags.length} 个标签` : "请选择工单标签"} 暂无可用标签 {flatTags.map((tag) => { const checked = selectedTagIDs.has(tag.id) return ( handleToggle(tag.id)} > {tag.name} ) })} {selectedTags.length > 0 ? (
{selectedTags.map((tag) => ( {tag.path} ))}
) : null}
) } export function EditDialog({ open, saving, itemId, initialValues, fixedConversationId, fixedCustomerId, titleOverride, descriptionOverride, onOpenChange, onSubmit, }: EditDialogProps) { if (!open) { return null } return ( ) } type TicketEditDialogBodyProps = EditDialogProps function TicketEditDialogBody({ open, saving, itemId, initialValues, fixedConversationId, fixedCustomerId, titleOverride, descriptionOverride, onOpenChange, onSubmit, }: TicketEditDialogBodyProps) { const formId = "ticket-edit-form" const [loading, setLoading] = useState(false) const [tags, setTags] = useState([]) const [agents, setAgents] = useState([]) const form = useForm< z.input, undefined, z.output >({ resolver: editFormResolver, defaultValues: emptyForm, }) const { register, control, handleSubmit, reset, formState: { errors }, } = form useEffect(() => { async function loadDetail() { if (!itemId) { reset(buildInitialForm(initialValues)) return } setLoading(true) try { const data = await fetchTicketDetail(itemId) reset(buildForm(data.ticket)) } finally { setLoading(false) } } void loadDetail() }, [initialValues, itemId, reset]) useEffect(() => { if (!open) { return } void (async () => { const [tagData, agentData] = await Promise.all([ fetchTagsAll(), fetchAgentProfilesAll(), ]) setTags(Array.isArray(tagData) ? tagData : []) setAgents(Array.isArray(agentData) ? agentData : []) })() }, [open]) const agentOptions = [{ value: "0", label: "不指定处理人" }].concat( agents.map((agent) => ({ value: String(agent.userId), label: agent.displayName || agent.nickname || agent.username || `客服#${agent.userId}`, })), ) async function onFormSubmit(values: EditForm) { const payload = buildPayload(values) if (itemId) { await onSubmit({ ticketId: itemId, ...payload, }) return } await onSubmit({ ...payload, source: fixedConversationId ? "conversation" : "manual", conversationId: fixedConversationId, customerId: fixedCustomerId, }) } return ( } > {loading ? (
加载中...
) : (
标题 描述 ( )} /> 处理人 ( field.onChange(Number(value))} placeholder="请选择处理人" options={agentOptions} /> )} /> 工单标签 ( )} />
)}
) }