'use client' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { Button } from '@/components/ui/button' import { CircleDot, ClipboardList, Users, CheckCircle2, Calendar, TrendingUp, ArrowRight, Layers, Activity, AlertTriangle, ShieldAlert, Plus, Upload, UserPlus, FileEdit, LogIn, Send, Eye, Trash2, } from 'lucide-react' import { GeographicSummaryCard } from '@/components/charts' import { AnimatedCard } from '@/components/shared/animated-container' import { StatusBadge } from '@/components/shared/status-badge' import { ProjectLogo } from '@/components/shared/project-logo' import { getCountryName } from '@/lib/countries' import { formatDateOnly, formatEnumLabel, formatRelativeTime, truncate, daysUntil, } from '@/lib/utils' type DashboardContentProps = { editionId: string sessionName: string } function formatAction(action: string, entityType: string | null): string { const entity = entityType?.toLowerCase() || 'record' const actionMap: Record = { CREATE: `created a ${entity}`, UPDATE: `updated a ${entity}`, DELETE: `deleted a ${entity}`, LOGIN: 'logged in', EXPORT: `exported ${entity} data`, SUBMIT: `submitted an ${entity}`, ASSIGN: `assigned a ${entity}`, INVITE: `invited a user`, STATUS_CHANGE: `changed ${entity} status`, BULK_UPDATE: `bulk updated ${entity}s`, IMPORT: `imported ${entity}s`, } return actionMap[action] || `${action.toLowerCase()} ${entity}` } function getActionIcon(action: string) { switch (action) { case 'CREATE': return case 'UPDATE': return case 'DELETE': return case 'LOGIN': return case 'EXPORT': return case 'SUBMIT': return case 'ASSIGN': return case 'INVITE': return default: return } } export function DashboardContent({ editionId, sessionName }: DashboardContentProps) { const { data, isLoading } = trpc.dashboard.getStats.useQuery( { editionId }, { enabled: !!editionId } ) if (isLoading) { return } if (!data) { return (

Edition not found

The selected edition could not be found

) } const { edition, activeRoundCount, totalRoundCount, projectCount, newProjectsThisWeek, totalJurors, activeJurors, evaluationStats, totalAssignments, recentRounds, latestProjects, categoryBreakdown, oceanIssueBreakdown, recentActivity, pendingCOIs, draftRounds, unassignedProjects, } = data const submittedCount = evaluationStats.find((e) => e.status === 'SUBMITTED')?._count || 0 const draftCount = evaluationStats.find((e) => e.status === 'DRAFT')?._count || 0 const totalEvaluations = submittedCount + draftCount const completionRate = totalEvaluations > 0 ? (submittedCount / totalEvaluations) * 100 : 0 const invitedJurors = totalJurors - activeJurors // Compute per-round eval stats const roundsWithEvalStats = recentRounds.map((round) => { const submitted = round.assignments.filter( (a) => a.evaluation?.status === 'SUBMITTED' ).length const total = round._count.assignments const percent = total > 0 ? Math.round((submitted / total) * 100) : 0 return { ...round, submittedEvals: submitted, totalEvals: total, evalPercent: percent } }) // Upcoming deadlines from rounds const now = new Date() const deadlines: { label: string; roundName: string; date: Date }[] = [] for (const round of recentRounds) { if (round.votingEndAt && new Date(round.votingEndAt) > now) { deadlines.push({ label: 'Voting closes', roundName: round.name, date: new Date(round.votingEndAt), }) } if (round.submissionEndDate && new Date(round.submissionEndDate) > now) { deadlines.push({ label: 'Submissions close', roundName: round.name, date: new Date(round.submissionEndDate), }) } } deadlines.sort((a, b) => a.date.getTime() - b.date.getTime()) const upcomingDeadlines = deadlines.slice(0, 4) // Category/issue bars const categories = categoryBreakdown .filter((c) => c.competitionCategory !== null) .map((c) => ({ label: formatEnumLabel(c.competitionCategory!), count: c._count, })) .sort((a, b) => b.count - a.count) const issues = oceanIssueBreakdown .filter((i) => i.oceanIssue !== null) .map((i) => ({ label: formatEnumLabel(i.oceanIssue!), count: i._count, })) .sort((a, b) => b.count - a.count) .slice(0, 5) const maxCategoryCount = Math.max(...categories.map((c) => c.count), 1) const maxIssueCount = Math.max(...issues.map((i) => i.count), 1) return ( <> {/* Header */}

Dashboard

Welcome back, {sessionName} — {edition.name} {edition.year}

{/* Stats Grid */}
Rounds
{totalRoundCount}

{activeRoundCount} active round{activeRoundCount !== 1 ? 's' : ''}

Projects
{projectCount}

{newProjectsThisWeek > 0 ? `${newProjectsThisWeek} new this week` : 'In this edition'}

Jury Members
{totalJurors}

{activeJurors} active{invitedJurors > 0 && `, ${invitedJurors} invited`}

Evaluations
{submittedCount} {totalAssignments > 0 && ( {' '}/ {totalAssignments} )}

{completionRate.toFixed(0)}% completion rate

{/* Quick Actions */}
{/* Two-Column Content */}
{/* Left Column */}
{/* Rounds Card (enhanced) */}
Rounds Voting rounds in {edition.name}
View all
{roundsWithEvalStats.length === 0 ? (

No rounds created yet

Create your first round
) : (
{roundsWithEvalStats.map((round) => (

{round.name}

{round._count.projects} projects · {round._count.assignments} assignments {round.totalEvals > 0 && ( <> · {round.evalPercent}% evaluated )}

{round.votingStartAt && round.votingEndAt && (

Voting: {formatDateOnly(round.votingStartAt)} – {formatDateOnly(round.votingEndAt)}

)}
{round.totalEvals > 0 && ( )}
))}
)}
{/* Latest Projects Card */}
Latest Projects Recently submitted projects
View all
{latestProjects.length === 0 ? (

No projects submitted yet

) : (
{latestProjects.map((project) => (

{truncate(project.title, 45)}

{[ project.teamName, project.country ? getCountryName(project.country) : null, formatDateOnly(project.submittedAt || project.createdAt), ] .filter(Boolean) .join(' \u00b7 ')}

{(project.competitionCategory || project.oceanIssue) && (

{[ project.competitionCategory ? formatEnumLabel(project.competitionCategory) : null, project.oceanIssue ? formatEnumLabel(project.oceanIssue) : null, ] .filter(Boolean) .join(' \u00b7 ')}

)}
))}
)}
{/* Right Column */}
{/* Pending Actions Card */} Pending Actions
{pendingCOIs > 0 && (
COI declarations to review
{pendingCOIs} )} {unassignedProjects > 0 && (
Projects without assignments
{unassignedProjects} )} {draftRounds > 0 && (
Draft rounds to activate
{draftRounds} )} {pendingCOIs === 0 && unassignedProjects === 0 && draftRounds === 0 && (

All caught up!

)}
{/* Evaluation Progress Card */} Evaluation Progress {roundsWithEvalStats.filter((r) => r.status !== 'DRAFT' && r.totalEvals > 0).length === 0 ? (

No evaluations in progress

) : (
{roundsWithEvalStats .filter((r) => r.status !== 'DRAFT' && r.totalEvals > 0) .map((round) => (

{round.name}

{round.evalPercent}%

{round.submittedEvals} of {round.totalEvals} evaluations submitted

))}
)}
{/* Category Breakdown Card */} Project Categories {categories.length === 0 && issues.length === 0 ? (

No category data available

) : (
{categories.length > 0 && (

By Type

{categories.map((cat) => (
{cat.label} {cat.count}
))}
)} {issues.length > 0 && (

Top Issues

{issues.map((issue) => (
{issue.label} {issue.count}
))}
)}
)} {/* Recent Activity Card */} Recent Activity {recentActivity.length === 0 ? (

No recent activity

) : (
{recentActivity.map((log) => (
{getActionIcon(log.action)}

{log.user?.name || 'System'} {' '}{formatAction(log.action, log.entityType)}

{formatRelativeTime(log.timestamp)}

))}
)}
{/* Upcoming Deadlines Card */} Upcoming Deadlines {upcomingDeadlines.length === 0 ? (

No upcoming deadlines

) : (
{upcomingDeadlines.map((deadline, i) => { const days = daysUntil(deadline.date) const isUrgent = days <= 7 return (

{deadline.label} — {deadline.roundName}

{formatDateOnly(deadline.date)} · in {days} day{days !== 1 ? 's' : ''}

) })}
)}
{/* Geographic Distribution (full width, at the bottom) */} ) } function DashboardSkeleton() { return ( <> {/* Header skeleton */}
{/* Stats grid skeleton */}
{[...Array(4)].map((_, i) => ( ))}
{/* Two-column content skeleton */}
{[...Array(3)].map((_, i) => ( ))}
{[...Array(5)].map((_, i) => ( ))}
{[...Array(2)].map((_, i) => ( ))}
{[...Array(4)].map((_, i) => ( ))}
{[...Array(2)].map((_, i) => ( ))}
{/* Map skeleton */} ) }