diff --git a/client/src/App.tsx b/client/src/App.tsx index 8dd7f5b..12c157e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,7 +6,7 @@ import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import Tickets from './pages/Tickets'; import MyTickets from './pages/MyTickets'; -import TicketDetail from './pages/TicketDetail'; +import TicketDetail from './pages/ticket-detail'; import Notifications from './pages/Notifications'; import Settings from './pages/Settings'; import AdminUsers from './pages/admin/Users'; diff --git a/client/src/pages/TicketDetail.tsx b/client/src/pages/TicketDetail.tsx deleted file mode 100644 index f5f5e4a..0000000 --- a/client/src/pages/TicketDetail.tsx +++ /dev/null @@ -1,774 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { useShortcut } from '../hooks/useShortcuts'; -import { format, formatDistanceToNow } from 'date-fns'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import { - Pencil, - Trash2, - Send, - X, - Check, - MessageSquare, - ClipboardList, - FileText, - ArrowLeft, - ChevronDown, - ChevronRight, -} from 'lucide-react'; -import { toast } from 'sonner'; -import Layout from '../components/Layout'; -import Modal from '../components/Modal'; -import SeverityBadge from '../components/SeverityBadge'; -import StatusBadge from '../components/StatusBadge'; -import CTISelect from '../components/CTISelect'; -import Avatar from '../components/Avatar'; -import MentionTextarea from '../components/MentionTextarea'; -import { injectMentionLinks } from '../lib/mentions'; -import { TicketStatus } from '../types'; -import { useAuth } from '../contexts/AuthContext'; -import { - useTicket, - useTicketAudit, - useUpdateTicket, - useDeleteTicket, - useUsers, - useAddComment, - useDeleteComment, -} from '../api/queries'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog'; - -type Tab = 'overview' | 'comments' | 'audit'; - -const SEVERITY_OPTIONS = [ - { value: 1, label: 'SEV 1 — Critical' }, - { value: 2, label: 'SEV 2 — High' }, - { value: 3, label: 'SEV 3 — Medium' }, - { value: 4, label: 'SEV 4 — Low' }, - { value: 5, label: 'SEV 5 — Minimal' }, -]; - -import { AUDIT_LABELS, AUDIT_COLORS } from '../../../shared/constants/labels'; - -const COMMENT_ACTIONS = new Set(['COMMENT_ADDED', 'COMMENT_DELETED']); - -export default function TicketDetail() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const { user: authUser } = useAuth(); - - const [tab, setTab] = useState('overview'); - const [editing, setEditing] = useState(false); - const [reroutingCTI, setReroutingCTI] = useState(false); - const [commentBody, setCommentBody] = useState(''); - const [preview, setPreview] = useState(false); - const [expandedLogs, setExpandedLogs] = useState>(new Set()); - const [deleteOpen, setDeleteOpen] = useState(false); - - const commentTextareaRef = useRef(null); - const [editForm, setEditForm] = useState({ title: '', overview: '' }); - const [pendingCTI, setPendingCTI] = useState({ categoryId: '', typeId: '', itemId: '' }); - const [statusOpen, setStatusOpen] = useState(false); - const [severityOpen, setSeverityOpen] = useState(false); - const [assigneeOpen, setAssigneeOpen] = useState(false); - const [expandedDates, setExpandedDates] = useState>(new Set()); - const [expandedCommentDates, setExpandedCommentDates] = useState>(new Set()); - - // Draft autosave - const draftKey = id ? `comment-draft:${id}` : null; - useEffect(() => { - if (!draftKey) return; - const saved = localStorage.getItem(draftKey); - if (saved) setCommentBody(saved); - }, [draftKey]); - useEffect(() => { - if (!draftKey) return; - if (commentBody) localStorage.setItem(draftKey, commentBody); - else localStorage.removeItem(draftKey); - }, [commentBody, draftKey]); - - const { data: ticket, isLoading } = useTicket(id); - const { data: users = [] } = useUsers(); - const { data: auditLogs = [] } = useTicketAudit(id, tab === 'audit'); - - const updateTicket = useUpdateTicket(); - const deleteTicketMutation = useDeleteTicket(); - const addComment = useAddComment(id); - const deleteCommentMutation = useDeleteComment(id); - - const toggleDate = (key: string) => - setExpandedDates((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - - const toggleCommentDate = (commentId: string) => - setExpandedCommentDates((prev) => { - const next = new Set(prev); - if (next.has(commentId)) next.delete(commentId); - else next.add(commentId); - return next; - }); - - const isAdmin = authUser?.role === 'ADMIN'; - - const patch = async (payload: Record) => { - if (!ticket) return; - await updateTicket.mutateAsync({ id: ticket.displayId, data: payload }); - }; - - const startEdit = () => { - if (!ticket) return; - setEditForm({ title: ticket.title, overview: ticket.overview }); - setEditing(true); - setTab('overview'); - }; - - const saveEdit = async () => { - await patch({ title: editForm.title, overview: editForm.overview }); - setEditing(false); - }; - - const startReroute = () => { - if (!ticket) return; - setPendingCTI({ categoryId: ticket.categoryId, typeId: ticket.typeId, itemId: ticket.itemId }); - setReroutingCTI(true); - }; - - const saveReroute = async () => { - await patch(pendingCTI); - setReroutingCTI(false); - }; - - const confirmDeleteTicket = async () => { - if (!ticket) return; - await deleteTicketMutation.mutateAsync(ticket.displayId); - toast.success('Ticket deleted'); - navigate('/tickets'); - }; - - const submitComment = async (e: React.FormEvent) => { - e.preventDefault(); - if (!ticket || !commentBody.trim()) return; - await addComment.mutateAsync(commentBody.trim()); - setCommentBody(''); - setPreview(false); - if (draftKey) localStorage.removeItem(draftKey); - }; - - const handleDeleteComment = async (commentId: string) => { - await deleteCommentMutation.mutateAsync(commentId); - }; - - useShortcut('e', (e) => { - if (!ticket || editing) return; - e.preventDefault(); - startEdit(); - }, [ticket, editing]); - - useShortcut('r', (e) => { - if (!ticket) return; - e.preventDefault(); - setTab('comments'); - setPreview(false); - setTimeout(() => commentTextareaRef.current?.focus(), 40); - }, [ticket]); - - const toggleLog = (logId: string) => { - setExpandedLogs((prev) => { - const next = new Set(prev); - if (next.has(logId)) next.delete(logId); - else next.add(logId); - return next; - }); - }; - - if (isLoading) { - return ( - -
- Loading... -
-
- ); - } - - if (!ticket) { - return ( - -
- Ticket not found -
-
- ); - } - - const commentCount = ticket.comments?.length ?? 0; - const agentUsers = users; - - const statusOptions: { value: TicketStatus; label: string }[] = [ - { value: 'OPEN', label: 'Open' }, - { value: 'IN_PROGRESS', label: 'In Progress' }, - { value: 'RESOLVED', label: 'Resolved' }, - ...(isAdmin ? [{ value: 'CLOSED' as TicketStatus, label: 'Closed' }] : []), - ]; - - return ( - - {/* Back link */} - - -
- {/* ── Main content ── */} -
- {/* Title card */} -
-
- - {ticket.displayId} - - - - - {ticket.category.name} › {ticket.type.name} › {ticket.item.name} - - -
- - {editing ? ( - setEditForm((f) => ({ ...f, title: e.target.value }))} - className="w-full text-2xl font-bold text-gray-100 bg-transparent border-0 border-b-2 border-indigo-500 focus:outline-none pb-1" - autoFocus - /> - ) : ( -

{ticket.title}

- )} -
- - {/* Tabs + content */} -
- {/* Tab bar */} -
- {( - [ - { key: 'overview', icon: FileText, label: 'Overview' }, - { - key: 'comments', - icon: MessageSquare, - label: `Comments${commentCount > 0 ? ` (${commentCount})` : ''}`, - }, - { key: 'audit', icon: ClipboardList, label: 'Audit Log' }, - ] as const - ).map(({ key, icon: Icon, label }) => ( - - ))} -
- - {/* ── Overview ── */} - {tab === 'overview' && ( -
- {editing ? ( -
-