'use client' import { Suspense, use, useState, useEffect } from 'react' import Link from 'next/link' 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 { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { ArrowLeft, Users, FileText, CheckCircle2, Clock, AlertCircle, Sparkles, Loader2, Plus, Trash2, RefreshCw, UserPlus, } from 'lucide-react' import { toast } from 'sonner' interface PageProps { params: Promise<{ id: string }> } function AssignmentManagementContent({ roundId }: { roundId: string }) { const [selectedSuggestions, setSelectedSuggestions] = useState>(new Set()) const [manualDialogOpen, setManualDialogOpen] = useState(false) const [selectedJuror, setSelectedJuror] = useState('') const [selectedProject, setSelectedProject] = useState('') const [useAI, setUseAI] = useState(false) const [activeJobId, setActiveJobId] = useState(null) const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId }) const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId }) const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId }) const { data: isAIAvailable } = trpc.assignment.isAIAvailable.useQuery() // AI Assignment job queries const { data: latestJob, refetch: refetchLatestJob } = trpc.assignment.getLatestAIAssignmentJob.useQuery( { roundId }, { enabled: useAI } ) // Poll for job status when there's an active job const { data: jobStatus } = trpc.assignment.getAIAssignmentJobStatus.useQuery( { jobId: activeJobId! }, { enabled: !!activeJobId, refetchInterval: activeJobId ? 2000 : false, } ) // Start AI assignment job mutation const startAIJob = trpc.assignment.startAIAssignmentJob.useMutation() const isAIJobRunning = jobStatus?.status === 'RUNNING' || jobStatus?.status === 'PENDING' const aiJobProgressPercent = jobStatus?.totalBatches ? Math.round((jobStatus.currentBatch / jobStatus.totalBatches) * 100) : 0 // Algorithmic suggestions (default) const { data: algorithmicSuggestions, isLoading: loadingAlgorithmic, refetch: refetchAlgorithmic } = trpc.assignment.getSuggestions.useQuery( { roundId }, { enabled: !!round && !useAI } ) // AI-powered suggestions (expensive - only used after job completes) const { data: aiSuggestionsRaw, isLoading: loadingAI, refetch: refetchAI } = trpc.assignment.getAISuggestions.useQuery( { roundId, useAI: true }, { enabled: !!round && useAI && !isAIJobRunning, staleTime: Infinity, // Never consider stale (only refetch manually) refetchOnWindowFocus: false, refetchOnReconnect: false, refetchOnMount: false, } ) // 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( `AI Assignment complete: ${jobStatus.suggestionsCount} suggestions generated${jobStatus.fallbackUsed ? ' (using fallback algorithm)' : ''}` ) setActiveJobId(null) refetchLatestJob() refetchAI() } else if (jobStatus?.status === 'FAILED') { toast.error(`AI Assignment failed: ${jobStatus.errorMessage || 'Unknown error'}`) setActiveJobId(null) refetchLatestJob() } }, [jobStatus?.status, jobStatus?.suggestionsCount, jobStatus?.fallbackUsed, jobStatus?.errorMessage, refetchLatestJob, refetchAI]) const handleStartAIJob = async () => { try { const result = await startAIJob.mutateAsync({ roundId }) setActiveJobId(result.jobId) toast.info('AI Assignment job started. Progress will update automatically.') } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to start AI assignment' ) } } // Normalize AI suggestions to match algorithmic format const aiSuggestions = aiSuggestionsRaw?.suggestions?.map((s) => ({ userId: s.jurorId, jurorName: s.jurorName, projectId: s.projectId, projectTitle: s.projectTitle, score: Math.round(s.confidenceScore * 100), reasoning: [s.reasoning], })) ?? [] // Use the appropriate suggestions based on mode const suggestions = useAI ? aiSuggestions : (algorithmicSuggestions ?? []) const loadingSuggestions = useAI ? (loadingAI || isAIJobRunning) : loadingAlgorithmic const refetchSuggestions = useAI ? refetchAI : refetchAlgorithmic // Get available jurors for manual assignment const { data: availableJurors } = trpc.user.getJuryMembers.useQuery( { roundId }, { enabled: manualDialogOpen } ) // Get projects in this round for manual assignment const { data: roundProjects } = trpc.project.list.useQuery( { roundId, perPage: 500 }, { enabled: manualDialogOpen } ) const utils = trpc.useUtils() const deleteAssignment = trpc.assignment.delete.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) }, }) const applySuggestions = trpc.assignment.applySuggestions.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) utils.assignment.getSuggestions.invalidate({ roundId }) utils.assignment.getAISuggestions.invalidate({ roundId }) setSelectedSuggestions(new Set()) }, }) const createAssignment = trpc.assignment.create.useMutation({ onSuccess: () => { utils.assignment.listByRound.invalidate({ roundId }) utils.assignment.getStats.invalidate({ roundId }) utils.assignment.getSuggestions.invalidate({ roundId }) setManualDialogOpen(false) setSelectedJuror('') setSelectedProject('') toast.success('Assignment created successfully') }, onError: (error) => { toast.error(error.message || 'Failed to create assignment') }, }) const handleCreateManualAssignment = () => { if (!selectedJuror || !selectedProject) { toast.error('Please select both a juror and a project') return } createAssignment.mutate({ userId: selectedJuror, projectId: selectedProject, roundId, }) } if (loadingRound || loadingAssignments) { return } if (!round) { return (

