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
+44
View File
@@ -0,0 +1,44 @@
const PALETTE = [
'#ef4444', '#f97316', '#f59e0b', '#10b981',
'#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899',
]
function nameToColor(name: string): string {
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
}
return PALETTE[Math.abs(hash) % PALETTE.length]
}
function initials(name: string): string {
return name
.split(' ')
.slice(0, 2)
.map((n) => n[0])
.join('')
.toUpperCase()
}
const SIZES = {
sm: 'w-6 h-6 text-xs',
md: 'w-8 h-8 text-sm',
lg: 'w-10 h-10 text-base',
}
interface AvatarProps {
name: string
size?: keyof typeof SIZES
}
export default function Avatar({ name, size = 'md' }: AvatarProps) {
return (
<div
className={`${SIZES[size]} rounded-full flex items-center justify-center font-semibold text-white flex-shrink-0 select-none`}
style={{ backgroundColor: nameToColor(name) }}
title={name}
>
{initials(name)}
</div>
)
}