'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 formatEntity(entityType: string | null): string { if (!entityType) return 'record' // Insert space before uppercase letters (PascalCase → words), then lowercase 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 = { // Generic CRUD CREATE: `created a ${entity}`, UPDATE: `updated a ${entity}`, DELETE: `deleted a ${entity}`, IMPORT: `imported ${entity}s`, EXPORT: `exported ${entity} data`, REORDER: `reordered ${entity}s`, // Auth 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', // Evaluations 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', // Assignments 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 STATUS_CHANGE: `changed ${entity} status`, UPDATE_STATUS: `updated ${entity} status`, ROLE_CHANGED: 'changed a user role', // Invitations INVITE: 'invited a user', SEND_INVITATION: 'sent an invitation', BULK_SEND_INVITATIONS: 'sent bulk invitations', // Files UPLOAD_FILE: 'uploaded a file', DELETE_FILE: 'deleted a file', REPLACE_FILE: 'replaced a file', FILE_DOWNLOADED: 'downloaded a file', // Filtering 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 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', // Tags ADD_TAG: 'added a tag', REMOVE_TAG: 'removed a tag', BULK_CREATE_TAGS: 'bulk created tags', // Mentor 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', // Messages & Webhooks 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', // Settings 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', // Live Voting START_VOTING: 'started live voting', END_SESSION: 'ended a live voting session', UPDATE_SESSION_CONFIG: 'updated session config', // Round Templates 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', // Grace Period 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', // Awards SET_AWARD_WINNER: 'set an award winner', // Reports & Applications REPORT_GENERATED: 'generated a report', DRAFT_SUBMITTED: 'submitted a draft application', SUBMIT: `submitted a ${entity}`, } if (actionMap[action]) return actionMap[action] // Fallback: convert ACTION_NAME to readable text 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 } ) 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, activeStageCount, totalStageCount, projectCount, newProjectsThisWeek, totalJurors, activeJurors, evaluationStats, totalAssignments, recentStages, latestProjects, categoryBreakdown, oceanIssueBreakdown, recentActivity, pendingCOIs, draftStages, 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-stage eval stats const stagesWithEvalStats = recentStages.map((stage: typeof recentStages[number]) => { const submitted = stage.assignments.filter( (a: { evaluation: { status: string } | null }) => a.evaluation?.status === 'SUBMITTED' ).length const total = stage._count.assignments const percent = total > 0 ? Math.round((submitted / total) * 100) : 0 return { ...stage, submittedEvals: submitted, totalEvals: total, evalPercent: percent } }) // Upcoming deadlines from stages const now = new Date() const deadlines: { label: string; stageName: string; date: Date }[] = [] for (const stage of recentStages) { if (stage.windowCloseAt && new Date(stage.windowCloseAt) > now) { deadlines.push({ label: 'Window closes', stageName: stage.name, date: new Date(stage.windowCloseAt), }) } } 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 */}

Stages

{totalStageCount}

{activeStageCount} active stage{activeStageCount !== 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 */}

Pipelines

Manage stages & pipelines

Import Projects

Upload a CSV file

Invite Jury

Add jury members

{/* Two-Column Content */}
{/* Left Column */}
{/* Stages Card (enhanced) */}
Stages
Pipeline stages in {edition.name}
View all
{stagesWithEvalStats.length === 0 ? (

No stages created yet

Set up your pipeline
) : (
{stagesWithEvalStats.map((stage) => (

{stage.name}

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

{stage.windowOpenAt && stage.windowCloseAt && (

Window: {formatDateOnly(stage.windowOpenAt)} – {formatDateOnly(stage.windowCloseAt)}

)}
{stage.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} )} {draftStages > 0 && (
Draft stages to activate
{draftStages} )} {pendingCOIs === 0 && unassignedProjects === 0 && draftStages === 0 && (

All caught up!

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

No evaluations in progress

) : (
{stagesWithEvalStats .filter((s: typeof stagesWithEvalStats[number]) => s.status !== 'STAGE_DRAFT' && s.totalEvals > 0) .map((stage: typeof stagesWithEvalStats[number]) => (

{stage.name}

{stage.evalPercent}%

{stage.submittedEvals} of {stage.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.stageName}

{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 */} ) }