import type { Metadata } from 'next' import { Suspense } from 'react' import { auth } from '@/lib/auth' import { prisma } from '@/lib/prisma' import Link from 'next/link' export const metadata: Metadata = { title: 'Admin Dashboard' } export const dynamic = 'force-dynamic' 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 DashboardStatsProps = { editionId: string | null sessionName: string } const statusColors: Record = { SUBMITTED: 'secondary', ELIGIBLE: 'default', ASSIGNED: 'default', UNDER_REVIEW: 'default', SHORTLISTED: 'success', SEMIFINALIST: 'success', FINALIST: 'success', WINNER: 'success', REJECTED: 'destructive', WITHDRAWN: 'secondary', } async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) { if (!editionId) { return (

No edition selected

Select an edition from the sidebar to view dashboard

) } try { const edition = await prisma.program.findUnique({ where: { id: editionId }, select: { name: true, year: true }, }) if (!edition) { return (

Edition not found

The selected edition could not be found

) } const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) const [ activeRoundCount, totalRoundCount, projectCount, newProjectsThisWeek, totalJurors, activeJurors, evaluationStats, totalAssignments, recentRounds, latestProjects, categoryBreakdown, oceanIssueBreakdown, recentActivity, pendingCOIs, draftRounds, unassignedProjects, ] = await Promise.all([ prisma.round.count({ where: { programId: editionId, status: 'ACTIVE' }, }), prisma.round.count({ where: { programId: editionId }, }), prisma.project.count({ where: { programId: editionId }, }), prisma.project.count({ where: { programId: editionId, createdAt: { gte: sevenDaysAgo }, }, }), prisma.user.count({ where: { role: 'JURY_MEMBER', status: { in: ['ACTIVE', 'INVITED'] }, assignments: { some: { round: { programId: editionId } } }, }, }), prisma.user.count({ where: { role: 'JURY_MEMBER', status: 'ACTIVE', assignments: { some: { round: { programId: editionId } } }, }, }), prisma.evaluation.groupBy({ by: ['status'], where: { assignment: { round: { programId: editionId } } }, _count: true, }), prisma.assignment.count({ where: { round: { programId: editionId } }, }), prisma.round.findMany({ where: { programId: editionId }, orderBy: { createdAt: 'desc' }, take: 5, select: { id: true, name: true, status: true, votingStartAt: true, votingEndAt: true, submissionEndDate: true, _count: { select: { projects: true, assignments: true, }, }, assignments: { select: { evaluation: { select: { status: true } }, }, }, }, }), prisma.project.findMany({ where: { programId: editionId }, orderBy: { createdAt: 'desc' }, take: 8, select: { id: true, title: true, teamName: true, country: true, competitionCategory: true, oceanIssue: true, logoKey: true, createdAt: true, submittedAt: true, status: true, round: { select: { name: true } }, }, }), prisma.project.groupBy({ by: ['competitionCategory'], where: { programId: editionId }, _count: true, }), prisma.project.groupBy({ by: ['oceanIssue'], where: { programId: editionId }, _count: true, }), // Recent activity feed (scoped to last 7 days for performance) prisma.auditLog.findMany({ where: { timestamp: { gte: sevenDaysAgo }, }, orderBy: { timestamp: 'desc' }, take: 8, select: { id: true, action: true, entityType: true, timestamp: true, user: { select: { name: true } }, }, }), // Pending COI declarations (hasConflict declared but not yet reviewed) prisma.conflictOfInterest.count({ where: { hasConflict: true, reviewedAt: null, assignment: { round: { programId: editionId } }, }, }), // Draft rounds needing activation prisma.round.count({ where: { programId: editionId, status: 'DRAFT' }, }), // Projects without assignments in active rounds prisma.project.count({ where: { programId: editionId, round: { status: 'ACTIVE' }, assignments: { none: {} }, }, }), ]) 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) // Helper: human-readable action descriptions for audit log 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}` } // Helper: pick an icon for an audit action 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 } } 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) */} ) } catch (err) { console.error('Dashboard data load failed:', err) return (

Dashboard temporarily unavailable

Could not load dashboard data. Please refresh the page.

) } } 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 */} ) } type PageProps = { searchParams: Promise<{ edition?: string }> } export default async function AdminDashboardPage({ searchParams }: PageProps) { const [session, params] = await Promise.all([ auth(), searchParams, ]) let editionId = params.edition || null if (!editionId) { const defaultEdition = await prisma.program.findFirst({ where: { status: 'ACTIVE' }, orderBy: { year: 'desc' }, select: { id: true }, }) editionId = defaultEdition?.id || null if (!editionId) { const anyEdition = await prisma.program.findFirst({ orderBy: { year: 'desc' }, select: { id: true }, }) editionId = anyEdition?.id || null } } const sessionName = session?.user?.name || 'Admin' return (
}>
) }