'use client' import { useState, useMemo, useCallback } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import { Loader2, Plus, Trash2, RotateCcw, Check, ChevronsUpDown, Search, MoreHorizontal, UserPlus, } from 'lucide-react' export type IndividualAssignmentsTableProps = { roundId: string projectStates: any[] | undefined } export function IndividualAssignmentsTable({ roundId, projectStates, }: IndividualAssignmentsTableProps) { const [addDialogOpen, setAddDialogOpen] = useState(false) const [confirmAction, setConfirmAction] = useState<{ type: 'reset' | 'delete'; assignment: any } | null>(null) const [assignMode, setAssignMode] = useState<'byJuror' | 'byProject'>('byJuror') // ── By Juror mode state ── const [selectedJurorId, setSelectedJurorId] = useState('') const [selectedProjectIds, setSelectedProjectIds] = useState>(new Set()) const [jurorPopoverOpen, setJurorPopoverOpen] = useState(false) const [projectSearch, setProjectSearch] = useState('') // ── By Project mode state ── const [selectedProjectId, setSelectedProjectId] = useState('') const [selectedJurorIds, setSelectedJurorIds] = useState>(new Set()) const [projectPopoverOpen, setProjectPopoverOpen] = useState(false) const [jurorSearch, setJurorSearch] = useState('') const utils = trpc.useUtils() const { data: assignments, isLoading } = trpc.assignment.listByStage.useQuery( { roundId }, { refetchInterval: 15_000 }, ) const { data: juryMembers } = trpc.user.getJuryMembers.useQuery( { roundId }, { enabled: addDialogOpen }, ) const deleteMutation = trpc.assignment.delete.useMutation({ onSuccess: () => { utils.assignment.listByStage.invalidate({ roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) toast.success('Assignment removed') }, onError: (err) => toast.error(err.message), }) const resetEvalMutation = trpc.evaluation.resetEvaluation.useMutation({ onSuccess: () => { utils.assignment.listByStage.invalidate({ roundId }) toast.success('Evaluation reset — juror can now start over') }, onError: (err) => toast.error(err.message), }) const reassignCOIMutation = trpc.assignment.reassignCOI.useMutation({ onSuccess: (data) => { utils.assignment.listByStage.invalidate({ roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) utils.analytics.getJurorWorkload.invalidate({ roundId }) utils.evaluation.listCOIByStage.invalidate({ roundId }) toast.success(`Reassigned to ${data.newJurorName}`) }, onError: (err) => toast.error(err.message), }) const createMutation = trpc.assignment.create.useMutation({ onSuccess: () => { utils.assignment.listByStage.invalidate({ roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) utils.user.getJuryMembers.invalidate({ roundId }) toast.success('Assignment created') resetDialog() }, onError: (err) => toast.error(err.message), }) const bulkCreateMutation = trpc.assignment.bulkCreate.useMutation({ onSuccess: (result) => { utils.assignment.listByStage.invalidate({ roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) utils.user.getJuryMembers.invalidate({ roundId }) toast.success(`${result.created} assignment(s) created`) resetDialog() }, onError: (err) => toast.error(err.message), }) const resetDialog = useCallback(() => { setAddDialogOpen(false) setAssignMode('byJuror') setSelectedJurorId('') setSelectedProjectIds(new Set()) setProjectSearch('') setSelectedProjectId('') setSelectedJurorIds(new Set()) setJurorSearch('') }, []) const selectedJuror = useMemo( () => juryMembers?.find((j: any) => j.id === selectedJurorId), [juryMembers, selectedJurorId], ) // Filter projects by search term const filteredProjects = useMemo(() => { const items = projectStates ?? [] if (!projectSearch) return items const q = projectSearch.toLowerCase() return items.filter((ps: any) => ps.project?.title?.toLowerCase().includes(q) || ps.project?.teamName?.toLowerCase().includes(q) || ps.project?.competitionCategory?.toLowerCase().includes(q) ) }, [projectStates, projectSearch]) // Existing assignments for the selected juror (to grey out already-assigned projects) const jurorExistingProjectIds = useMemo(() => { if (!selectedJurorId || !assignments) return new Set() return new Set( assignments .filter((a: any) => a.userId === selectedJurorId) .map((a: any) => a.projectId) ) }, [selectedJurorId, assignments]) const toggleProject = useCallback((projectId: string) => { setSelectedProjectIds(prev => { const next = new Set(prev) if (next.has(projectId)) { next.delete(projectId) } else { next.add(projectId) } return next }) }, []) const selectAllUnassigned = useCallback(() => { const unassigned = filteredProjects .filter((ps: any) => !jurorExistingProjectIds.has(ps.project?.id)) .map((ps: any) => ps.project?.id) .filter(Boolean) setSelectedProjectIds(new Set(unassigned)) }, [filteredProjects, jurorExistingProjectIds]) const handleCreate = useCallback(() => { if (!selectedJurorId || selectedProjectIds.size === 0) return const projectIds = Array.from(selectedProjectIds) if (projectIds.length === 1) { createMutation.mutate({ userId: selectedJurorId, projectId: projectIds[0], roundId, }) } else { bulkCreateMutation.mutate({ roundId, assignments: projectIds.map(projectId => ({ userId: selectedJurorId, projectId, })), }) } }, [selectedJurorId, selectedProjectIds, roundId, createMutation, bulkCreateMutation]) const isMutating = createMutation.isPending || bulkCreateMutation.isPending // ── By Project mode helpers ── // Existing assignments for the selected project (to grey out already-assigned jurors) const projectExistingJurorIds = useMemo(() => { if (!selectedProjectId || !assignments) return new Set() return new Set( assignments .filter((a: any) => a.projectId === selectedProjectId) .map((a: any) => a.userId) ) }, [selectedProjectId, assignments]) // Count assignments per juror in this round (for display) const jurorAssignmentCounts = useMemo(() => { if (!assignments) return new Map() const counts = new Map() for (const a of assignments) { counts.set(a.userId, (counts.get(a.userId) || 0) + 1) } return counts }, [assignments]) // Filter jurors by search term const filteredJurors = useMemo(() => { const items = juryMembers ?? [] if (!jurorSearch) return items const q = jurorSearch.toLowerCase() return items.filter((j: any) => j.name?.toLowerCase().includes(q) || j.email?.toLowerCase().includes(q) ) }, [juryMembers, jurorSearch]) const toggleJuror = useCallback((jurorId: string) => { setSelectedJurorIds(prev => { const next = new Set(prev) if (next.has(jurorId)) next.delete(jurorId) else next.add(jurorId) return next }) }, []) const handleCreateByProject = useCallback(() => { if (!selectedProjectId || selectedJurorIds.size === 0) return const jurorIds = Array.from(selectedJurorIds) if (jurorIds.length === 1) { createMutation.mutate({ userId: jurorIds[0], projectId: selectedProjectId, roundId, }) } else { bulkCreateMutation.mutate({ roundId, assignments: jurorIds.map(userId => ({ userId, projectId: selectedProjectId, })), }) } }, [selectedProjectId, selectedJurorIds, roundId, createMutation, bulkCreateMutation]) return (

{assignments?.length ?? 0} individual assignments

{isLoading ? (
{[1, 2, 3, 4, 5].map((i) => )}
) : !assignments || assignments.length === 0 ? (

No assignments yet. Generate assignments or add one manually.

) : (
Juror Project Status Actions
{assignments.map((a: any, idx: number) => (
{a.user?.name || a.user?.email || 'Unknown'} {a.project?.title || 'Unknown'}
{a.conflictOfInterest?.hasConflict ? ( COI ) : ( {a.evaluation?.status || 'PENDING'} )}
{a.conflictOfInterest?.hasConflict && ( <> reassignCOIMutation.mutate({ assignmentId: a.id })} disabled={reassignCOIMutation.isPending} > Reassign (COI) )} {a.evaluation && ( <> setConfirmAction({ type: 'reset', assignment: a })} disabled={resetEvalMutation.isPending} > Reset Evaluation )} setConfirmAction({ type: 'delete', assignment: a })} disabled={deleteMutation.isPending} > Delete Assignment
))}
)}
{/* Add Assignment Dialog */} { if (!open) resetDialog() else setAddDialogOpen(true) }}> Add Assignment {assignMode === 'byJuror' ? 'Select a juror, then choose projects to assign' : 'Select a project, then choose jurors to assign' } {/* Mode Toggle */} { setAssignMode(v as 'byJuror' | 'byProject') // Reset selections when switching setSelectedJurorId('') setSelectedProjectIds(new Set()) setProjectSearch('') setSelectedProjectId('') setSelectedJurorIds(new Set()) setJurorSearch('') }}> By Juror By Project {/* ── By Juror Tab ── */} {/* Juror Selector */}
No jury members found. {juryMembers?.map((juror: any) => { const atCapacity = juror.maxAssignments !== null && juror.availableSlots === 0 return ( { setSelectedJurorId(juror.id === selectedJurorId ? '' : juror.id) setSelectedProjectIds(new Set()) setJurorPopoverOpen(false) }} >

{juror.name || 'Unnamed'}

{juror.email}

{juror.currentAssignments}/{juror.maxAssignments ?? '\u221E'} {atCapacity ? ' full' : ''}
) })}
{/* Project Multi-Select */}
{selectedJurorId && (
{selectedProjectIds.size > 0 && ( )}
)}
{/* Search input */}
setProjectSearch(e.target.value)} className="pl-9 h-9" />
{/* Project checklist */}
{!selectedJurorId ? (

Select a juror first

) : filteredProjects.length === 0 ? (

No projects found

) : ( filteredProjects.map((ps: any) => { const project = ps.project if (!project) return null const alreadyAssigned = jurorExistingProjectIds.has(project.id) const isSelected = selectedProjectIds.has(project.id) return ( ) }) )}
{/* ── By Project Tab ── */} {/* Project Selector */}
No projects found. {(projectStates ?? []).map((ps: any) => { const project = ps.project if (!project) return null return ( { setSelectedProjectId(project.id === selectedProjectId ? '' : project.id) setSelectedJurorIds(new Set()) setProjectPopoverOpen(false) }} >

{project.title}

{project.teamName}

{project.competitionCategory && ( {project.competitionCategory === 'STARTUP' ? 'Startup' : 'Concept'} )}
) })}
{/* Juror Multi-Select */}
{selectedProjectId && selectedJurorIds.size > 0 && ( )}
{/* Search input */}
setJurorSearch(e.target.value)} className="pl-9 h-9" />
{/* Juror checklist */}
{!selectedProjectId ? (

Select a project first

) : filteredJurors.length === 0 ? (

No jurors found

) : ( filteredJurors.map((juror: any) => { const alreadyAssigned = projectExistingJurorIds.has(juror.id) const isSelected = selectedJurorIds.has(juror.id) const assignCount = jurorAssignmentCounts.get(juror.id) ?? 0 return ( ) }) )}
{/* Confirmation AlertDialog for reset/delete */} { if (!open) setConfirmAction(null) }}> {confirmAction?.type === 'reset' ? 'Reset evaluation?' : 'Delete assignment?'} {confirmAction?.type === 'reset' ? `Reset evaluation by ${confirmAction.assignment?.user?.name || confirmAction.assignment?.user?.email} for "${confirmAction.assignment?.project?.title}"? This will erase all scores and feedback so they can start over.` : `Remove assignment for ${confirmAction?.assignment?.user?.name || confirmAction?.assignment?.user?.email} on "${confirmAction?.assignment?.project?.title}"?` } Cancel { if (confirmAction?.type === 'reset') { resetEvalMutation.mutate({ assignmentId: confirmAction.assignment.id }) } else if (confirmAction?.type === 'delete') { deleteMutation.mutate({ id: confirmAction.assignment.id }) } setConfirmAction(null) }} > {confirmAction?.type === 'reset' ? 'Reset' : 'Delete'}
) }