'use client' import { useState } from 'react' import { useParams } from 'next/navigation' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Skeleton } from '@/components/ui/skeleton' import { Badge } from '@/components/ui/badge' import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { cn } from '@/lib/utils' import { ArrowLeft, Save, Loader2, ChevronDown, Play, Square, Archive, Calendar, FileBox, Users, } from 'lucide-react' import { RoundConfigForm } from '@/components/admin/competition/round-config-form' import { ProjectStatesTable } from '@/components/admin/round/project-states-table' import { SubmissionWindowManager } from '@/components/admin/round/submission-window-manager' import { FileRequirementsEditor } from '@/components/admin/rounds/config/file-requirements-editor' const roundTypeColors: Record = { INTAKE: 'bg-gray-100 text-gray-700', FILTERING: 'bg-amber-100 text-amber-700', EVALUATION: 'bg-blue-100 text-blue-700', SUBMISSION: 'bg-purple-100 text-purple-700', MENTORING: 'bg-teal-100 text-teal-700', LIVE_FINAL: 'bg-red-100 text-red-700', DELIBERATION: 'bg-indigo-100 text-indigo-700', } const roundStatusConfig: Record = { ROUND_DRAFT: { label: 'Draft', bgClass: 'bg-gray-100 text-gray-700' }, ROUND_ACTIVE: { label: 'Active', bgClass: 'bg-emerald-100 text-emerald-700' }, ROUND_CLOSED: { label: 'Closed', bgClass: 'bg-blue-100 text-blue-700' }, ROUND_ARCHIVED: { label: 'Archived', bgClass: 'bg-muted text-muted-foreground' }, } export default function RoundDetailPage() { const params = useParams() const roundId = params.roundId as string const [config, setConfig] = useState>({}) const [hasChanges, setHasChanges] = useState(false) const [confirmAction, setConfirmAction] = useState(null) const [windowOpenAt, setWindowOpenAt] = useState('') const [windowCloseAt, setWindowCloseAt] = useState('') const utils = trpc.useUtils() const { data: round, isLoading } = trpc.round.getById.useQuery({ id: roundId }) // Fetch competition for jury groups (DELIBERATION) and awards const competitionId = round?.competitionId const { data: competition } = trpc.competition.getById.useQuery( { id: competitionId! }, { enabled: !!competitionId } ) const juryGroups = competition?.juryGroups?.map((g: any) => ({ id: g.id, name: g.name })) // Fetch awards linked to this round const programId = competition?.programId const { data: awards } = trpc.specialAward.list.useQuery( { programId: programId! }, { enabled: !!programId } ) // Filter awards by this round const roundAwards = awards?.filter((a) => a.evaluationRoundId === roundId) || [] // Update local config when round data changes if (round && !hasChanges) { const roundConfig = (round.configJson as Record) ?? {} if (JSON.stringify(roundConfig) !== JSON.stringify(config)) { setConfig(roundConfig) } // Sync date fields const openStr = round.windowOpenAt ? new Date(round.windowOpenAt).toISOString().slice(0, 16) : '' const closeStr = round.windowCloseAt ? new Date(round.windowCloseAt).toISOString().slice(0, 16) : '' if (openStr !== windowOpenAt) setWindowOpenAt(openStr) if (closeStr !== windowCloseAt) setWindowCloseAt(closeStr) } const updateMutation = trpc.round.update.useMutation({ onSuccess: () => { utils.round.getById.invalidate({ id: roundId }) toast.success('Round configuration saved') setHasChanges(false) }, onError: (err) => toast.error(err.message), }) // Round lifecycle mutations const activateMutation = trpc.roundEngine.activate.useMutation({ onSuccess: () => { utils.round.getById.invalidate({ id: roundId }) toast.success('Round activated') setConfirmAction(null) }, onError: (err) => toast.error(err.message), }) const closeMutation = trpc.roundEngine.close.useMutation({ onSuccess: () => { utils.round.getById.invalidate({ id: roundId }) toast.success('Round closed') setConfirmAction(null) }, onError: (err) => toast.error(err.message), }) const archiveMutation = trpc.roundEngine.archive.useMutation({ onSuccess: () => { utils.round.getById.invalidate({ id: roundId }) toast.success('Round archived') setConfirmAction(null) }, onError: (err) => toast.error(err.message), }) const handleConfigChange = (newConfig: Record) => { setConfig(newConfig) setHasChanges(true) } const handleSave = () => { updateMutation.mutate({ id: roundId, configJson: config, windowOpenAt: windowOpenAt ? new Date(windowOpenAt) : null, windowCloseAt: windowCloseAt ? new Date(windowCloseAt) : null, }) } const handleLifecycleAction = () => { if (confirmAction === 'activate') activateMutation.mutate({ roundId }) else if (confirmAction === 'close') closeMutation.mutate({ roundId }) else if (confirmAction === 'archive') archiveMutation.mutate({ roundId }) } const isLifecyclePending = activateMutation.isPending || closeMutation.isPending || archiveMutation.isPending if (isLoading) { return (
) } if (!round) { return (

Round Not Found

The requested round does not exist

) } const statusCfg = roundStatusConfig[round.status] ?? roundStatusConfig.ROUND_DRAFT const canActivate = round.status === 'ROUND_DRAFT' const canClose = round.status === 'ROUND_ACTIVE' const canArchive = round.status === 'ROUND_CLOSED' return (
{/* Header */}

Back to Rounds

{round.name}

{round.roundType.replace('_', ' ')} {/* Status Dropdown */} {canActivate && ( setConfirmAction('activate')}> Activate Round )} {canClose && ( setConfirmAction('close')}> Close Round )} {canArchive && ( <> setConfirmAction('archive')}> Archive Round )} {!canActivate && !canClose && !canArchive && ( No actions available )}