Round Not Found

) } const handleToggleSuggestion = (key: string) => { setSelectedSuggestions((prev) => { const newSet = new Set(prev) if (newSet.has(key)) { newSet.delete(key) } else { newSet.add(key) } return newSet }) } const handleSelectAllSuggestions = () => { if (suggestions) { if (selectedSuggestions.size === suggestions.length) { setSelectedSuggestions(new Set()) } else { setSelectedSuggestions( new Set(suggestions.map((s) => `${s.userId}-${s.projectId}`)) ) } } } const handleApplySelected = async () => { if (!suggestions) return const selected = suggestions.filter((s) => selectedSuggestions.has(`${s.userId}-${s.projectId}`) ) await applySuggestions.mutateAsync({ roundId, assignments: selected.map((s) => ({ userId: s.userId, projectId: s.projectId, reasoning: s.reasoning.join('; '), })), }) } // Group assignments by project const assignmentsByProject = assignments?.reduce((acc, assignment) => { const projectId = assignment.project.id if (!acc[projectId]) { acc[projectId] = { project: assignment.project, assignments: [], } } acc[projectId].assignments.push(assignment) return acc }, {} as Record) || {} return (
{/* Header */}
{round.program.name} / {round.name}

Manage Judge Assignments

{/* Manual Assignment Button */} Create Manual Assignment Assign a jury member to evaluate a specific project
{/* Juror Select */}
{selectedJuror && availableJurors && (

{(() => { const juror = availableJurors.find(j => j.id === selectedJuror) if (!juror) return null const available = (juror.maxAssignments ?? 10) - juror.currentAssignments return `${available} assignment slot${available !== 1 ? 's' : ''} available` })()}

)}
{/* Project Select */}
{/* Stats */} {stats && (
Total Judge Assignments
{stats.totalAssignments}
Completed
{stats.completedAssignments}

{stats.completionPercentage}% complete

Projects Covered
{stats.projectsWithFullCoverage}/{stats.totalProjects}

{stats.coveragePercentage}% have {round.requiredReviews}+ reviews

Jury Members
{stats.juryMembersAssigned}

assigned to projects

)} {/* Coverage Progress */} {stats && ( Project Coverage {stats.projectsWithFullCoverage} of {stats.totalProjects} projects have at least {round.requiredReviews} reviewers assigned )} {/* Smart Suggestions */}
{useAI ? 'AI Assignment Suggestions' : 'Smart Assignment Suggestions'} {useAI ? 'GPT-powered recommendations analyzing project descriptions and judge expertise' : 'Algorithmic recommendations based on tag matching and workload balance'}
{useAI && !isAIJobRunning && ( )} {!useAI && ( )}
{/* AI Job Progress Indicator */} {isAIJobRunning && jobStatus && (

AI Assignment Analysis in Progress

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

Batch {jobStatus.currentBatch} of {jobStatus.totalBatches}
{jobStatus.processedCount} of {jobStatus.totalProjects} projects processed {aiJobProgressPercent}%
)} {loadingSuggestions && !isAIJobRunning ? (
) : suggestions && suggestions.length > 0 ? (
{selectedSuggestions.size} of {suggestions.length} selected
Juror Project Score Reasoning {suggestions.map((suggestion) => { const key = `${suggestion.userId}-${suggestion.projectId}` const isSelected = selectedSuggestions.has(key) return ( handleToggleSuggestion(key)} /> {suggestion.jurorName} {suggestion.projectTitle} = 60 ? 'default' : suggestion.score >= 40 ? 'secondary' : 'outline' } > {suggestion.score.toFixed(0)}
    {suggestion.reasoning.map((r, i) => (
  • {r}
  • ))}
) })}
) : (

All projects are covered!

No additional assignments are needed at this time

)}
{/* Current Judge Assignments */} Current Judge Assignments View and manage existing project-to-judge assignments {Object.keys(assignmentsByProject).length > 0 ? (
{Object.entries(assignmentsByProject).map( ([projectId, { project, assignments: projectAssignments }]) => (

{project.title}

{projectAssignments.length} reviewer {projectAssignments.length !== 1 ? 's' : ''} {projectAssignments.length >= round.requiredReviews && ( Full coverage )}
{projectAssignments.map((assignment) => (
{assignment.user.name || assignment.user.email} {assignment.evaluation?.status === 'SUBMITTED' ? ( Submitted ) : assignment.evaluation?.status === 'DRAFT' ? ( In Progress ) : ( Pending )}
Remove Assignment? This will remove {assignment.user.name || assignment.user.email} from evaluating this project. This action cannot be undone. Cancel deleteAssignment.mutate({ id: assignment.id }) } className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Remove
))}
) )}
) : (

No Judge Assignments Yet

Use the smart suggestions above or manually assign judges to projects

)}
) } function AssignmentsSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
) } export default function AssignmentManagementPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }