'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, Sparkles, LayoutTemplate, ShieldCheck, Download, RotateCcw, } from 'lucide-react' import { toast } from 'sonner' 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) // 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 }) const { data: progress } = trpc.round.getProgress.useQuery({ id: roundId }) // Check if this is a filtering round - roundType is stored directly on the round const isFilteringRound = round?.roundType === 'FILTERING' // Filtering queries (only fetch for FILTERING rounds) const { data: filteringStats, isLoading: isLoadingFilteringStats, refetch: refetchFilteringStats } = trpc.filtering.getResultStats.useQuery( { roundId }, { enabled: isFilteringRound, staleTime: 0 } ) const { data: filteringRules } = trpc.filtering.getRules.useQuery( { roundId }, { enabled: isFilteringRound } ) const { data: aiStatus } = trpc.filtering.checkAIStatus.useQuery( { roundId }, { enabled: isFilteringRound } ) const { data: latestJob, refetch: refetchLatestJob } = trpc.filtering.getLatestJob.useQuery( { roundId }, { enabled: isFilteringRound, staleTime: 0 } ) // Poll for job status when there's an active job const { data: jobStatus } = trpc.filtering.getJobStatus.useQuery( { jobId: activeJobId! }, { enabled: !!activeJobId, refetchInterval: activeJobId ? 2000 : false, staleTime: 0, } ) const utils = trpc.useUtils() const updateStatus = trpc.round.updateStatus.useMutation({ onSuccess: () => { utils.round.get.invalidate({ id: roundId }) utils.round.list.invalidate() utils.program.list.invalidate({ includeRounds: true }) }, }) const deleteRound = trpc.round.delete.useMutation({ onSuccess: () => { toast.success('Round deleted') utils.program.list.invalidate() 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 }) utils.project.list.invalidate() }, }) // 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: 0, } ) 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 }) 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.project.list.invalidate() utils.program.list.invalidate({ includeRounds: true }) 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() utils.project.list.invalidate() } catch { toast.error('Failed to override result') } } const handleReinstate = async (projectId: string) => { try { await reinstateProject.mutateAsync({ roundId, projectId }) toast.success('Project reinstated') refetchResults() refetchFilteringStats() utils.project.list.invalidate() } 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 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()}
{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}
Judge Assignments
{round._count.assignments}
Required Reviews
{round.requiredReviews}

per project

Completion
{progress?.completionPercentage || 0}%

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

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

{count}

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

))}
)} {/* Voting Window */} 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 && ( )}
)} {/* Quick Actions */} Quick Actions {/* Project Management */}

Project Management

{/* Round Management */}

Round Management

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 ( }> ) }