'use client' import Link from 'next/link' import type { Route } from 'next' 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, Waves, Clock, BarChart3, Zap, } 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' import { motion } from 'motion/react' type DashboardContentProps = { editionId: string sessionName: string } function formatEntity(entityType: string | null): string { if (!entityType) return 'record' return entityType .replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/_/g, ' ') .toLowerCase() } function formatAction(action: string, entityType: string | null): string { const entity = formatEntity(entityType) const actionMap: Record = { CREATE: `created a ${entity}`, UPDATE: `updated a ${entity}`, DELETE: `deleted a ${entity}`, IMPORT: `imported ${entity}s`, EXPORT: `exported ${entity} data`, REORDER: `reordered ${entity}s`, LOGIN: 'logged in', LOGIN_SUCCESS: 'logged in', LOGIN_FAILED: 'failed to log in', PASSWORD_SET: 'set their password', PASSWORD_CHANGED: 'changed their password', REQUEST_PASSWORD_RESET: 'requested a password reset', COMPLETE_ONBOARDING: 'completed onboarding', DELETE_OWN_ACCOUNT: 'deleted their account', EVALUATION_SUBMITTED: 'submitted an evaluation', COI_DECLARED: 'declared a conflict of interest', COI_REVIEWED: 'reviewed a COI declaration', REMINDERS_TRIGGERED: 'triggered evaluation reminders', DISCUSSION_COMMENT_ADDED: 'added a discussion comment', DISCUSSION_CLOSED: 'closed a discussion', ASSIGN: `assigned a ${entity}`, BULK_CREATE: `bulk created ${entity}s`, BULK_ASSIGN: 'bulk assigned users', BULK_DELETE: `bulk deleted ${entity}s`, BULK_UPDATE: `bulk updated ${entity}s`, BULK_UPDATE_STATUS: 'bulk updated statuses', APPLY_SUGGESTIONS: 'applied assignment suggestions', ASSIGN_PROJECTS_TO_ROUND: 'assigned projects to round', REMOVE_PROJECTS_FROM_ROUND: 'removed projects from round', ADVANCE_PROJECTS: 'advanced projects to next round', BULK_ASSIGN_TO_ROUND: 'bulk assigned to round', REORDER_ROUNDS: 'reordered rounds', STATUS_CHANGE: `changed ${entity} status`, UPDATE_STATUS: `updated ${entity} status`, ROLE_CHANGED: 'changed a user role', INVITE: 'invited a user', SEND_INVITATION: 'sent an invitation', BULK_SEND_INVITATIONS: 'sent bulk invitations', UPLOAD_FILE: 'uploaded a file', DELETE_FILE: 'deleted a file', REPLACE_FILE: 'replaced a file', FILE_DOWNLOADED: 'downloaded a file', EXECUTE_FILTERING: 'ran project filtering', FINALIZE_FILTERING: 'finalized filtering results', OVERRIDE: `overrode a ${entity} result`, BULK_OVERRIDE: 'bulk overrode filtering results', REINSTATE: 'reinstated a project', BULK_REINSTATE: 'bulk reinstated projects', AI_TAG: 'ran AI tagging', START_AI_TAG_JOB: 'started AI tagging job', EVALUATION_SUMMARY: 'generated an AI summary', AWARD_ELIGIBILITY: 'ran award eligibility check', PROJECT_TAGGING: 'ran project tagging', FILTERING: 'ran AI filtering', MENTOR_MATCHING: 'ran mentor matching', ADD_TAG: 'added a tag', REMOVE_TAG: 'removed a tag', BULK_CREATE_TAGS: 'bulk created tags', MENTOR_ASSIGN: 'assigned a mentor', MENTOR_UNASSIGN: 'unassigned a mentor', MENTOR_AUTO_ASSIGN: 'auto-assigned mentors', MENTOR_BULK_ASSIGN: 'bulk assigned mentors', CREATE_MENTOR_NOTE: 'created a mentor note', COMPLETE_MILESTONE: 'completed a milestone', SEND_MESSAGE: 'sent a message', CREATE_MESSAGE_TEMPLATE: 'created a message template', UPDATE_MESSAGE_TEMPLATE: 'updated a message template', DELETE_MESSAGE_TEMPLATE: 'deleted a message template', CREATE_WEBHOOK: 'created a webhook', UPDATE_WEBHOOK: 'updated a webhook', DELETE_WEBHOOK: 'deleted a webhook', TEST_WEBHOOK: 'tested a webhook', REGENERATE_WEBHOOK_SECRET: 'regenerated a webhook secret', UPDATE_SETTING: 'updated a setting', UPDATE_SETTINGS_BATCH: 'updated settings', UPDATE_NOTIFICATION_PREFERENCES: 'updated notification preferences', UPDATE_DIGEST_SETTINGS: 'updated digest settings', UPDATE_ANALYTICS_SETTINGS: 'updated analytics settings', UPDATE_AUDIT_SETTINGS: 'updated audit settings', UPDATE_LOCALIZATION_SETTINGS: 'updated localization settings', UPDATE_RETENTION_CONFIG: 'updated retention config', START_VOTING: 'started live voting', END_SESSION: 'ended a live voting session', UPDATE_SESSION_CONFIG: 'updated session config', CREATE_ROUND_TEMPLATE: 'created a round template', CREATE_ROUND_TEMPLATE_FROM_ROUND: 'saved round as template', UPDATE_ROUND_TEMPLATE: 'updated a round template', DELETE_ROUND_TEMPLATE: 'deleted a round template', UPDATE_EVALUATION_FORM: 'updated the evaluation form', GRANT_GRACE_PERIOD: 'granted a grace period', UPDATE_GRACE_PERIOD: 'updated a grace period', REVOKE_GRACE_PERIOD: 'revoked a grace period', BULK_GRANT_GRACE_PERIOD: 'bulk granted grace periods', SET_AWARD_WINNER: 'set an award winner', REPORT_GENERATED: 'generated a report', DRAFT_SUBMITTED: 'submitted a draft application', SUBMIT: `submitted a ${entity}`, } if (actionMap[action]) return actionMap[action] return action.toLowerCase().replace(/_/g, ' ') } function getActionIcon(action: string) { switch (action) { case 'CREATE': case 'BULK_CREATE': return case 'UPDATE': case 'UPDATE_STATUS': case 'BULK_UPDATE': case 'BULK_UPDATE_STATUS': case 'STATUS_CHANGE': case 'ROLE_CHANGED': return case 'DELETE': case 'BULK_DELETE': return case 'LOGIN': case 'LOGIN_SUCCESS': case 'LOGIN_FAILED': case 'PASSWORD_SET': case 'PASSWORD_CHANGED': case 'COMPLETE_ONBOARDING': return case 'EXPORT': case 'REPORT_GENERATED': return case 'SUBMIT': case 'EVALUATION_SUBMITTED': case 'DRAFT_SUBMITTED': return case 'ASSIGN': case 'BULK_ASSIGN': case 'APPLY_SUGGESTIONS': case 'ASSIGN_PROJECTS_TO_ROUND': case 'MENTOR_ASSIGN': case 'MENTOR_BULK_ASSIGN': return case 'INVITE': case 'SEND_INVITATION': case 'BULK_SEND_INVITATIONS': return case 'IMPORT': return default: return } } export function DashboardContent({ editionId, sessionName }: DashboardContentProps) { const { data, isLoading, error } = trpc.dashboard.getStats.useQuery( { editionId }, { enabled: !!editionId, retry: 1, refetchInterval: 30_000 } ) if (isLoading) { return } if (error) { return (

