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 [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">
|
||||||
|
|||||||
Reference in New Issue
Block a user