Ticket IDs, audit log, markdown comments, tabbed detail page

- Tickets get a random display ID (V + 9 digits, e.g. V325813929)
- Ticket detail page has Overview / Comments / Audit Log tabs
- Audit log records every action (create, status, assignee, severity,
  reroute, title/overview edit, comment add/delete) with who and when
- Comments redesigned: avatar (initials + color), markdown rendering
  via react-markdown + remark-gfm, Write/Preview toggle
- Dashboard shows displayId and assignee avatar
- URLs now use displayId (/tickets/V325813929)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 20:53:37 -04:00
parent 429a530fc8
commit f65c259a71
11 changed files with 2157 additions and 309 deletions
+21 -8
View File
@@ -6,6 +6,7 @@ import api from '../api/client'
import Layout from '../components/Layout'
import SeverityBadge from '../components/SeverityBadge'
import StatusBadge from '../components/StatusBadge'
import Avatar from '../components/Avatar'
import { Ticket, TicketStatus } from '../types'
const STATUSES: { value: TicketStatus | ''; label: string }[] = [
@@ -102,7 +103,7 @@ export default function Dashboard() {
{tickets.map((ticket) => (
<Link
key={ticket.id}
to={`/tickets/${ticket.id}`}
to={`/tickets/${ticket.displayId}`}
className="flex items-center gap-4 bg-white border border-gray-200 rounded-lg px-4 py-3 hover:border-blue-400 hover:shadow-sm transition-all group"
>
{/* Severity stripe */}
@@ -122,6 +123,9 @@ export default function Dashboard() {
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-xs font-mono font-medium text-gray-400">
{ticket.displayId}
</span>
<SeverityBadge severity={ticket.severity} />
<StatusBadge status={ticket.status} />
<span className="text-xs text-gray-400">
@@ -131,15 +135,24 @@ export default function Dashboard() {
<p className="text-sm font-medium text-gray-900 truncate group-hover:text-blue-700">
{ticket.title}
</p>
<p className="text-xs text-gray-400 truncate mt-0.5">{ticket.overview}</p>
</div>
<div className="text-right text-xs text-gray-400 flex-shrink-0 space-y-0.5">
<div className="font-medium text-gray-600">
{ticket.assignee?.displayName ?? 'Unassigned'}
</div>
<div>{ticket._count?.comments ?? 0} comments</div>
<div>{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}</div>
<div className="flex items-center gap-3 flex-shrink-0">
{ticket.assignee && (
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Avatar name={ticket.assignee.displayName} size="sm" />
<span>{ticket.assignee.displayName}</span>
</div>
)}
{!ticket.assignee && (
<span className="text-xs text-gray-400">Unassigned</span>
)}
<span className="text-xs text-gray-400">
{ticket._count?.comments ?? 0} comments
</span>
<span className="text-xs text-gray-400">
{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
</span>
</div>
</Link>
))}