Failed to load dashboard

{error.message || 'An unexpected error occurred. Please try refreshing the page.'}

) } 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 const roundsWithEvalStats = recentRounds.map((round: typeof recentRounds[number]) => { const submitted = round.assignments.filter( (a: { evaluation: { status: string } | null }) => 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 } }) const now = new Date() const deadlines: { label: string; roundName: string; date: Date }[] = [] for (const round of recentRounds) { if (round.windowCloseAt && new Date(round.windowCloseAt) > now) { deadlines.push({ label: 'Window closes', roundName: round.name, date: new Date(round.windowCloseAt), }) } } deadlines.sort((a, b) => a.date.getTime() - b.date.getTime()) const upcomingDeadlines = deadlines.slice(0, 4) 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) const pendingTotal = pendingCOIs + unassignedProjects + draftRounds const summaryParts: string[] = [] if (activeRoundCount > 0) summaryParts.push(`${activeRoundCount} round${activeRoundCount !== 1 ? 's' : ''} active`) if (totalAssignments - submittedCount > 0) summaryParts.push(`${totalAssignments - submittedCount} evaluations pending`) if (pendingTotal > 0) summaryParts.push(`${pendingTotal} action${pendingTotal !== 1 ? 's' : ''} needed`) return ( <> {/* ── Header Banner ── */} {/* Decorative background pattern */}

Welcome back, {sessionName}

{edition.name} {edition.year} — Command Center

{summaryParts.length > 0 && (

{summaryParts.join(' \u00b7 ')}

)}
{/* ── Stats Row ── */}

Rounds

{totalRoundCount}

{activeRoundCount} active

Projects

{projectCount}

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

Jury

{totalJurors}

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

Evaluations

{submittedCount} /{totalAssignments}

{completionRate.toFixed(0)}% complete

0 ? 'border-l-amber-500' : 'border-l-emerald-400'}`}>

Pending

{pendingTotal}

0 ? 'text-amber-600' : 'text-emerald-600'}`}> {pendingTotal > 0 ? 'Actions needed' : 'All clear'}

0 ? 'bg-amber-500/10' : 'bg-emerald-400/10'}`}> {pendingTotal > 0 ? : }
{/* ── Main Two-Column Layout ── */}
{/* Left Column (2/3) */}
{/* Active Rounds */}
Active Rounds {edition.name} — {roundsWithEvalStats.length} round{roundsWithEvalStats.length !== 1 ? 's' : ''}
All rounds
{roundsWithEvalStats.length === 0 ? (

No rounds created yet

) : (
{roundsWithEvalStats.map((round: typeof roundsWithEvalStats[number], idx: number) => (

{round.name}

{round._count.projectRoundStates} projects {round._count.assignments} assignments {round.windowOpenAt && round.windowCloseAt && ( {formatDateOnly(round.windowOpenAt)} – {formatDateOnly(round.windowCloseAt)} )}
{round.totalEvals > 0 && (

{round.evalPercent}%

evaluated

)}
{round.totalEvals > 0 && (

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

)}
))}
)}
{/* Latest Projects */}
Recent Projects Latest submissions
All projects
{latestProjects.length === 0 ? (

No projects submitted yet

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

{truncate(project.title, 50)}

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

))}
)}
{/* Right Column (1/3) */}
{/* Action Required */} 0 ? 'border-amber-200/60' : ''}>
0 ? 'bg-amber-500/10' : 'bg-emerald-500/10'}`}> {pendingTotal > 0 ? : }
{pendingTotal > 0 ? 'Action Required' : 'All Clear'}
{pendingCOIs > 0 && (
COI declarations
{pendingCOIs} )} {unassignedProjects > 0 && (
Unassigned projects
{unassignedProjects} )} {draftRounds > 0 && (
Draft rounds
{draftRounds} )} {pendingTotal === 0 && (

All caught up!

No pending actions

)}
{/* Evaluation Progress */}
Eval Progress
{roundsWithEvalStats.filter((s: typeof roundsWithEvalStats[number]) => s.status !== 'ROUND_DRAFT' && s.totalEvals > 0).length === 0 ? (

No evaluations yet

) : (
{roundsWithEvalStats .filter((r: typeof roundsWithEvalStats[number]) => r.status !== 'ROUND_DRAFT' && r.totalEvals > 0) .map((round: typeof roundsWithEvalStats[number]) => (

{round.name}

{round.evalPercent}%

{round.submittedEvals}/{round.totalEvals} submitted

))}
)}
{/* Upcoming Deadlines */} {upcomingDeadlines.length > 0 && (
Deadlines
{upcomingDeadlines.map((deadline, i) => { const days = daysUntil(deadline.date) const isUrgent = days <= 7 return (

{deadline.roundName}

{formatDateOnly(deadline.date)} · {days}d remaining

) })}
)} {/* Activity Feed */}
Activity
{recentActivity.length === 0 ? (

No recent activity

) : (
{/* Timeline line */}
{recentActivity.map((log, idx) => (
{getActionIcon(log.action)}

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

{formatRelativeTime(log.timestamp)}

))}
)}
{/* ── Bottom Full Width Section ── */}
{/* Geographic Distribution */}
{/* Category & Issue Breakdown */}
Categories
{categories.length === 0 && issues.length === 0 ? (

No category data

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

Competition Type

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

Top Issues

{issues.map((issue) => (
{issue.label} {issue.count}
))}
)}
)}
) } function DashboardSkeleton() { return ( <> {/* Header skeleton */} {/* Stats row skeleton */}
{[...Array(5)].map((_, i) => ( ))}
{/* Two-column content skeleton */}
{[...Array(3)].map((_, i) => ( ))}
{[...Array(5)].map((_, i) => ( ))}
{[...Array(4)].map((_, i) => (
{[...Array(3)].map((_, j) => ( ))}
))}
{/* Bottom skeleton */}
) }