feat: audit log clickable links, communication hub recipient details & link options
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user