'use client' import { use, useEffect, useRef, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Switch } from '@/components/ui/switch' import { Label } from '@/components/ui/label' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Input } from '@/components/ui/input' import { Progress } from '@/components/ui/progress' import { UserAvatar } from '@/components/shared/user-avatar' import { Pagination } from '@/components/shared/pagination' import { toast } from 'sonner' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { ArrowLeft, Trophy, Users, CheckCircle2, Brain, BarChart3, Loader2, Crown, UserPlus, X, Play, Lock, Pencil, Trash2, Plus, Search, Vote, ChevronDown, AlertCircle, } from 'lucide-react' const STATUS_COLORS: Record = { DRAFT: 'secondary', NOMINATIONS_OPEN: 'default', VOTING_OPEN: 'default', CLOSED: 'outline', ARCHIVED: 'secondary', } // Status workflow steps for the step indicator const WORKFLOW_STEPS = [ { key: 'DRAFT', label: 'Draft' }, { key: 'NOMINATIONS_OPEN', label: 'Nominations' }, { key: 'VOTING_OPEN', label: 'Voting' }, { key: 'CLOSED', label: 'Closed' }, ] as const function getStepIndex(status: string): number { const idx = WORKFLOW_STEPS.findIndex((s) => s.key === status) return idx >= 0 ? idx : (status === 'ARCHIVED' ? 3 : 0) } function ConfidenceBadge({ confidence }: { confidence: number }) { if (confidence > 0.8) { return ( {Math.round(confidence * 100)}% ) } if (confidence >= 0.5) { return ( {Math.round(confidence * 100)}% ) } return ( {Math.round(confidence * 100)}% ) } export default function AwardDetailPage({ params, }: { params: Promise<{ id: string }> }) { const { id: awardId } = use(params) const router = useRouter() const { data: award, isLoading, refetch } = trpc.specialAward.get.useQuery({ id: awardId }) const { data: eligibilityData, refetch: refetchEligibility } = trpc.specialAward.listEligible.useQuery({ awardId, page: 1, perPage: 500, }) const { data: jurors, refetch: refetchJurors } = trpc.specialAward.listJurors.useQuery({ awardId }) const { data: voteResults } = trpc.specialAward.getVoteResults.useQuery({ awardId }) const { data: allUsers } = trpc.user.list.useQuery({ role: 'JURY_MEMBER', page: 1, perPage: 100 }) // Fetch all projects in the program for manual eligibility addition const { data: allProjects } = trpc.project.list.useQuery( { programId: award?.programId ?? '', perPage: 500 }, { enabled: !!award?.programId } ) const [isPollingJob, setIsPollingJob] = useState(false) const pollingIntervalRef = useRef | null>(null) // Eligibility job polling const { data: jobStatus, refetch: refetchJobStatus } = trpc.specialAward.getEligibilityJobStatus.useQuery( { awardId }, { enabled: isPollingJob } ) useEffect(() => { if (!isPollingJob) return pollingIntervalRef.current = setInterval(() => { refetchJobStatus() }, 2000) return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current) pollingIntervalRef.current = null } } }, [isPollingJob, refetchJobStatus]) // React to job status changes useEffect(() => { if (!jobStatus || !isPollingJob) return if (jobStatus.eligibilityJobStatus === 'COMPLETED') { setIsPollingJob(false) toast.success('Eligibility processing completed') refetchEligibility() refetch() } else if (jobStatus.eligibilityJobStatus === 'FAILED') { setIsPollingJob(false) toast.error(jobStatus.eligibilityJobError || 'Eligibility processing failed') } }, [jobStatus, isPollingJob, refetchEligibility, refetch]) // Check on mount if there's an ongoing job useEffect(() => { if (award?.eligibilityJobStatus === 'PROCESSING' || award?.eligibilityJobStatus === 'PENDING') { setIsPollingJob(true) } }, [award?.eligibilityJobStatus]) const updateStatus = trpc.specialAward.updateStatus.useMutation() const runEligibility = trpc.specialAward.runEligibility.useMutation() const setEligibility = trpc.specialAward.setEligibility.useMutation() const addJuror = trpc.specialAward.addJuror.useMutation() const removeJuror = trpc.specialAward.removeJuror.useMutation() const setWinner = trpc.specialAward.setWinner.useMutation() const deleteAward = trpc.specialAward.delete.useMutation() const [selectedJurorId, setSelectedJurorId] = useState('') const [includeSubmitted, setIncludeSubmitted] = useState(true) const [addProjectDialogOpen, setAddProjectDialogOpen] = useState(false) const [projectSearchQuery, setProjectSearchQuery] = useState('') const [expandedRows, setExpandedRows] = useState>(new Set()) const handleStatusChange = async ( status: 'DRAFT' | 'NOMINATIONS_OPEN' | 'VOTING_OPEN' | 'CLOSED' | 'ARCHIVED' ) => { try { await updateStatus.mutateAsync({ id: awardId, status }) toast.success(`Status updated to ${status.replace('_', ' ')}`) refetch() } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to update status' ) } } const handleRunEligibility = async () => { try { await runEligibility.mutateAsync({ awardId, includeSubmitted }) toast.success('Eligibility processing started') setIsPollingJob(true) } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to start eligibility' ) } } const handleToggleEligibility = async ( projectId: string, eligible: boolean ) => { try { await setEligibility.mutateAsync({ awardId, projectId, eligible }) refetchEligibility() } catch { toast.error('Failed to update eligibility') } } const handleAddJuror = async () => { if (!selectedJurorId) return try { await addJuror.mutateAsync({ awardId, userId: selectedJurorId }) toast.success('Juror added') setSelectedJurorId('') refetchJurors() } catch { toast.error('Failed to add juror') } } const handleRemoveJuror = async (userId: string) => { try { await removeJuror.mutateAsync({ awardId, userId }) refetchJurors() } catch { toast.error('Failed to remove juror') } } const handleSetWinner = async (projectId: string) => { try { await setWinner.mutateAsync({ awardId, projectId, overridden: true, }) toast.success('Winner set') refetch() } catch { toast.error('Failed to set winner') } } const handleDeleteAward = async () => { try { await deleteAward.mutateAsync({ id: awardId }) toast.success('Award deleted') router.push('/admin/awards') } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to delete award' ) } } const handleAddProjectToEligibility = async (projectId: string) => { try { await setEligibility.mutateAsync({ awardId, projectId, eligible: true }) toast.success('Project added to eligibility list') refetchEligibility() refetch() } catch { toast.error('Failed to add project') } } const handleRemoveFromEligibility = async (projectId: string) => { try { await setEligibility.mutateAsync({ awardId, projectId, eligible: false }) toast.success('Project removed from eligibility') refetchEligibility() refetch() } catch { toast.error('Failed to remove project') } } // Get projects that aren't already in the eligibility list const eligibleProjectIds = new Set( eligibilityData?.eligibilities.map((e) => e.projectId) || [] ) const availableProjects = allProjects?.projects.filter( (p) => !eligibleProjectIds.has(p.id) ) || [] const filteredAvailableProjects = availableProjects.filter( (p) => p.title.toLowerCase().includes(projectSearchQuery.toLowerCase()) || p.teamName?.toLowerCase().includes(projectSearchQuery.toLowerCase()) ) if (isLoading) { return (
) } if (!award) return null const jurorUserIds = new Set(jurors?.map((j) => j.userId) || []) const availableUsers = allUsers?.users.filter((u) => !jurorUserIds.has(u.id)) || [] return (
{/* Header */}

{award.name}

{award.status.replace('_', ' ')} {award.program.year} Edition {award.votingStartAt && ( Voting: {new Date(award.votingStartAt).toLocaleDateString()} - {award.votingEndAt ? new Date(award.votingEndAt).toLocaleDateString() : 'No end date'} )}
{award.status === 'DRAFT' && ( )} {award.status === 'NOMINATIONS_OPEN' && ( )} {award.status === 'VOTING_OPEN' && ( )} Delete Award? This will permanently delete "{award.name}" and all associated eligibility data, juror assignments, and votes. This action cannot be undone. Cancel {deleteAward.isPending ? ( ) : ( )} Delete Award
{/* Description */} {award.description && (

{award.description}

)} {/* Status Workflow Step Indicator */}
{WORKFLOW_STEPS.map((step, i) => { const currentIdx = getStepIndex(award.status) const isComplete = i < currentIdx const isCurrent = i === currentIdx return (
{isComplete ? ( ) : ( i + 1 )}
{step.label}
{i < WORKFLOW_STEPS.length - 1 && (
)}
) })}
{/* Stats Cards */}

Eligible

{award.eligibleCount}

Evaluated

{award._count.eligibilities}

Jurors

{award._count.jurors}

Votes

{award._count.votes}

{/* Tabs */} Eligibility ({award.eligibleCount}) Jurors ({award._count.jurors}) Results {/* Eligibility Tab */}

{award.eligibleCount} of {award._count.eligibilities} projects eligible

{award.useAiEligibility ? ( ) : ( )} Add Project to Eligibility List Manually add a project that wasn't included by AI or rule-based filtering
setProjectSearchQuery(e.target.value)} className="pl-9" />
{filteredAvailableProjects.length > 0 ? ( Project Category Country Action {filteredAvailableProjects.slice(0, 50).map((project) => (

{project.title}

{project.teamName}

{project.competitionCategory ? ( {project.competitionCategory.replace('_', ' ')} ) : ( '-' )} {project.country || '-'}
))}
) : (

{projectSearchQuery ? 'No projects match your search' : 'All projects are already in the eligibility list'}

)}
{filteredAvailableProjects.length > 50 && (

Showing first 50 of {filteredAvailableProjects.length} projects. Use search to filter.

)}
{/* Eligibility job progress */} {isPollingJob && jobStatus && (
{jobStatus.eligibilityJobStatus === 'PENDING' ? 'Preparing...' : `Processing... ${jobStatus.eligibilityJobDone ?? 0} of ${jobStatus.eligibilityJobTotal ?? '?'} projects`} {jobStatus.eligibilityJobTotal && jobStatus.eligibilityJobTotal > 0 && ( {Math.round( ((jobStatus.eligibilityJobDone ?? 0) / jobStatus.eligibilityJobTotal) * 100 )}% )}
0 ? ((jobStatus.eligibilityJobDone ?? 0) / jobStatus.eligibilityJobTotal) * 100 : 0 } />
)} {/* Failed job notice */} {!isPollingJob && award.eligibilityJobStatus === 'FAILED' && (
Last eligibility run failed: {award.eligibilityJobError || 'Unknown error'}
)} {!award.useAiEligibility && (

AI eligibility is off for this award. Projects are loaded for manual selection.

)} {eligibilityData && eligibilityData.eligibilities.length > 0 ? ( Project Category Country Method {award.useAiEligibility && AI Confidence} Eligible Actions {eligibilityData.eligibilities.map((e) => { const aiReasoning = e.aiReasoningJson as { confidence?: number; reasoning?: string } | null const hasReasoning = !!aiReasoning?.reasoning const isExpanded = expandedRows.has(e.id) return ( { setExpandedRows((prev) => { const next = new Set(prev) if (open) next.add(e.id) else next.delete(e.id) return next }) }} asChild> <>
{hasReasoning && ( )}

{e.project.title}

{e.project.teamName}

{e.project.competitionCategory ? ( {e.project.competitionCategory.replace('_', ' ')} ) : ( '-' )} {e.project.country || '-'} {e.method === 'MANUAL' ? 'Manual' : 'Auto'} {award.useAiEligibility && ( {aiReasoning?.confidence != null ? ( AI confidence: {Math.round(aiReasoning.confidence * 100)}% ) : ( - )} )} handleToggleEligibility(e.projectId, checked) } />
{hasReasoning && (
)} ) })}

