import { useState, useEffect } from 'react' import { useParams, useNavigate } from 'react-router-dom' 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 api from '../api/client' import Layout from '../components/Layout' import SeverityBadge from '../components/SeverityBadge' import StatusBadge from '../components/StatusBadge' import CTISelect from '../components/CTISelect' import Avatar from '../components/Avatar' import { Ticket, TicketStatus, User, Comment, AuditLog } from '../types' import { useAuth } from '../contexts/AuthContext' 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' }, ] const AUDIT_LABELS: Record = { CREATED: 'created this ticket', STATUS_CHANGED: 'changed status', ASSIGNEE_CHANGED: 'changed assignee', SEVERITY_CHANGED: 'changed severity', REROUTED: 'rerouted ticket', TITLE_CHANGED: 'updated title', OVERVIEW_CHANGED: 'updated overview', COMMENT_ADDED: 'added a comment', COMMENT_DELETED: 'deleted a comment', } const AUDIT_COLORS: Record = { CREATED: 'bg-green-500', STATUS_CHANGED: 'bg-blue-500', ASSIGNEE_CHANGED: 'bg-purple-500', SEVERITY_CHANGED: 'bg-orange-500', REROUTED: 'bg-cyan-500', TITLE_CHANGED: 'bg-gray-500', OVERVIEW_CHANGED: 'bg-gray-500', COMMENT_ADDED: 'bg-gray-500', COMMENT_DELETED: 'bg-red-500', } const COMMENT_ACTIONS = new Set(['COMMENT_ADDED', 'COMMENT_DELETED']) const selectClass = 'w-full bg-gray-800 border border-gray-700 text-gray-100 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' function SidebarField({ label, children }: { label: string; children: React.ReactNode }) { return (

{label}

{children}
) } export default function TicketDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { user: authUser } = useAuth() const [ticket, setTicket] = useState(null) const [users, setUsers] = useState([]) const [auditLogs, setAuditLogs] = useState([]) const [loading, setLoading] = useState(true) const [tab, setTab] = useState('overview') const [editing, setEditing] = useState(false) const [reroutingCTI, setReroutingCTI] = useState(false) const [commentBody, setCommentBody] = useState('') const [submittingComment, setSubmittingComment] = useState(false) const [preview, setPreview] = useState(false) const [expandedLogs, setExpandedLogs] = useState>(new Set()) const [editForm, setEditForm] = useState({ title: '', overview: '' }) const [pendingCTI, setPendingCTI] = useState({ categoryId: '', typeId: '', itemId: '' }) const isAdmin = authUser?.role === 'ADMIN' useEffect(() => { Promise.all([ api.get(`/tickets/${id}`), api.get('/users'), ]).then(([tRes, uRes]) => { setTicket(tRes.data) setUsers(uRes.data) }).finally(() => setLoading(false)) }, [id]) useEffect(() => { if (tab === 'audit' && ticket) { api.get(`/tickets/${id}/audit`).then((r) => setAuditLogs(r.data)) } }, [tab, ticket, id]) const patch = async (payload: Record) => { if (!ticket) return const res = await api.patch(`/tickets/${ticket.displayId}`, payload) setTicket(res.data) return res.data } 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 deleteTicket = async () => { if (!ticket || !confirm('Delete this ticket? This cannot be undone.')) return await api.delete(`/tickets/${ticket.displayId}`) navigate('/') } const submitComment = async (e: React.FormEvent) => { e.preventDefault() if (!ticket || !commentBody.trim()) return setSubmittingComment(true) try { const res = await api.post(`/tickets/${ticket.displayId}/comments`, { body: commentBody.trim(), }) setTicket((t) => t ? { ...t, comments: [...(t.comments ?? []), res.data] } : t) setCommentBody('') setPreview(false) } finally { setSubmittingComment(false) } } const deleteComment = async (commentId: string) => { if (!ticket) return await api.delete(`/tickets/${ticket.displayId}/comments/${commentId}`) setTicket((t) => t ? { ...t, comments: t.comments?.filter((c) => c.id !== commentId) } : t) } 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 (loading) { return (
Loading...
) } if (!ticket) { return (
Ticket not found
) } const commentCount = ticket.comments?.length ?? 0 const agentUsers = users.filter((u) => u.role !== 'SERVICE') // Status options: CLOSED only for admins 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-blue-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 ? (