'use client' import { useState } from 'react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { Plus, Layers, Calendar, ChevronDown, ChevronRight, Settings, Users, FileBox, Save, Loader2, } from 'lucide-react' import { useEdition } from '@/contexts/edition-context' const ROUND_TYPES = [ { value: 'INTAKE', label: 'Intake' }, { value: 'FILTERING', label: 'Filtering' }, { value: 'EVALUATION', label: 'Evaluation' }, { value: 'SUBMISSION', label: 'Submission' }, { value: 'MENTORING', label: 'Mentoring' }, { value: 'LIVE_FINAL', label: 'Live Final' }, { value: 'DELIBERATION', label: 'Deliberation' }, ] as const 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 statusConfig = { DRAFT: { label: 'Draft', bgClass: 'bg-gray-100 text-gray-700', dotClass: 'bg-gray-500' }, ACTIVE: { label: 'Active', bgClass: 'bg-emerald-100 text-emerald-700', dotClass: 'bg-emerald-500' }, CLOSED: { label: 'Closed', bgClass: 'bg-blue-100 text-blue-700', dotClass: 'bg-blue-500' }, ARCHIVED: { label: 'Archived', bgClass: 'bg-muted text-muted-foreground', dotClass: 'bg-muted-foreground' }, } as const const roundStatusColors: Record = { ROUND_DRAFT: 'bg-gray-100 text-gray-600', ROUND_ACTIVE: 'bg-emerald-100 text-emerald-700', ROUND_CLOSED: 'bg-blue-100 text-blue-700', ROUND_ARCHIVED: 'bg-muted text-muted-foreground', } export default function RoundsPage() { const { currentEdition } = useEdition() const programId = currentEdition?.id const utils = trpc.useUtils() const [addRoundOpen, setAddRoundOpen] = useState(false) const [roundForm, setRoundForm] = useState({ name: '', roundType: '', competitionId: '' }) const [expandedCompetitions, setExpandedCompetitions] = useState>(new Set()) const [editingCompetition, setEditingCompetition] = useState(null) const [competitionEdits, setCompetitionEdits] = useState>({}) const [filterType, setFilterType] = useState('all') const { data: competitions, isLoading } = trpc.competition.list.useQuery( { programId: programId! }, { enabled: !!programId } ) const createRoundMutation = trpc.round.create.useMutation({ onSuccess: () => { utils.competition.list.invalidate() toast.success('Round created') setAddRoundOpen(false) setRoundForm({ name: '', roundType: '', competitionId: '' }) }, onError: (err) => toast.error(err.message), }) const updateCompMutation = trpc.competition.update.useMutation({ onSuccess: () => { utils.competition.list.invalidate() toast.success('Competition settings saved') setEditingCompetition(null) setCompetitionEdits({}) }, onError: (err) => toast.error(err.message), }) const toggleExpanded = (id: string) => { setExpandedCompetitions((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const handleCreateRound = () => { if (!roundForm.name.trim() || !roundForm.roundType || !roundForm.competitionId) { toast.error('All fields are required') return } const slug = roundForm.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') const comp = competitions?.find((c) => c.id === roundForm.competitionId) const nextOrder = comp ? (comp as any).rounds?.length ?? comp._count.rounds : 0 createRoundMutation.mutate({ competitionId: roundForm.competitionId, name: roundForm.name.trim(), slug, roundType: roundForm.roundType as any, sortOrder: nextOrder, }) } const startEditCompetition = (comp: any) => { setEditingCompetition(comp.id) setCompetitionEdits({ name: comp.name, categoryMode: comp.categoryMode, startupFinalistCount: comp.startupFinalistCount, conceptFinalistCount: comp.conceptFinalistCount, notifyOnRoundAdvance: comp.notifyOnRoundAdvance, notifyOnDeadlineApproach: comp.notifyOnDeadlineApproach, }) } const saveCompetitionEdit = (id: string) => { updateCompMutation.mutate({ id, ...competitionEdits } as any) } if (!programId) { return (

Rounds

No Edition Selected

Select an edition from the sidebar

) } return (
{/* Header */}

Rounds

Manage all competition rounds for {currentEdition?.name}

{/* Filter */}
{/* Loading */} {isLoading && (
{[1, 2].map((i) => ( ))}
)} {/* Empty State */} {!isLoading && (!competitions || competitions.length === 0) && (

No Competitions Yet

Create a competition first, then add rounds to define the evaluation flow.

)} {/* Competition Groups with Rounds */} {competitions && competitions.length > 0 && (
{competitions.map((comp) => { const status = comp.status as keyof typeof statusConfig const cfg = statusConfig[status] || statusConfig.DRAFT const isExpanded = expandedCompetitions.has(comp.id) || competitions.length === 1 const isEditing = editingCompetition === comp.id return ( toggleExpanded(comp.id)} onStartEdit={() => startEditCompetition(comp)} onCancelEdit={() => { setEditingCompetition(null); setCompetitionEdits({}) }} onSaveEdit={() => saveCompetitionEdit(comp.id)} onEditChange={setCompetitionEdits} /> ) })}
)} {/* Add Round Dialog */} Add Round Create a new round in a competition.
setRoundForm({ ...roundForm, name: e.target.value })} />
) } // ─── Competition Group Component ───────────────────────────────────────────── type CompetitionGroupProps = { competition: any statusConfig: { label: string; bgClass: string; dotClass: string } isExpanded: boolean isEditing: boolean competitionEdits: Record filterType: string updateCompMutation: any onToggle: () => void onStartEdit: () => void onCancelEdit: () => void onSaveEdit: () => void onEditChange: (edits: Record) => void } function CompetitionGroup({ competition: comp, statusConfig: cfg, isExpanded, isEditing, competitionEdits, filterType, updateCompMutation, onToggle, onStartEdit, onCancelEdit, onSaveEdit, onEditChange, }: CompetitionGroupProps) { // We need to fetch rounds for this competition const { data: compDetail } = trpc.competition.getById.useQuery( { id: comp.id }, { enabled: isExpanded } ) const rounds = compDetail?.rounds ?? [] const filteredRounds = filterType === 'all' ? rounds : rounds.filter((r: any) => r.roundType === filterType) return ( {/* Competition Header */}
{comp.name} {cfg.label} {comp._count.rounds} rounds {comp._count.juryGroups} juries
{/* Inline Competition Settings Editor */} {isEditing && (
onEditChange({ ...competitionEdits, name: e.target.value })} />
onEditChange({ ...competitionEdits, categoryMode: e.target.value })} />
onEditChange({ ...competitionEdits, startupFinalistCount: parseInt(e.target.value, 10) || 10 })} />
onEditChange({ ...competitionEdits, conceptFinalistCount: parseInt(e.target.value, 10) || 10 })} />
onEditChange({ ...competitionEdits, notifyOnRoundAdvance: v })} />
onEditChange({ ...competitionEdits, notifyOnDeadlineApproach: v })} />
)} {/* Rounds List */} {isExpanded && ( {!compDetail ? (
) : filteredRounds.length === 0 ? (

{filterType !== 'all' ? 'No rounds match the filter.' : 'No rounds configured.'}

) : (
{filteredRounds.map((round: any, index: number) => (
{round.sortOrder + 1}

{round.name}

{round.slug}

{round.roundType.replace('_', ' ')}
))}
)}
)}
) }