'use client' import { useState, useCallback } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Skeleton } from '@/components/ui/skeleton' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Loader2, MoreHorizontal, ArrowRight, XCircle, CheckCircle2, Clock, Play, LogOut, Layers, Trash2, Plus, } from 'lucide-react' import Link from 'next/link' import type { Route } from 'next' const PROJECT_STATES = ['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'] as const type ProjectState = (typeof PROJECT_STATES)[number] const stateConfig: Record = { PENDING: { label: 'Pending', color: 'bg-gray-100 text-gray-700 border-gray-200', icon: Clock }, IN_PROGRESS: { label: 'In Progress', color: 'bg-blue-100 text-blue-700 border-blue-200', icon: Play }, PASSED: { label: 'Passed', color: 'bg-green-100 text-green-700 border-green-200', icon: CheckCircle2 }, REJECTED: { label: 'Rejected', color: 'bg-red-100 text-red-700 border-red-200', icon: XCircle }, COMPLETED: { label: 'Completed', color: 'bg-emerald-100 text-emerald-700 border-emerald-200', icon: CheckCircle2 }, WITHDRAWN: { label: 'Withdrawn', color: 'bg-orange-100 text-orange-700 border-orange-200', icon: LogOut }, } type ProjectStatesTableProps = { competitionId: string roundId: string } export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTableProps) { const [selectedIds, setSelectedIds] = useState>(new Set()) const [stateFilter, setStateFilter] = useState('ALL') const [batchDialogOpen, setBatchDialogOpen] = useState(false) const [batchNewState, setBatchNewState] = useState('PASSED') const [removeConfirmId, setRemoveConfirmId] = useState(null) const [batchRemoveOpen, setBatchRemoveOpen] = useState(false) const utils = trpc.useUtils() const { data: projectStates, isLoading } = trpc.roundEngine.getProjectStates.useQuery( { roundId }, ) const transitionMutation = trpc.roundEngine.transitionProject.useMutation({ onSuccess: () => { utils.roundEngine.getProjectStates.invalidate({ roundId }) toast.success('Project state updated') }, onError: (err) => toast.error(err.message), }) const batchTransitionMutation = trpc.roundEngine.batchTransition.useMutation({ onSuccess: (data) => { utils.roundEngine.getProjectStates.invalidate({ roundId }) setSelectedIds(new Set()) setBatchDialogOpen(false) toast.success(`${data.succeeded.length} projects updated${data.failed.length > 0 ? `, ${data.failed.length} failed` : ''}`) }, onError: (err) => toast.error(err.message), }) const removeMutation = trpc.roundEngine.removeFromRound.useMutation({ onSuccess: (data) => { utils.roundEngine.getProjectStates.invalidate({ roundId }) setRemoveConfirmId(null) toast.success(`Removed from ${data.removedFromRounds} round(s)`) }, onError: (err) => toast.error(err.message), }) const batchRemoveMutation = trpc.roundEngine.batchRemoveFromRound.useMutation({ onSuccess: (data) => { utils.roundEngine.getProjectStates.invalidate({ roundId }) setSelectedIds(new Set()) setBatchRemoveOpen(false) toast.success(`${data.removedCount} project(s) removed from this round and later rounds`) }, onError: (err) => toast.error(err.message), }) const handleTransition = (projectId: string, newState: ProjectState) => { transitionMutation.mutate({ projectId, roundId, newState }) } const handleBatchTransition = () => { batchTransitionMutation.mutate({ projectIds: Array.from(selectedIds), roundId, newState: batchNewState, }) } const toggleSelect = (id: string) => { setSelectedIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const filtered = projectStates?.filter((ps: any) => stateFilter === 'ALL' ? true : ps.state === stateFilter ) ?? [] const toggleSelectAll = useCallback(() => { const ids = filtered.map((ps: any) => ps.projectId) const allSelected = ids.length > 0 && ids.every((id: string) => selectedIds.has(id)) if (allSelected) { setSelectedIds((prev) => { const next = new Set(prev) ids.forEach((id: string) => next.delete(id)) return next }) } else { setSelectedIds((prev) => { const next = new Set(prev) ids.forEach((id: string) => next.add(id)) return next }) } }, [filtered, selectedIds]) // State counts const counts = projectStates?.reduce((acc: Record, ps: any) => { acc[ps.state] = (acc[ps.state] || 0) + 1 return acc }, {} as Record) ?? {} if (isLoading) { return (
{[1, 2, 3, 4, 5].map((i) => ( ))}
) } if (!projectStates || projectStates.length === 0) { return (

No Projects in This Round

Assign projects from the Project Pool to this round to get started.

) } return (
{/* Top bar: filters + add button */}
{PROJECT_STATES.map((state) => { const count = counts[state] || 0 if (count === 0) return null const cfg = stateConfig[state] return ( ) })}
{/* Bulk actions bar */} {selectedIds.size > 0 && (
{selectedIds.size} selected
)} {/* Table */}
{/* Header */}
0 && filtered.every((ps: any) => selectedIds.has(ps.projectId))} onCheckedChange={toggleSelectAll} />
Project
Category
State
Entered
{/* Rows */} {filtered.map((ps: any) => { const cfg = stateConfig[ps.state as ProjectState] || stateConfig.PENDING const StateIcon = cfg.icon return (
toggleSelect(ps.projectId)} />

{ps.project?.title || 'Unknown'}

{ps.project?.teamName}

{ps.project?.competitionCategory || '—'}
{cfg.label}
{ps.enteredAt ? new Date(ps.enteredAt).toLocaleDateString() : '—'}
{PROJECT_STATES.filter((s) => s !== ps.state).map((state) => { const sCfg = stateConfig[state] return ( handleTransition(ps.projectId, state)} disabled={transitionMutation.isPending} > Move to {sCfg.label} ) })} setRemoveConfirmId(ps.projectId)} className="text-destructive focus:text-destructive" > Remove from Round
) })}
{/* Single Remove Confirmation */} { if (!open) setRemoveConfirmId(null) }}> Remove project from this round? The project will be removed from this round and all subsequent rounds. It will remain in any prior rounds it was already assigned to. Cancel { if (removeConfirmId) { removeMutation.mutate({ projectId: removeConfirmId, roundId }) } }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" disabled={removeMutation.isPending} > {removeMutation.isPending && } Remove {/* Batch Remove Confirmation */} Remove {selectedIds.size} projects from this round? These projects will be removed from this round and all subsequent rounds in the competition. They will remain in any prior rounds they were already assigned to. Cancel { batchRemoveMutation.mutate({ projectIds: Array.from(selectedIds), roundId, }) }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" disabled={batchRemoveMutation.isPending} > {batchRemoveMutation.isPending && } Remove {selectedIds.size} Projects {/* Batch Transition Dialog */} Change State for {selectedIds.size} Projects All selected projects will be moved to the new state.
) }