import type { Metadata } from 'next' import { Suspense } from 'react' import Link from 'next/link' import { auth } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { AutoRefresh } from '@/components/shared/auto-refresh' export const metadata: Metadata = { title: 'Jury Dashboard' } export const dynamic = 'force-dynamic' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { ClipboardList, CheckCircle2, Clock, ArrowRight, GitCompare, Zap, BarChart3, Waves, Send, } from 'lucide-react' import { formatDateOnly } from '@/lib/utils' import { CountdownTimer } from '@/components/shared/countdown-timer' import { AnimatedCard } from '@/components/shared/animated-container' import { JuryPreferencesBanner } from '@/components/jury/preferences-banner' import { cn } from '@/lib/utils' function getGreeting(): string { const hour = new Date().getHours() if (hour < 12) return 'Good morning' if (hour < 18) return 'Good afternoon' return 'Good evening' } async function JuryDashboardContent() { const session = await auth() const userId = session?.user?.id if (!userId) { return null } // Get assignments, grace periods, and feature flags in parallel const [assignments, gracePeriods, compareFlag] = await Promise.all([ prisma.assignment.findMany({ where: { userId }, include: { project: { select: { id: true, title: true, teamName: true, country: true, }, }, round: { select: { id: true, name: true, status: true, windowOpenAt: true, windowCloseAt: true, competition: { select: { program: { select: { name: true, year: true, }, }, }, }, }, }, evaluation: { select: { id: true, status: true, submittedAt: true, criterionScoresJson: true, form: { select: { criteriaJson: true }, }, }, }, }, orderBy: [ { round: { windowCloseAt: 'asc' } }, { createdAt: 'asc' }, ], }), prisma.gracePeriod.findMany({ where: { userId, extendedUntil: { gte: new Date() }, }, select: { roundId: true, extendedUntil: true, }, }), prisma.systemSettings.findUnique({ where: { key: 'jury_compare_enabled' } }), ]) const juryCompareEnabled = compareFlag?.value === 'true' // Calculate stats const totalAssignments = assignments.length const completedAssignments = assignments.filter( (a) => a.evaluation?.status === 'SUBMITTED' ).length const inProgressAssignments = assignments.filter( (a) => a.evaluation?.status === 'DRAFT' ).length const pendingAssignments = totalAssignments - completedAssignments - inProgressAssignments const completionRate = totalAssignments > 0 ? (completedAssignments / totalAssignments) * 100 : 0 // Group assignments by round const assignmentsByRound = assignments.reduce( (acc, assignment) => { const roundId = assignment.round.id if (!acc[roundId]) { acc[roundId] = { round: assignment.round, assignments: [], } } acc[roundId].assignments.push(assignment) return acc }, {} as Record ) const graceByRound = new Map() for (const gp of gracePeriods) { const existing = graceByRound.get(gp.roundId) if (!existing || gp.extendedUntil > existing) { graceByRound.set(gp.roundId, gp.extendedUntil) } } // Active rounds (voting window open) const now = new Date() const activeRounds = Object.values(assignmentsByRound).filter( ({ round }) => round.status === 'ROUND_ACTIVE' && round.windowOpenAt && round.windowCloseAt && new Date(round.windowOpenAt) <= now && new Date(round.windowCloseAt) >= now ) // Find next unevaluated assignment in an active round const nextUnevaluated = assignments.find((a) => { const isActive = a.round.status === 'ROUND_ACTIVE' && a.round.windowOpenAt && a.round.windowCloseAt && new Date(a.round.windowOpenAt) <= now && new Date(a.round.windowCloseAt) >= now const isIncomplete = !a.evaluation || a.evaluation.status === 'NOT_STARTED' || a.evaluation.status === 'DRAFT' return isActive && isIncomplete }) // Recent assignments for the quick list (latest 5) const recentAssignments = assignments.slice(0, 6) // Get active round remaining count const activeRemaining = assignments.filter((a) => { const isActive = a.round.status === 'ROUND_ACTIVE' && a.round.windowOpenAt && a.round.windowCloseAt && new Date(a.round.windowOpenAt) <= now && new Date(a.round.windowCloseAt) >= now const isIncomplete = !a.evaluation || a.evaluation.status !== 'SUBMITTED' return isActive && isIncomplete }).length const stats = [ { value: totalAssignments, label: 'Assigned', detail: 'Total projects', accent: 'text-brand-blue', }, { value: completedAssignments, label: 'Completed', detail: `${completionRate.toFixed(0)}% done`, accent: 'text-emerald-600', }, { value: inProgressAssignments, label: 'In draft', detail: inProgressAssignments > 0 ? 'Work in progress' : 'None started', accent: inProgressAssignments > 0 ? 'text-amber-600' : 'text-emerald-600', }, { value: pendingAssignments, label: 'Pending', detail: pendingAssignments > 0 ? 'Not yet started' : 'All started', accent: pendingAssignments > 0 ? 'text-amber-600' : 'text-emerald-600', }, ] // Zero-assignment state: compact welcome card if (totalAssignments === 0) { return (

No assignments yet

Your project assignments will appear here once an administrator assigns them to you.

All Assignments

View evaluations

{juryCompareEnabled && (

Compare Projects

Side-by-side view

)}
) } return ( <> {/* Hero CTA - Jump to next evaluation */} {nextUnevaluated && activeRemaining > 0 && (

{activeRemaining} evaluation{activeRemaining > 1 ? 's' : ''} remaining

Continue with "{nextUnevaluated.project.title}"

)} {/* Stats — editorial strip */} {/* Mobile: compact horizontal data strip */}
{stats.map((s, i) => (
0 ? 'border-l border-border/50' : ''}`}> {s.value}

{s.label}

))}
{/* Desktop: editorial stat row */}
{stats.map((s, i) => (
{s.value}

{s.label}

{s.detail}

))}
{/* Main content -- two column layout */}
{/* Left column */}
{/* Recent Assignments */}
My Assignments
{recentAssignments.length > 0 ? (
{recentAssignments.map((assignment, idx) => { const evaluation = assignment.evaluation const isCompleted = evaluation?.status === 'SUBMITTED' const isDraft = evaluation?.status === 'DRAFT' const isVotingOpen = assignment.round.status === 'ROUND_ACTIVE' return (

{assignment.project.title}

{assignment.project.teamName} {assignment.round.name}
{isCompleted ? ( Done ) : isDraft && isVotingOpen ? ( Ready to submit ) : isDraft ? ( Draft ) : ( Pending )} {isCompleted ? ( ) : isVotingOpen && isDraft ? ( ) : isVotingOpen ? ( ) : ( )}
) })}
) : (

No assignments yet

Assignments will appear here once an administrator assigns projects to you.

)}
{/* Quick Actions */}
Quick Actions

All Assignments

View and manage evaluations

{juryCompareEnabled && (

Compare Projects

Side-by-side comparison

)}
{/* Right column */}
{/* Active Rounds */} {activeRounds.length > 0 && (
Active Voting Stages Stages currently open for evaluation
{activeRounds.map(({ round, assignments: roundAssignments }: { round: (typeof assignments)[0]['round']; assignments: typeof assignments }) => { const roundCompleted = roundAssignments.filter( (a: typeof assignments[0]) => a.evaluation?.status === 'SUBMITTED' ).length const roundTotal = roundAssignments.length const roundProgress = roundTotal > 0 ? (roundCompleted / roundTotal) * 100 : 0 const isAlmostDone = roundProgress >= 80 const deadline = graceByRound.get(round.id) ?? (round.windowCloseAt ? new Date(round.windowCloseAt) : null) const isUrgent = deadline && (deadline.getTime() - now.getTime()) < 24 * 60 * 60 * 1000 const program = round.competition.program return (

{round.name}

{program.name} · {program.year}

{isAlmostDone ? ( Almost done ) : ( Active )}
Progress {roundCompleted}/{roundTotal}
{deadline && (
{round.windowCloseAt && ( ({formatDateOnly(round.windowCloseAt)}) )}
)}
) })} )} {/* No active stages */} {activeRounds.length === 0 && (

No active voting stages

Check back later when a voting window opens

)} {/* Completion Summary by Round */} {Object.keys(assignmentsByRound).length > 0 && (
Stage Summary
{Object.values(assignmentsByRound).map(({ round, assignments: roundAssignments }: { round: (typeof assignments)[0]['round']; assignments: typeof assignments }) => { const done = roundAssignments.filter((a: typeof assignments[0]) => a.evaluation?.status === 'SUBMITTED').length const total = roundAssignments.length const pct = total > 0 ? Math.round((done / total) * 100) : 0 return (
{round.name}
{pct}% ({done}/{total})
) })} )}
) } function DashboardSkeleton() { return ( <> {/* Stats skeleton */}
{[...Array(4)].map((_, i) => (
0 ? 'border-l border-border/50' : ''}`}>
))}
{[...Array(4)].map((_, i) => (
))}
{/* Two-column skeleton */}
{[...Array(4)].map((_, i) => (
))}
{[...Array(2)].map((_, i) => (
))}
) } export default async function JuryDashboardPage() { const session = await auth() return (
{/* Header */}

{getGreeting()}, {session?.user?.name || 'Juror'}

Here's an overview of your evaluation progress

{/* Preferences banner (shown when juror has unconfirmed preferences) */} {/* Content */} }> {/* Auto-refresh every 30s so voting round changes appear promptly */}
) }