'use client' import { Suspense, use, useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Separator } from '@/components/ui/separator' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { ArrowLeft, Edit, Users, FileText, CheckCircle2, Clock, AlertCircle, Archive, Play, Pause, BarChart3, Upload, Filter, Trash2, Loader2, Plus, ArrowRightCircle, Minus, XCircle, AlertTriangle, ListChecks, ClipboardCheck, FileSearch, LayoutTemplate, ShieldCheck, Download, RotateCcw, Zap, QrCode, ExternalLink, } from 'lucide-react' import { toast } from 'sonner' import { ROUND_FIELD_VISIBILITY, roundTypeLabels } from '@/types/round-settings' import { AnimatedCard } from '@/components/shared/animated-container' import { AssignProjectsDialog } from '@/components/admin/assign-projects-dialog' import { AdvanceProjectsDialog } from '@/components/admin/advance-projects-dialog' import { RemoveProjectsDialog } from '@/components/admin/remove-projects-dialog' import { Pagination } from '@/components/shared/pagination' import { CsvExportDialog } from '@/components/shared/csv-export-dialog' import { format, formatDistanceToNow, isFuture } from 'date-fns' const OUTCOME_BADGES: Record< string, { variant: 'default' | 'destructive' | 'secondary' | 'outline'; icon: React.ReactNode; label: string } > = { PASSED: { variant: 'default', icon: , label: 'Passed', }, FILTERED_OUT: { variant: 'destructive', icon: , label: 'Filtered Out', }, FLAGGED: { variant: 'secondary', icon: , label: 'Flagged', }, } interface PageProps { params: Promise<{ id: string }> } function RoundDetailContent({ roundId }: { roundId: string }) { const router = useRouter() const [assignOpen, setAssignOpen] = useState(false) const [advanceOpen, setAdvanceOpen] = useState(false) const [removeOpen, setRemoveOpen] = useState(false) const [activeJobId, setActiveJobId] = useState(null) const [jobPollInterval, setJobPollInterval] = useState(2000) // Inline filtering results state const [outcomeFilter, setOutcomeFilter] = useState('') const [resultsPage, setResultsPage] = useState(1) const [expandedRows, setExpandedRows] = useState>(new Set()) const [overrideDialog, setOverrideDialog] = useState<{ id: string currentOutcome: string } | null>(null) const [overrideOutcome, setOverrideOutcome] = useState('PASSED') const [overrideReason, setOverrideReason] = useState('') const [showExportDialog, setShowExportDialog] = useState(false) const { data: round, isLoading, refetch: refetchRound } = trpc.round.get.useQuery({ id: roundId }) // Progress data is now included in round.get response (eliminates duplicate evaluation.groupBy) const progress = round?.progress // Check round type const isFilteringRound = round?.roundType === 'FILTERING' const isLiveEventRound = round?.roundType === 'LIVE_EVENT' // Filtering queries (only fetch for FILTERING rounds) const { data: filteringStats, isLoading: isLoadingFilteringStats, refetch: refetchFilteringStats } = trpc.filtering.getResultStats.useQuery( { roundId }, { enabled: isFilteringRound, staleTime: 30_000 } ) const { data: filteringRules } = trpc.filtering.getRules.useQuery( { roundId }, { enabled: isFilteringRound } ) const { data: aiStatus } = trpc.filtering.checkAIStatus.useQuery( { roundId }, { enabled: isFilteringRound } ) // Live voting session (only fetch for LIVE_EVENT rounds) const { data: liveSession } = trpc.liveVoting.getSession.useQuery( { roundId }, { enabled: isLiveEventRound, staleTime: 30_000 } ) const { data: latestJob, refetch: refetchLatestJob } = trpc.filtering.getLatestJob.useQuery( { roundId }, { enabled: isFilteringRound, staleTime: 30_000 } ) // Poll for job status with exponential backoff (2s โ†’ 4s โ†’ 8s โ†’ 15s cap) const { data: jobStatus } = trpc.filtering.getJobStatus.useQuery( { jobId: activeJobId! }, { enabled: !!activeJobId, refetchInterval: activeJobId ? jobPollInterval : false, refetchIntervalInBackground: false, staleTime: 0, } ) // Increase polling interval over time (exponential backoff) useEffect(() => { if (!activeJobId) { setJobPollInterval(2000) return } const timer = setTimeout(() => { setJobPollInterval((prev) => Math.min(prev * 2, 15000)) }, jobPollInterval) return () => clearTimeout(timer) }, [activeJobId, jobPollInterval]) const utils = trpc.useUtils() const updateStatus = trpc.round.updateStatus.useMutation({ onSuccess: () => { utils.round.get.invalidate({ id: roundId }) }, }) const deleteRound = trpc.round.delete.useMutation({ onSuccess: () => { toast.success('Round deleted') utils.round.list.invalidate() router.push('/admin/rounds') }, onError: () => { toast.error('Failed to delete round') }, }) // Filtering mutations const startJob = trpc.filtering.startJob.useMutation() const finalizeResults = trpc.filtering.finalizeResults.useMutation({ onSuccess: () => { utils.round.get.invalidate({ id: roundId }) }, }) // Inline filtering results const resultsPerPage = 20 const { data: filteringResults, refetch: refetchResults } = trpc.filtering.getResults.useQuery( { roundId, outcome: outcomeFilter ? (outcomeFilter as 'PASSED' | 'FILTERED_OUT' | 'FLAGGED') : undefined, page: resultsPage, perPage: resultsPerPage, }, { enabled: isFilteringRound && (filteringStats?.total ?? 0) > 0, staleTime: 30_000, } ) const overrideResult = trpc.filtering.overrideResult.useMutation() const reinstateProject = trpc.filtering.reinstateProject.useMutation() const exportResults = trpc.export.filteringResults.useQuery( { roundId }, { enabled: false } ) // Save as template const saveAsTemplate = trpc.roundTemplate.createFromRound.useMutation({ onSuccess: (data) => { toast.success('Saved as template', { action: { label: 'View', onClick: () => router.push(`/admin/round-templates/${data.id}`), }, }) }, onError: (err) => { toast.error(err.message) }, }) // AI summary bulk generation const bulkSummaries = trpc.evaluation.generateBulkSummaries.useMutation({ onSuccess: (data) => { if (data.errors.length > 0) { toast.warning( `Generated ${data.generated} of ${data.total} summaries. ${data.errors.length} failed.` ) } else { toast.success(`Generated ${data.generated} AI summaries successfully`) } }, onError: (error) => { toast.error(error.message || 'Failed to generate AI summaries') }, }) // Set active job from latest job on load useEffect(() => { if (latestJob && (latestJob.status === 'RUNNING' || latestJob.status === 'PENDING')) { setActiveJobId(latestJob.id) } }, [latestJob]) // Handle job completion useEffect(() => { if (jobStatus?.status === 'COMPLETED') { toast.success( `Filtering complete: ${jobStatus.passedCount} passed, ${jobStatus.filteredCount} filtered out, ${jobStatus.flaggedCount} flagged` ) setActiveJobId(null) refetchFilteringStats() refetchResults() refetchLatestJob() } else if (jobStatus?.status === 'FAILED') { toast.error(`Filtering failed: ${jobStatus.errorMessage || 'Unknown error'}`) setActiveJobId(null) refetchLatestJob() } }, [jobStatus?.status, jobStatus?.passedCount, jobStatus?.filteredCount, jobStatus?.flaggedCount, jobStatus?.errorMessage, refetchFilteringStats, refetchResults, refetchLatestJob]) const handleStartFiltering = async () => { try { const result = await startJob.mutateAsync({ roundId }) setJobPollInterval(2000) setActiveJobId(result.jobId) toast.info('Filtering job started. Progress will update automatically.') } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to start filtering' ) } } const handleFinalizeFiltering = async () => { try { const result = await finalizeResults.mutateAsync({ roundId }) if (result.advancedToRoundName) { toast.success( `Finalized: ${result.passed} projects advanced to "${result.advancedToRoundName}", ${result.filteredOut} filtered out` ) } else { toast.success( `Finalized: ${result.passed} passed, ${result.filteredOut} filtered out. No next round to advance to.` ) } refetchFilteringStats() refetchRound() utils.round.get.invalidate({ id: roundId }) } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to finalize' ) } } // Inline results handlers const toggleResultRow = (id: string) => { const next = new Set(expandedRows) if (next.has(id)) next.delete(id) else next.add(id) setExpandedRows(next) } const handleOverride = async () => { if (!overrideDialog || !overrideReason.trim()) return try { await overrideResult.mutateAsync({ id: overrideDialog.id, finalOutcome: overrideOutcome as 'PASSED' | 'FILTERED_OUT' | 'FLAGGED', reason: overrideReason.trim(), }) toast.success('Result overridden') setOverrideDialog(null) setOverrideReason('') refetchResults() refetchFilteringStats() } catch { toast.error('Failed to override result') } } const handleReinstate = async (projectId: string) => { try { await reinstateProject.mutateAsync({ roundId, projectId }) toast.success('Project reinstated') refetchResults() refetchFilteringStats() } catch { toast.error('Failed to reinstate project') } } const handleRequestExportData = useCallback(async () => { const result = await exportResults.refetch() return result.data ?? undefined }, [exportResults]) const isJobRunning = jobStatus?.status === 'RUNNING' || jobStatus?.status === 'PENDING' const progressPercent = jobStatus?.totalBatches ? Math.round((jobStatus.currentBatch / jobStatus.totalBatches) * 100) : 0 if (isLoading) { return } if (!round) { return (

Round Not Found

) } const visibility = ROUND_FIELD_VISIBILITY[round.roundType] || ROUND_FIELD_VISIBILITY.EVALUATION const isLiveEvent = isLiveEventRound const now = new Date() const isVotingOpen = round.status === 'ACTIVE' && round.votingStartAt && round.votingEndAt && new Date(round.votingStartAt) <= now && new Date(round.votingEndAt) >= now const getStatusBadge = () => { if (round.status === 'ACTIVE' && isVotingOpen) { return ( Voting Open ) } switch (round.status) { case 'DRAFT': return Draft case 'ACTIVE': return ( Active ) case 'CLOSED': return Closed case 'ARCHIVED': return ( Archived ) default: return {round.status} } } return (
{/* Header */}
{round.program.year} Edition

{round.name}

{getStatusBadge()} {roundTypeLabels[round.roundType] || round.roundType}
{round.status === 'DRAFT' && ( )} {round.status === 'ACTIVE' && ( )} {round.status === 'CLOSED' && ( )} {round.status === 'ACTIVE' && ( )} Delete Round
{round.status === 'ACTIVE' && (
Warning: This round is currently ACTIVE. Deleting it will immediately end all ongoing evaluations.
)}

This will permanently delete “{round.name}” and all associated data:

  • {progress?.totalProjects || 0} projects in this round
  • {progress?.totalAssignments || 0} jury assignments
  • {progress?.completedAssignments || 0} submitted evaluations

This action cannot be undone.

Cancel deleteRound.mutate({ id: round.id })} disabled={deleteRound.isPending} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {deleteRound.isPending ? ( <> Deleting... ) : ( 'Delete Round' )}
{/* Stats Grid */}
Projects
{round._count.projects}
{visibility.showAssignmentLimits && ( Judge Assignments
{round._count.assignments}
)} {visibility.showRequiredReviews && ( Required Reviews
{round.requiredReviews}

per project

)} {isLiveEvent ? 'Session' : 'Completion'}
{isLiveEvent ? ( ) : ( )}
{isLiveEvent && liveSession ? ( <>
{liveSession.status === 'IN_PROGRESS' ? 'Live' : liveSession.status.toLowerCase().replace('_', ' ')}

{liveSession.audienceVoterCount} audience voters

) : ( <>
{progress?.completionPercentage || 0}%

{progress?.completedAssignments || 0} of {progress?.totalAssignments || 0}

)}
{/* Progress - only for evaluation rounds */} {visibility.showRequiredReviews && progress && progress.totalAssignments > 0 && (
Evaluation Progress
Overall Completion {progress.completionPercentage}%
{Object.entries(progress.evaluationsByStatus).map(([status, count]) => (

{count}

{status.toLowerCase().replace('_', ' ')}

))}
)} {/* Voting Window - only for evaluation rounds */} {visibility.showVotingWindow && (
Voting Window

Start Date

{round.votingStartAt ? (

{format(new Date(round.votingStartAt), 'PPP')}

{format(new Date(round.votingStartAt), 'p')}

) : (

Not set

)}

End Date

{round.votingEndAt ? (

{format(new Date(round.votingEndAt), 'PPP')}

{format(new Date(round.votingEndAt), 'p')}

{isFuture(new Date(round.votingEndAt)) && (

Ends {formatDistanceToNow(new Date(round.votingEndAt), { addSuffix: true })}

)}
) : (

Not set

)}
{/* Voting status */} {round.votingStartAt && round.votingEndAt && (
{isVotingOpen ? (
Voting is currently open
) : isFuture(new Date(round.votingStartAt)) ? (
Voting opens {formatDistanceToNow(new Date(round.votingStartAt), { addSuffix: true })}
) : isFuture(new Date(round.votingEndAt)) ? (
{round.status === 'DRAFT' ? 'Voting window configured (round not yet active)' : `Voting ends ${formatDistanceToNow(new Date(round.votingEndAt), { addSuffix: true })}` }
) : (
Voting period has ended
)}
)}
)} {/* Filtering Section (for FILTERING rounds) */} {isFilteringRound && (
Project Filtering
Run automated screening rules on projects in this round
{/* Progress Card (when job is running) */} {isJobRunning && jobStatus && (

AI Filtering in Progress

Processing {jobStatus.totalProjects} projects in {jobStatus.totalBatches} batches

Batch {jobStatus.currentBatch} of {jobStatus.totalBatches}
{jobStatus.processedCount} of {jobStatus.totalProjects} projects processed {progressPercent}%
)} {/* AI Status Warning */} {aiStatus?.hasAIRules && !aiStatus?.configured && (

AI Configuration Required

{aiStatus.error || 'AI screening rules require OpenAI to be configured.'}

)} {/* Stats */} {isLoadingFilteringStats && !isJobRunning ? (
{[1, 2, 3, 4].map((i) => (
))}
) : filteringStats && filteringStats.total > 0 ? (

{filteringStats.total}

Total

{filteringStats.passed}

Passed

{filteringStats.filteredOut}

Filtered Out

{filteringStats.flagged}

Flagged

) : !isJobRunning && (

No filtering results yet

Configure rules and run filtering to screen projects

)} {/* Inline Filtering Results Table */} {filteringStats && filteringStats.total > 0 && ( <>
{/* Outcome Filter Tabs */}
{['', 'PASSED', 'FILTERED_OUT', 'FLAGGED'].map((outcome) => ( ))}
{/* Results Table */} {filteringResults && filteringResults.results.length > 0 ? ( <>
Project Category Outcome AI Reason Actions {filteringResults.results.map((result) => { const isExpanded = expandedRows.has(result.id) const effectiveOutcome = result.finalOutcome || result.outcome const badge = OUTCOME_BADGES[effectiveOutcome] const aiScreening = result.aiScreeningJson as Record | null const firstAiResult = aiScreening ? Object.values(aiScreening)[0] : null const aiReasoning = firstAiResult?.reasoning return ( <> toggleResultRow(result.id)} >

{result.project.title}

{result.project.teamName} {result.project.country && ` ยท ${result.project.country}`}

{result.project.competitionCategory ? ( {result.project.competitionCategory.replace( '_', ' ' )} ) : ( '-' )}
{badge?.icon} {badge?.label || effectiveOutcome} {result.overriddenByUser && (

Overridden by {result.overriddenByUser.name || result.overriddenByUser.email}

)}
{aiReasoning ? (

{aiReasoning}

{firstAiResult && (
{firstAiResult.confidence !== undefined && ( Confidence: {Math.round(firstAiResult.confidence * 100)}% )} {firstAiResult.qualityScore !== undefined && ( Quality: {firstAiResult.qualityScore}/10 )} {firstAiResult.spamRisk && ( Spam Risk )}
)}
) : ( No AI screening )}
e.stopPropagation()} > {effectiveOutcome === 'FILTERED_OUT' && ( )}
{isExpanded && (
{/* Rule Results */}

Rule Results

{result.ruleResultsJson && Array.isArray(result.ruleResultsJson) ? (
{( result.ruleResultsJson as Array<{ ruleName: string ruleType: string passed: boolean action: string reasoning?: string }> ).filter((rr) => rr.ruleType !== 'AI_SCREENING').map((rr, i) => (
{rr.passed ? ( ) : ( )}
{rr.ruleName} {rr.ruleType.replace('_', ' ')}
{rr.reasoning && (

{rr.reasoning}

)}
))}
) : (

No detailed rule results available

)}
{/* AI Screening Details */} {aiScreening && Object.keys(aiScreening).length > 0 && (

AI Screening Analysis

{Object.entries(aiScreening).map(([ruleId, screening]) => (
{screening.meetsCriteria ? ( ) : ( )} {screening.meetsCriteria ? 'Meets Criteria' : 'Does Not Meet Criteria'} {screening.spamRisk && ( Spam Risk )}
{screening.reasoning && (

{screening.reasoning}

)}
{screening.confidence !== undefined && ( Confidence: {Math.round(screening.confidence * 100)}% )} {screening.qualityScore !== undefined && ( Quality Score: {screening.qualityScore}/10 )}
))}
)} {/* Override Info */} {result.overriddenByUser && (

Manual Override

Overridden to {result.finalOutcome} by{' '} {result.overriddenByUser.name || result.overriddenByUser.email}

{result.overrideReason && (

Reason: {result.overrideReason}

)}
)}
)} ) })}
) : filteringResults ? (

No results match this filter

) : null}
)} {/* Quick links */}
{filteringStats && filteringStats.total > 0 && ( )}
)} {/* Live Event Section (for LIVE_EVENT rounds) */} {isLiveEventRound && liveSession && (
Live Voting Session
Real-time voting during project presentations
{/* Session Status */}
{liveSession.status === 'IN_PROGRESS' ? ( ) : liveSession.status === 'COMPLETED' ? ( ) : ( )}

{liveSession.status === 'IN_PROGRESS' ? 'Live Now' : liveSession.status.toLowerCase().replace('_', ' ')}

Status

{liveSession.round.projects.length}

Projects

{liveSession.currentVotes.length}

Jury Votes

{liveSession.audienceVoterCount}

Audience

{/* Current Status Indicator */} {liveSession.status === 'IN_PROGRESS' && liveSession.currentProjectId && (

Voting in progress

Project {(liveSession.currentProjectIndex ?? 0) + 1} of {liveSession.round.projects.length}

)} {liveSession.status === 'COMPLETED' && (

Voting session completed - view results in the dashboard

)} {/* Quick Links */}
{liveSession.allowAudienceVotes && ( )}
)} {/* Quick Actions */}
Quick Actions
{/* Project Management */}

Project Management

{/* Type-Specific Management */}

{isFilteringRound ? 'Filtering' : isLiveEvent ? 'Live Event' : 'Evaluation'} Management

{/* Filtering-specific actions */} {isFilteringRound && ( <> )} {/* Evaluation-specific actions */} {visibility.showAssignmentLimits && ( )} {/* Live Event-specific actions */} {isLiveEvent && ( )} {/* Evaluation-round-only: AI Summaries */} {!isFilteringRound && !isLiveEvent && (

Uses AI to analyze all submitted evaluations for projects in this round and generate summary insights including strengths, weaknesses, and scoring patterns.

)}
{/* Dialogs */} utils.round.get.invalidate({ id: roundId })} /> utils.round.get.invalidate({ id: roundId })} /> utils.round.get.invalidate({ id: roundId })} /> {/* Override Dialog */} { if (!open) { setOverrideDialog(null) setOverrideReason('') } }} > Override Filtering Result Change the outcome for this project. This will be logged in the audit trail.
setOverrideReason(e.target.value)} placeholder="Explain why you're overriding..." />
{/* CSV Export Dialog */}
) } function RoundDetailSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
) } export default function RoundDetailPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }