'use client' import { Suspense, use, useState } 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 { 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: suggestions, isLoading: loadingSuggestions, refetch: refetchSuggestions } = trpc.assignment.getSuggestions.useQuery( { roundId, maxPerJuror: 10, minPerProject: 3 }, { enabled: !!round } ) // 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 }) 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 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 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 */}
Smart Assignment Suggestions AI-powered recommendations based on expertise matching and workload balance
{loadingSuggestions ? (
) : 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 Assignments */} Current Assignments View and manage existing project 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 Assignments Yet

Use the smart suggestions above or manually assign jury members to projects

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