diff --git a/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx b/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx index efd5754..e566bfe 100644 --- a/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx +++ b/web/app/dashboard/tickets/_components/ticket-detail-dialog.tsx @@ -1,12 +1,22 @@ "use client" import { useCallback, useEffect, useRef, useState } from "react" -import { MessageSquareTextIcon, PencilIcon, PlusIcon, RefreshCcwIcon, SendIcon, UserRoundIcon } from "lucide-react" +import { + CheckIcon, + ChevronDownIcon, + MessageSquareTextIcon, + PencilIcon, + PlusIcon, + RefreshCcwIcon, + SendIcon, + UserRoundIcon, +} from "lucide-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 { useConfirm } from "@/components/confirm-provider" import { ContentEditor } from "@/components/content-editor" import { ProjectDialog } from "@/components/project-dialog" import { isRichTextEmpty, SafeRichHTML } from "@/components/safe-rich-html" @@ -19,6 +29,12 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { Separator } from "@/components/ui/separator" import { saveCustomerProfile } from "@/lib/api/customer" import { @@ -35,7 +51,7 @@ import { useI18n } from "@/i18n/provider" import { cn, formatDateTime } from "@/lib/utils" import { EditDialog } from "./edit" import { TicketAssignDialog } from "./ticket-assign-dialog" -import { TicketStatusBadge } from "./ticket-status-badge" +import { getTicketStatusMeta, TicketStatusBadge } from "./ticket-status-badge" type TicketDetailDialogProps = { ticketId: number | null @@ -54,6 +70,10 @@ function getStatusOptions(t: TFunction): Array<{ value: TicketStatus; label: str ] } +function getStatusLabel(status: TicketStatus, t: TFunction) { + return getStatusOptions(t).find((option) => option.value === status)?.label ?? status +} + function sourceLabel(source: string, t: TFunction) { switch (source) { case "manual": @@ -83,6 +103,7 @@ export function TicketDetailDialog({ onChanged, }: TicketDetailDialogProps) { const t = useI18n() + const confirm = useConfirm() const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(false) const [statusSaving, setStatusSaving] = useState(null) @@ -158,6 +179,17 @@ export function TicketDetailDialog({ if (!detail || detail.ticket.status === status) { return } + const confirmed = await confirm({ + title: t("ticket.statusChangeConfirmTitle"), + description: t("ticket.statusChangeConfirmDescription", { + from: getStatusLabel(detail.ticket.status, t), + to: getStatusLabel(status, t), + }), + confirmText: t("ticket.statusChangeConfirm"), + }) + if (!confirmed) { + return + } const activeTicketId = detail.ticket.id const activeDialogSeq = dialogSeqRef.current setStatusSaving(status) @@ -363,20 +395,42 @@ export function TicketDetailDialog({
-
{t("ticket.columnStatus")}
-
- {statusOptions.map((option) => ( - - ))} + {statusSaving ? t("ticket.updating") : getStatusLabel(ticket.status, t)} + + + + {statusOptions.map((option) => { + const selected = ticket.status === option.value + return ( + void handleStatusChange(option.value)} + className="justify-between" + > + {option.label} + {selected ? : null} + + ) + })} + +
diff --git a/web/app/dashboard/tickets/_components/ticket-status-badge.tsx b/web/app/dashboard/tickets/_components/ticket-status-badge.tsx index 0871e90..5734d17 100644 --- a/web/app/dashboard/tickets/_components/ticket-status-badge.tsx +++ b/web/app/dashboard/tickets/_components/ticket-status-badge.tsx @@ -14,9 +14,13 @@ export function ticketStatusLabel(status: string) { return status } +export function getTicketStatusMeta(status: string) { + return statusMap[status as TicketStatus] +} + export function TicketStatusBadge({ status }: { status: string }) { const t = useI18n() - const option = statusMap[status as TicketStatus] + const option = getTicketStatusMeta(status) return ( diff --git a/web/messages/en-US.json b/web/messages/en-US.json index 47247db..5f26aca 100644 --- a/web/messages/en-US.json +++ b/web/messages/en-US.json @@ -1416,6 +1416,7 @@ "loadingRows": "Loading tickets...", "emptyRows": "No tickets yet", "detail": "Details", + "currentStatus": "Current Status", "statusPending": "Pending", "statusInProgress": "In Progress", "statusDone": "Resolved", @@ -1446,6 +1447,9 @@ "loadDetailFailed": "Could not load ticket details.", "statusUpdated": "Ticket status updated.", "statusUpdateFailed": "Could not update ticket status.", + "statusChangeConfirmTitle": "Confirm Status Change", + "statusChangeConfirmDescription": "Change ticket status from \"{from}\" to \"{to}\"?", + "statusChangeConfirm": "Confirm Change", "updating": "Updating...", "progressRequired": "Enter an update.", "progressRecorded": "Update added.", diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index 9a8b020..c3b969d 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -1416,6 +1416,7 @@ "loadingRows": "正在加载工单...", "emptyRows": "暂无工单", "detail": "详情", + "currentStatus": "当前状态", "statusPending": "待处理", "statusInProgress": "处理中", "statusDone": "已处理", @@ -1446,6 +1447,9 @@ "loadDetailFailed": "加载工单详情失败", "statusUpdated": "工单状态已更新", "statusUpdateFailed": "更新工单状态失败", + "statusChangeConfirmTitle": "确认修改工单状态", + "statusChangeConfirmDescription": "确定要将工单状态从「{from}」修改为「{to}」吗?", + "statusChangeConfirm": "确认修改", "updating": "更新中...", "progressRequired": "请填写处理进展", "progressRecorded": "处理进展已记录",