feat: audit log clickable links, communication hub recipient details & link options
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

- Audit log: user names link to /admin/members/{id}, entity IDs link to
  relevant detail pages (projects, rounds, awards, users)
- Communication hub: expandable "View Recipients" section in sidebar shows
  actual users/projects that will receive the message, with collapsible
  project-level detail for applicants and juror assignment counts
- Email link type selector: choose between no link, messages inbox, login
  page, or invite/accept link (auto-detects new vs existing members)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:49:49 +01:00
parent 2e8ab91e07
commit d4c946470a
4 changed files with 455 additions and 25 deletions

View File

@@ -56,6 +56,7 @@ import { Switch } from '@/components/ui/switch'
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
import { formatDate } from '@/lib/utils'
import { cn } from '@/lib/utils'
import Link from 'next/link'
// Action type options (manual audit actions + auto-generated mutation audit actions)
const ACTION_TYPES = [
@@ -223,6 +224,26 @@ const actionColors: Record<string, 'default' | 'destructive' | 'secondary' | 'ou
}
function getEntityLink(entityType: string, entityId: string): string | null {
switch (entityType) {
case 'User':
return `/admin/members/${entityId}`
case 'Project':
return `/admin/projects/${entityId}`
case 'Round':
return `/admin/rounds/${entityId}`
case 'Competition':
return `/admin/competitions`
case 'Evaluation':
case 'EvaluationForm':
return null // no dedicated page
case 'SpecialAward':
return `/admin/awards/${entityId}`
default:
return null
}
}
export default function AuditLogPage() {
// Filter state
const [filters, setFilters] = useState({
@@ -555,14 +576,24 @@ export default function AuditLogPage() {
{formatDate(log.timestamp)}
</TableCell>
<TableCell>
<div>
<p className="font-medium text-sm">
{log.user?.name || 'System'}
</p>
<p className="text-xs text-muted-foreground">
{log.user?.email}
</p>
</div>
{log.userId ? (
<Link
href={`/admin/members/${log.userId}`}
className="group block"
onClick={(e) => e.stopPropagation()}
>
<p className="font-medium text-sm group-hover:text-primary group-hover:underline">
{log.user?.name || 'System'}
</p>
<p className="text-xs text-muted-foreground">
{log.user?.email}
</p>
</Link>
) : (
<div>
<p className="font-medium text-sm">System</p>
</div>
)}
</TableCell>
<TableCell>
<Badge
@@ -574,11 +605,22 @@ export default function AuditLogPage() {
<TableCell>
<div>
<p className="text-sm">{log.entityType}</p>
{log.entityId && (
<p className="text-xs text-muted-foreground font-mono">
{log.entityId.slice(0, 8)}...
</p>
)}
{log.entityId && (() => {
const link = getEntityLink(log.entityType, log.entityId)
return link ? (
<Link
href={link}
className="text-xs text-primary font-mono hover:underline"
onClick={(e) => e.stopPropagation()}
>
{log.entityId.slice(0, 8)}...
</Link>
) : (
<p className="text-xs text-muted-foreground font-mono">
{log.entityId.slice(0, 8)}...
</p>
)
})()}
</div>
</TableCell>
<TableCell className="font-mono text-xs">
@@ -601,9 +643,18 @@ export default function AuditLogPage() {
<p className="text-xs font-medium text-muted-foreground">
Entity ID
</p>
<p className="font-mono text-sm">
{log.entityId || 'N/A'}
</p>
{log.entityId ? (() => {
const link = getEntityLink(log.entityType, log.entityId)
return link ? (
<Link href={link} className="font-mono text-sm text-primary hover:underline" onClick={(e) => e.stopPropagation()}>
{log.entityId}
</Link>
) : (
<p className="font-mono text-sm">{log.entityId}</p>
)
})() : (
<p className="font-mono text-sm">N/A</p>
)}
</div>
<div>
<p className="text-xs font-medium text-muted-foreground">
@@ -700,12 +751,23 @@ export default function AuditLogPage() {
{formatDate(log.timestamp)}
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<User className="h-3 w-3" />
<span className="text-xs">
{log.user?.name || 'System'}
</span>
</div>
{log.userId ? (
<Link
href={`/admin/members/${log.userId}`}
className="flex items-center gap-1 text-muted-foreground hover:text-primary"
onClick={(e) => e.stopPropagation()}
>
<User className="h-3 w-3" />
<span className="text-xs hover:underline">
{log.user?.name || 'System'}
</span>
</Link>
) : (
<div className="flex items-center gap-1 text-muted-foreground">
<User className="h-3 w-3" />
<span className="text-xs">System</span>
</div>
)}
</div>
{isExpanded && (