Redesign ticket sidebar: modal pickers for status/severity, CTI as clickable unit
- 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:
@@ -85,6 +85,8 @@ export default function TicketDetail() {
|
||||
|
||||
const [editForm, setEditForm] = useState({ title: '', overview: '' })
|
||||
const [pendingCTI, setPendingCTI] = useState({ categoryId: '', typeId: '', itemId: '' })
|
||||
const [editingStatus, setEditingStatus] = useState(false)
|
||||
const [editingSeverity, setEditingSeverity] = useState(false)
|
||||
|
||||
const isAdmin = authUser?.role === 'ADMIN'
|
||||
|
||||
@@ -484,40 +486,72 @@ export default function TicketDetail() {
|
||||
{/* ── Sidebar ── */}
|
||||
<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="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>
|
||||
|
||||
{/* Status + Severity */}
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
<SidebarField label="Status">
|
||||
<select
|
||||
value={ticket.status}
|
||||
onChange={(e) => patch({ status: e.target.value })}
|
||||
className={selectClass}
|
||||
<button
|
||||
onClick={() => setEditingStatus(true)}
|
||||
className="flex items-center gap-1.5 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{statusOptions.map((s) => (
|
||||
<option key={s.value} value={s.value}>{s.label}</option>
|
||||
))}
|
||||
</select>
|
||||
{!isAdmin && ticket.status !== 'CLOSED' && (
|
||||
<p className="text-xs text-gray-600 mt-1">Closing requires admin</p>
|
||||
)}
|
||||
<StatusBadge status={ticket.status} />
|
||||
<ChevronDown size={12} className="text-gray-500" />
|
||||
</button>
|
||||
</SidebarField>
|
||||
|
||||
<SidebarField label="Severity">
|
||||
<select
|
||||
value={ticket.severity}
|
||||
onChange={(e) => patch({ severity: Number(e.target.value) })}
|
||||
className={selectClass}
|
||||
<button
|
||||
onClick={() => setEditingSeverity(true)}
|
||||
className="flex items-center gap-1.5 hover:opacity-75 transition-opacity"
|
||||
>
|
||||
{SEVERITY_OPTIONS.map((s) => (
|
||||
<option key={s.value} value={s.value}>{s.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<SeverityBadge severity={ticket.severity} />
|
||||
<ChevronDown size={12} className="text-gray-500" />
|
||||
</button>
|
||||
</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">
|
||||
<select
|
||||
value={ticket.assigneeId ?? ''}
|
||||
@@ -536,49 +570,13 @@ export default function TicketDetail() {
|
||||
</div>
|
||||
)}
|
||||
</SidebarField>
|
||||
</div>
|
||||
|
||||
{/* Routing */}
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Routing</p>
|
||||
<p className="text-xs text-gray-300 leading-relaxed">
|
||||
{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">
|
||||
<SidebarField label="Requester">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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>
|
||||
<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>
|
||||
<span className="text-xs text-gray-300">{ticket.createdBy.displayName}</span>
|
||||
</div>
|
||||
</SidebarField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -603,7 +601,58 @@ export default function TicketDetail() {
|
||||
</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 && (
|
||||
<Modal title="Change Routing" onClose={() => setReroutingCTI(false)} size="lg">
|
||||
<div className="space-y-5">
|
||||
|
||||
Reference in New Issue
Block a user