'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 { Textarea } from '@/components/ui/textarea' import { Progress } from '@/components/ui/progress' import { UserAvatar } from '@/components/shared/user-avatar' import { AnimatedCard } from '@/components/shared/animated-container' import { Pagination } from '@/components/shared/pagination' import { toast } from 'sonner' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { Collapsible, CollapsibleContent, } from '@/components/ui/collapsible' import { ArrowLeft, Trophy, Users, CheckCircle2, ListChecks, BarChart3, Loader2, Bot, Crown, UserPlus, X, Play, Lock, Pencil, Trash2, Plus, Search, Vote, ChevronDown, AlertCircle, Layers, Info, Mail, } 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() // State declarations (before queries that depend on them) const [isPollingJob, setIsPollingJob] = useState(false) const pollingIntervalRef = useRef | null>(null) 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 [activeTab, setActiveTab] = useState('eligibility') const [addRoundOpen, setAddRoundOpen] = useState(false) const [roundForm, setRoundForm] = useState({ name: '', roundType: 'EVALUATION' as string }) const [notifyDialogOpen, setNotifyDialogOpen] = useState(false) const [notifyCustomMessage, setNotifyCustomMessage] = useState('') // Pagination for eligibility list const [eligibilityPage, setEligibilityPage] = useState(1) const eligibilityPerPage = 25 // Core queries — lazy-load tab-specific data based on activeTab const { data: award, isLoading, refetch } = trpc.specialAward.get.useQuery({ id: awardId }, { refetchInterval: 30_000 }) const { data: eligibilityData, refetch: refetchEligibility } = trpc.specialAward.listEligible.useQuery({ awardId, page: eligibilityPage, perPage: eligibilityPerPage, }, { enabled: activeTab === 'eligibility', }) const { data: jurors, refetch: refetchJurors } = trpc.specialAward.listJurors.useQuery({ awardId }, { enabled: activeTab === 'jurors', }) const { data: voteResults } = trpc.specialAward.getVoteResults.useQuery({ awardId }, { enabled: activeTab === 'results', }) const { data: awardRounds, refetch: refetchRounds } = trpc.specialAward.listRounds.useQuery({ awardId }, { enabled: activeTab === 'rounds', }) // Deferred queries - only load when needed const { data: allUsers } = trpc.user.list.useQuery( { role: 'JURY_MEMBER', page: 1, perPage: 100 }, { enabled: activeTab === 'jurors' } ) const { data: allProjects } = trpc.project.list.useQuery( { programId: award?.programId ?? '', perPage: 200 }, { enabled: !!award?.programId && addProjectDialogOpen } ) // 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 utils = trpc.useUtils() const invalidateAward = () => { utils.specialAward.get.invalidate({ id: awardId }) utils.specialAward.listEligible.invalidate({ awardId }) utils.specialAward.listJurors.invalidate({ awardId }) utils.specialAward.getVoteResults.invalidate({ awardId }) } const updateStatus = trpc.specialAward.updateStatus.useMutation({ onSuccess: invalidateAward, }) const runEligibility = trpc.specialAward.runEligibility.useMutation({ onSuccess: invalidateAward, }) const setEligibility = trpc.specialAward.setEligibility.useMutation({ onSuccess: () => utils.specialAward.listEligible.invalidate({ awardId }), }) const addJuror = trpc.specialAward.addJuror.useMutation({ onSuccess: () => utils.specialAward.listJurors.invalidate({ awardId }), }) const removeJuror = trpc.specialAward.removeJuror.useMutation({ onSuccess: () => utils.specialAward.listJurors.invalidate({ awardId }), }) const setWinner = trpc.specialAward.setWinner.useMutation({ onSuccess: invalidateAward, }) const deleteAward = trpc.specialAward.delete.useMutation({ onSuccess: () => utils.specialAward.list.invalidate(), }) const createRound = trpc.specialAward.createRound.useMutation({ onSuccess: () => { refetchRounds() setAddRoundOpen(false) setRoundForm({ name: '', roundType: 'EVALUATION' }) toast.success('Round created') }, onError: (err) => toast.error(err.message), }) const deleteRound = trpc.specialAward.deleteRound.useMutation({ onSuccess: () => { refetchRounds() toast.success('Round deleted') }, onError: (err) => toast.error(err.message), }) const { data: notifyStats } = trpc.specialAward.getNotificationStats.useQuery( { awardId }, { enabled: notifyDialogOpen } ) const notifyEligible = trpc.specialAward.notifyEligibleProjects.useMutation({ onSuccess: (result) => { toast.success(`Notified ${result.notified} projects (${result.emailsSent} emails sent${result.emailsFailed ? `, ${result.emailsFailed} failed` : ''})`) setNotifyDialogOpen(false) setNotifyCustomMessage('') }, onError: (err) => toast.error(err.message), }) 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(/_/g, ' ')} {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' && ( <> Notify Eligible Projects Send "Selected for {award.name}" emails to all {award.eligibleCount} eligible projects.
{notifyStats && (
{notifyStats.needsInvite > 0 && ( {notifyStats.needsInvite} will receive Create Account link )} {notifyStats.hasAccount > 0 && ( {notifyStats.hasAccount} will receive Dashboard link )}
)}