AI Reasoning

{aiReasoning?.reasoning}

) : (

No eligibility data yet

{award.useAiEligibility ? 'Run AI eligibility to automatically evaluate projects against this award\'s criteria, or manually add projects.' : 'Load all eligible projects into the evaluation list, or manually add specific projects.'}

)}
{/* Jurors Tab */}
{jurors && jurors.length > 0 ? ( Member Role Actions {jurors.map((j) => (

{j.user.name || 'Unnamed'}

{j.user.email}

{j.user.role.replace('_', ' ')}
))}
) : (

No jurors assigned

Add jury members who will vote on eligible projects for this award. Select from existing jury members above.

)}
{/* Results Tab */} {voteResults && voteResults.results.length > 0 ? (() => { const maxPoints = Math.max(...voteResults.results.map((r) => r.points), 1) return ( <>
{voteResults.votedJurorCount} of {voteResults.jurorCount}{' '} jurors voted {voteResults.scoringMode.replace('_', ' ')}
# Project Votes Score Actions {voteResults.results.map((r, i) => { const isWinner = r.project.id === voteResults.winnerId const barPercent = (r.points / maxPoints) * 100 return ( {i + 1}
{isWinner && ( )}

{r.project.title}

{r.project.teamName}

{r.votes}
{r.points}
{!isWinner && ( )} ) })}
) })() : (

No votes yet

{award._count.jurors === 0 ? 'Assign jurors to this award first, then open voting to collect their selections.' : award.status === 'DRAFT' || award.status === 'NOMINATIONS_OPEN' ? 'Open voting to allow jurors to submit their selections for this award.' : 'Votes will appear here as jurors submit their selections.'}

{award.status === 'NOMINATIONS_OPEN' && ( )}
)}
) }