Redesign ticket sidebar: modal pickers for status/severity, CTI as clickable unit
All checks were successful
Build & Push / Build Server (push) Successful in 44s
Build & Push / Build Client (push) Successful in 37s

- Rename 'Details' card to 'Ticket Summary'
- Replace status/severity dropdowns with badge displays that open small modal pickers on click
- Show Category, Type, Issue as separate labeled rows that together act as one clickable unit opening the routing modal
- Reorganize into sections: status/severity, CTI routing, dates (created/modified/resolved), people (assignee + requester)
- Add Requester field showing the ticket creator with avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Josh Wright
2026-03-31 11:56:53 -04:00
parent 2b76ad27b1
commit 44e5e2d373

View File

@@ -85,6 +85,8 @@ export default function TicketDetail() {
const [editForm, setEditForm] = useState({ title: '', overview: '' }) const [editForm, setEditForm] = useState({ title: '', overview: '' })
const [pendingCTI, setPendingCTI] = useState({ categoryId: '', typeId: '', itemId: '' }) const [pendingCTI, setPendingCTI] = useState({ categoryId: '', typeId: '', itemId: '' })
const [editingStatus, setEditingStatus] = useState(false)
const [editingSeverity, setEditingSeverity] = useState(false)
const isAdmin = authUser?.role === 'ADMIN' const isAdmin = authUser?.role === 'ADMIN'
@@ -484,40 +486,72 @@ export default function TicketDetail() {
{/* ── Sidebar ── */} {/* ── Sidebar ── */}
<div className="w-64 flex-shrink-0 sticky top-0 space-y-3"> <div className="w-64 flex-shrink-0 sticky top-0 space-y-3">
{/* Details */} {/* Ticket Summary */}
<div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800"> <div className="bg-gray-900 border border-gray-800 rounded-xl divide-y divide-gray-800">
<div className="px-4 py-3"> <div className="px-4 py-3">
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Details</p> <p className="text-xs font-semibold text-gray-500 uppercase tracking-wide">Ticket Summary</p>
</div> </div>
{/* Status + Severity */}
<div className="px-4 py-3 space-y-3"> <div className="px-4 py-3 space-y-3">
<SidebarField label="Status"> <SidebarField label="Status">
<select <button
value={ticket.status} onClick={() => setEditingStatus(true)}
onChange={(e) => patch({ status: e.target.value })} className="flex items-center gap-1.5 hover:opacity-75 transition-opacity"
className={selectClass}
> >
{statusOptions.map((s) => ( <StatusBadge status={ticket.status} />
<option key={s.value} value={s.value}>{s.label}</option> <ChevronDown size={12} className="text-gray-500" />
))} </button>
</select>
{!isAdmin && ticket.status !== 'CLOSED' && (
<p className="text-xs text-gray-600 mt-1">Closing requires admin</p>
)}
</SidebarField> </SidebarField>
<SidebarField label="Severity"> <SidebarField label="Severity">
<select <button
value={ticket.severity} onClick={() => setEditingSeverity(true)}
onChange={(e) => patch({ severity: Number(e.target.value) })} className="flex items-center gap-1.5 hover:opacity-75 transition-opacity"
className={selectClass}
> >
{SEVERITY_OPTIONS.map((s) => ( <SeverityBadge severity={ticket.severity} />
<option key={s.value} value={s.value}>{s.label}</option> <ChevronDown size={12} className="text-gray-500" />
))} </button>
</select>
</SidebarField> </SidebarField>
</div>
{/* CTI — one clickable unit */}
<button
onClick={startReroute}
className="w-full px-4 py-3 text-left space-y-3 hover:bg-gray-800/50 transition-colors group"
>
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Category</p>
<p className="text-sm text-gray-300">{ticket.category.name}</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Type</p>
<p className="text-sm text-gray-300">{ticket.type.name}</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500 mb-1">Issue</p>
<p className="text-sm text-gray-300">{ticket.item.name}</p>
</div>
<p className="text-xs text-blue-500 group-hover:text-blue-400 transition-colors">Change routing</p>
</button>
{/* Dates */}
<div className="px-4 py-3 space-y-2.5">
<SidebarField label="Created">
<p className="text-xs text-gray-300">{format(new Date(ticket.createdAt), 'MMM d, yyyy HH:mm')}</p>
</SidebarField>
<SidebarField label="Modified">
<p className="text-xs text-gray-300">{formatDistanceToNow(new Date(ticket.updatedAt), { addSuffix: true })}</p>
</SidebarField>
{ticket.resolvedAt && (
<SidebarField label="Resolved">
<p className="text-xs text-gray-300">{format(new Date(ticket.resolvedAt), 'MMM d, yyyy HH:mm')}</p>
</SidebarField>
)}
</div>
{/* People */}
<div className="px-4 py-3 space-y-3">
<SidebarField label="Assignee"> <SidebarField label="Assignee">
<select <select
value={ticket.assigneeId ?? ''} value={ticket.assigneeId ?? ''}
@@ -536,49 +570,13 @@ export default function TicketDetail() {
</div> </div>
)} )}
</SidebarField> </SidebarField>
</div>
{/* Routing */} <SidebarField label="Requester">
<div className="px-4 py-3"> <div className="flex items-center gap-1.5">
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Routing</p> <Avatar name={ticket.createdBy.displayName} size="sm" />
<p className="text-xs text-gray-300 leading-relaxed"> <span className="text-xs text-gray-300">{ticket.createdBy.displayName}</span>
{ticket.category.name}
<span className="text-gray-600"> </span>
{ticket.type.name}
<span className="text-gray-600"> </span>
{ticket.item.name}
</p>
<button
onClick={startReroute}
className="mt-1.5 text-xs text-blue-500 hover:text-blue-400 transition-colors"
>
Change routing
</button>
</div>
{/* Dates */}
<div className="px-4 py-3 space-y-2.5">
<div className="flex items-center gap-2">
<Avatar name={ticket.createdBy.displayName} size="sm" />
<div>
<p className="text-xs text-gray-500">Opened by</p>
<p className="text-xs font-medium text-gray-300">{ticket.createdBy.displayName}</p>
</div> </div>
</div> </SidebarField>
<div>
<p className="text-xs text-gray-500">Created</p>
<p className="text-xs text-gray-300">{format(new Date(ticket.createdAt), 'MMM d, yyyy HH:mm')}</p>
</div>
{ticket.resolvedAt && (
<div>
<p className="text-xs text-gray-500">Resolved</p>
<p className="text-xs text-gray-300">{format(new Date(ticket.resolvedAt), 'MMM d, yyyy HH:mm')}</p>
</div>
)}
<div>
<p className="text-xs text-gray-500">Updated</p>
<p className="text-xs text-gray-300">{formatDistanceToNow(new Date(ticket.updatedAt), { addSuffix: true })}</p>
</div>
</div> </div>
</div> </div>
@@ -603,7 +601,58 @@ export default function TicketDetail() {
</div> </div>
</div> </div>
</div> </div>
{/* Reroute modal */} {editingStatus && (
<Modal title="Change Status" onClose={() => setEditingStatus(false)}>
<div className="space-y-2">
{statusOptions.map((s) => (
<button
key={s.value}
onClick={async () => {
await patch({ status: s.value })
setEditingStatus(false)
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors ${
ticket.status === s.value
? 'border-blue-500/50 bg-blue-500/10'
: 'border-gray-700 hover:bg-gray-800'
}`}
>
<StatusBadge status={s.value} />
{ticket.status === s.value && <Check size={14} className="ml-auto text-blue-400" />}
</button>
))}
{!isAdmin && (
<p className="text-xs text-gray-500 pt-1">Closing a ticket requires admin access</p>
)}
</div>
</Modal>
)}
{editingSeverity && (
<Modal title="Change Severity" onClose={() => setEditingSeverity(false)}>
<div className="space-y-2">
{SEVERITY_OPTIONS.map((s) => (
<button
key={s.value}
onClick={async () => {
await patch({ severity: s.value })
setEditingSeverity(false)
}}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg border transition-colors ${
ticket.severity === s.value
? 'border-blue-500/50 bg-blue-500/10'
: 'border-gray-700 hover:bg-gray-800'
}`}
>
<SeverityBadge severity={s.value} />
<span className="text-sm text-gray-400">{s.label.split(' — ')[1]}</span>
{ticket.severity === s.value && <Check size={14} className="ml-auto text-blue-400" />}
</button>
))}
</div>
</Modal>
)}
{reroutingCTI && ( {reroutingCTI && (
<Modal title="Change Routing" onClose={() => setReroutingCTI(false)} size="lg"> <Modal title="Change Routing" onClose={() => setReroutingCTI(false)} size="lg">
<div className="space-y-5"> <div className="space-y-5">