'use client' import { useState, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { ChevronDown, ChevronUp, Loader2, CheckCircle2, Play, Trophy, AlertTriangle, Search, } from 'lucide-react' import { CountryDisplay } from '@/components/shared/country-display' type AwardShortlistProps = { awardId: string roundId: string awardName: string criteriaText?: string | null eligibilityMode: string shortlistSize: number jobStatus?: string | null jobTotal?: number | null jobDone?: number | null } export function AwardShortlist({ awardId, roundId, awardName, criteriaText, eligibilityMode, shortlistSize, jobStatus, jobTotal, jobDone, }: AwardShortlistProps) { const [expanded, setExpanded] = useState(false) const [eligibilitySearch, setEligibilitySearch] = useState('') const utils = trpc.useUtils() const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING' const { data: shortlist, isLoading: isLoadingShortlist } = trpc.specialAward.listShortlist.useQuery( { awardId, perPage: 100 }, { enabled: expanded && !isRunning } ) const { data: jobPoll } = trpc.specialAward.getEligibilityJobStatus.useQuery( { awardId }, { enabled: isRunning, refetchInterval: 3000 } ) const runMutation = trpc.specialAward.runEligibilityForRound.useMutation({ onSuccess: () => { toast.success('Eligibility evaluation started') utils.specialAward.getEligibilityJobStatus.invalidate({ awardId }) utils.specialAward.listForRound.invalidate({ roundId }) }, onError: (err) => toast.error(`Failed: ${err.message}`), }) const toggleMutation = trpc.specialAward.toggleShortlisted.useMutation({ onSuccess: (data) => { utils.specialAward.listShortlist.invalidate({ awardId }) utils.specialAward.listForRound.invalidate({ roundId }) toast.success(data.shortlisted ? 'Added to shortlist' : 'Removed from shortlist') }, onError: (err) => toast.error(`Failed: ${err.message}`), }) const bulkToggleMutation = trpc.specialAward.bulkToggleShortlisted.useMutation({ onSuccess: (data) => { utils.specialAward.listShortlist.invalidate({ awardId }) utils.specialAward.listForRound.invalidate({ roundId }) toast.success(`${data.updated} projects ${data.shortlisted ? 'added to' : 'removed from'} shortlist`) }, onError: (err) => toast.error(`Failed: ${err.message}`), }) const { data: awardRounds } = trpc.specialAward.listRounds.useQuery( { awardId }, { enabled: expanded && eligibilityMode === 'SEPARATE_POOL' } ) const hasAwardRounds = (awardRounds?.length ?? 0) > 0 const confirmMutation = trpc.specialAward.confirmShortlist.useMutation({ onSuccess: (data) => { utils.specialAward.listShortlist.invalidate({ awardId }) utils.specialAward.listForRound.invalidate({ roundId }) toast.success( `Confirmed ${data.confirmedCount} projects` + (data.routedCount > 0 ? ` — ${data.routedCount} routed to award track` : '') ) }, onError: (err) => toast.error(`Failed: ${err.message}`), }) const currentJobStatus = jobPoll?.eligibilityJobStatus ?? jobStatus const currentJobDone = jobPoll?.eligibilityJobDone ?? jobDone const currentJobTotal = jobPoll?.eligibilityJobTotal ?? jobTotal const jobProgress = currentJobTotal && currentJobTotal > 0 ? Math.round(((currentJobDone ?? 0) / currentJobTotal) * 100) : 0 const filteredEligibilities = useMemo(() => { if (!shortlist) return [] if (!eligibilitySearch.trim()) return shortlist.eligibilities const q = eligibilitySearch.toLowerCase() return shortlist.eligibilities.filter((e: any) => (e.project?.title || '').toLowerCase().includes(q) || (e.project?.teamName || '').toLowerCase().includes(q) || (e.project?.country || '').toLowerCase().includes(q) || (e.project?.institution || '').toLowerCase().includes(q) || (e.project?.competitionCategory || '').toLowerCase().includes(q) ) }, [shortlist, eligibilitySearch]) const shortlistedCount = shortlist?.eligibilities?.filter((e) => e.shortlisted).length ?? 0 const allShortlisted = shortlist && shortlist.eligibilities.length > 0 && shortlist.eligibilities.every((e) => e.shortlisted) const someShortlisted = shortlistedCount > 0 && !allShortlisted const handleBulkToggle = () => { if (!shortlist) return const projectIds = shortlist.eligibilities.map((e) => e.project.id) const newValue = !allShortlisted bulkToggleMutation.mutate({ awardId, projectIds, shortlisted: newValue }) } return (
{/* Job controls */}
Evaluate PASSED projects against this award's criteria
{/* Progress bar */} {isRunning && currentJobTotal && currentJobTotal > 0 && (

{currentJobDone ?? 0} / {currentJobTotal} projects

)} {/* Shortlist table */} {expanded && currentJobStatus === 'COMPLETED' && ( <> {isLoadingShortlist ? (
{[1, 2, 3].map((i) => )}
) : shortlist && shortlist.eligibilities.length > 0 ? (

{shortlist.total} eligible projects {shortlistedCount > 0 && ( ({shortlistedCount} shortlisted) )}

{shortlistedCount > 0 && ( Confirm Shortlist

{eligibilityMode === 'SEPARATE_POOL' ? `This will confirm ${shortlistedCount} projects for the "${awardName}" award track. Projects will be routed to the award's rounds for separate evaluation.` : `This will confirm ${shortlistedCount} projects as eligible for the "${awardName}" award. Projects remain in the main competition pool.` }

{eligibilityMode === 'SEPARATE_POOL' && !hasAwardRounds && (

No award rounds have been created yet. Projects will be confirmed but not routed to an evaluation track. Create rounds on the award page first.

)}
Cancel confirmMutation.mutate({ awardId })} disabled={confirmMutation.isPending} > {confirmMutation.isPending ? 'Confirming...' : 'Confirm'}
)}
setEligibilitySearch(e.target.value)} className="pl-9 h-9 max-w-sm" />
{filteredEligibilities.map((e, i) => { const reasoning = (e.aiReasoningJson as Record)?.reasoning as string | undefined const isTop5 = i < shortlistSize return ( ) })}
# Project Score Reasoning
All
{isTop5 ? ( {i + 1} ) : ( i + 1 )}

ev.stopPropagation()} > {e.project.title}

{[e.project.teamName, e.project.competitionCategory].filter(Boolean).length > 0 || e.project.country ? ( <> {[e.project.teamName, e.project.competitionCategory].filter(Boolean).join(', ')} {(e.project.teamName || e.project.competitionCategory) && e.project.country ? ', ' : ''} {e.project.country && } ) : '—'}

{e.qualityScore ?? 0}
{reasoning ? (

{reasoning}

) : ( )}
toggleMutation.mutate({ awardId, projectId: e.project.id }) } disabled={toggleMutation.isPending} />
) : (

No eligible projects found

)} )} {/* Not yet evaluated */} {expanded && !currentJobStatus && (

Click "Run Eligibility" to evaluate projects against this award's criteria

)} {/* Failed */} {currentJobStatus === 'FAILED' && (

Eligibility evaluation failed. Try again.

)}
) }