{round.slug}

{hasChanges && ( )}
{/* Summary Stats */}

{round._count?.projectRoundStates ?? 0}

Projects

{round.juryGroup?.members?.length ?? 0}

Jury Members

{round.juryGroup ? ( <>

{round.juryGroup.name}

Jury Group

) : ( <>

No jury assigned

Jury Group

)}
{round.windowOpenAt || round.windowCloseAt ? ( <>

{round.windowOpenAt && new Date(round.windowOpenAt).toLocaleDateString()} {round.windowOpenAt && round.windowCloseAt && ' - '} {round.windowCloseAt && new Date(round.windowCloseAt).toLocaleDateString()}

Schedule

) : ( <>

Not scheduled

Schedule

)}
{/* Schedule Editor */}

Round Schedule

{ setWindowOpenAt(e.target.value); setHasChanges(true) }} className="h-10" />
{ setWindowCloseAt(e.target.value); setHasChanges(true) }} className="h-10" />
{/* Tabs */} Configuration Projects Submission Windows Documents Awards {roundAwards.length > 0 && ( {roundAwards.length} )} {roundAwards.length === 0 ? (

No awards linked to this round

Create an award and set this round as its source round to see it here

) : (
{roundAwards.map((award) => { const eligibleCount = award._count?.eligibilities || 0 const autoTagRules = award.autoTagRulesJson as { rules?: unknown[] } | null const ruleCount = autoTagRules?.rules?.length || 0 return (

{award.name}

{award.eligibilityMode === 'SEPARATE_POOL' ? 'Separate Pool' : 'Stay in Main'}
{award.description && (

{award.description}

)}
{ruleCount}
{ruleCount === 1 ? 'rule' : 'rules'}
{eligibleCount}
eligible
) })}
)}
{/* Lifecycle Confirmation Dialog */} setConfirmAction(null)}> {confirmAction === 'activate' && 'Activate Round'} {confirmAction === 'close' && 'Close Round'} {confirmAction === 'archive' && 'Archive Round'} {confirmAction === 'activate' && 'This will open the round for submissions and evaluations. Projects will be able to enter this round.'} {confirmAction === 'close' && 'This will close the round. No more submissions or evaluations will be accepted.'} {confirmAction === 'archive' && 'This will archive the round. It will no longer appear in active views.'}
) }