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 { CircleDot, ClipboardList, Users, CheckCircle2, Calendar, TrendingUp, ArrowRight, Layers, } from 'lucide-react' import { GeographicSummaryCard } from '@/components/charts' import { ProjectLogo } from '@/components/shared/project-logo' import { getCountryName } from '@/lib/countries' import { formatDateOnly, formatEnumLabel, 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

) } 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, ] = await Promise.all([ prisma.round.count({ where: { programId: editionId, status: 'ACTIVE' }, }), prisma.round.count({ where: { programId: editionId }, }), prisma.project.count({ where: { round: { programId: editionId } }, }), prisma.project.count({ where: { round: { 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, include: { _count: { select: { projects: true, assignments: true, }, }, assignments: { select: { evaluation: { select: { status: true } }, }, }, }, }), prisma.project.findMany({ where: { round: { programId: editionId } }, orderBy: { createdAt: 'desc' }, take: 8, select: { id: true, title: true, teamName: true, status: true, country: true, competitionCategory: true, oceanIssue: true, logoKey: true, createdAt: true, submittedAt: true, round: { select: { name: true } }, }, }), prisma.project.groupBy({ by: ['competitionCategory'], where: { round: { programId: editionId } }, _count: true, }), prisma.project.groupBy({ by: ['oceanIssue'], where: { round: { programId: editionId } }, _count: true, }), ]) 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

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

{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.status.replace('_', ' ')}

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