'use client' import { Suspense, useState } from 'react' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { RoundTypeSettings } from '@/components/forms/round-type-settings' import { ROUND_FIELD_VISIBILITY } from '@/types/round-settings' import { ArrowLeft, Loader2, AlertCircle, Bell, LayoutTemplate } from 'lucide-react' import { toast } from 'sonner' import { DateTimePicker } from '@/components/ui/datetime-picker' // Available notification types for teams entering a round const TEAM_NOTIFICATION_OPTIONS = [ { value: '', label: 'No automatic notification', description: 'Teams will not receive a notification when entering this round' }, { value: 'ADVANCED_SEMIFINAL', label: 'Advanced to Semi-Finals', description: 'Congratulates team for advancing to semi-finals' }, { value: 'ADVANCED_FINAL', label: 'Selected as Finalist', description: 'Congratulates team for being selected as finalist' }, { value: 'NOT_SELECTED', label: 'Not Selected', description: 'Informs team they were not selected to continue' }, { value: 'WINNER_ANNOUNCEMENT', label: 'Winner Announcement', description: 'Announces the team as a winner' }, { value: 'SUBMISSION_RECEIVED', label: 'Submission Received', description: 'Confirms to the team that their submission has been received' }, ] const createRoundSchema = z.object({ programId: z.string().min(1, 'Please select a program'), name: z.string().min(1, 'Name is required').max(255), requiredReviews: z.number().int().min(0).max(10), votingStartAt: z.date().nullable().optional(), votingEndAt: z.date().nullable().optional(), }).refine((data) => { if (data.votingStartAt && data.votingEndAt) { return data.votingEndAt > data.votingStartAt } return true }, { message: 'End date must be after start date', path: ['votingEndAt'], }) type CreateRoundForm = z.infer function CreateRoundContent() { const router = useRouter() const searchParams = useSearchParams() const programIdParam = searchParams.get('program') const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION') const [roundSettings, setRoundSettings] = useState>({}) const [entryNotificationType, setEntryNotificationType] = useState('') const [selectedTemplateId, setSelectedTemplateId] = useState('') const utils = trpc.useUtils() const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery() const { data: templates } = trpc.roundTemplate.list.useQuery() const loadTemplate = (templateId: string) => { if (!templateId || !templates) return const template = templates.find((t: { id: string; name: string; roundType: string; settingsJson: unknown }) => t.id === templateId) if (!template) return // Apply template settings const typeMap: Record = { EVALUATION: 'EVALUATION', SELECTION: 'EVALUATION', FINAL: 'EVALUATION', LIVE_VOTING: 'LIVE_EVENT', FILTERING: 'FILTERING', } setRoundType(typeMap[template.roundType] || 'EVALUATION') if (template.settingsJson && typeof template.settingsJson === 'object') { setRoundSettings(template.settingsJson as Record) } if (template.name) { form.setValue('name', template.name) } setSelectedTemplateId(templateId) toast.success(`Loaded template: ${template.name}`) } const createRound = trpc.round.create.useMutation({ onSuccess: (data) => { utils.program.list.invalidate({ includeRounds: true }) router.push(`/admin/rounds/${data.id}`) }, }) const form = useForm({ resolver: zodResolver(createRoundSchema), defaultValues: { programId: programIdParam || '', name: '', requiredReviews: 3, votingStartAt: null, votingEndAt: null, }, }) const onSubmit = async (data: CreateRoundForm) => { const visibility = ROUND_FIELD_VISIBILITY[roundType] await createRound.mutateAsync({ programId: data.programId, name: data.name, roundType, requiredReviews: visibility?.showRequiredReviews ? data.requiredReviews : 0, settingsJson: roundSettings, votingStartAt: visibility?.showVotingWindow ? (data.votingStartAt ?? undefined) : undefined, votingEndAt: visibility?.showVotingWindow ? (data.votingEndAt ?? undefined) : undefined, entryNotificationType: entryNotificationType || undefined, }) } if (loadingPrograms) { return } if (!programs || programs.length === 0) { return (

No Programs Found

Create a program first before creating rounds

) } return (
{/* Header */}

Create Round

Set up a new selection round for project evaluation

{/* Template Selector */} {templates && templates.length > 0 && ( Start from Template Load settings from a saved template to get started quickly
{selectedTemplateId && ( )}
)} {/* Form */}
Basic Information ( Edition )} /> ( Round Name A descriptive name for this selection round )} /> {/* Round Type & Settings */} ( Required Reviews per Project field.onChange(parseInt(e.target.value) || 1)} /> Minimum number of evaluations each project should receive )} /> } /> {ROUND_FIELD_VISIBILITY[roundType]?.showVotingWindow && ( Voting Window Optional: Set when jury members can submit their evaluations
( Start Date & Time )} /> ( End Date & Time )} />

Leave empty to set the voting window later. Past dates are allowed.

)} {/* Team Notification */} Team Notification Notification sent to project teams when they enter this round

When projects advance to this round, the selected notification will be sent to the project team automatically.

{/* Error */} {createRound.error && (

{(() => { const msg = createRound.error.message try { const parsed = JSON.parse(msg) if (Array.isArray(parsed)) { return parsed.map((e: { message?: string; path?: string[] }) => e.message || 'Validation error' ).join('. ') } } catch { // Not JSON, use as-is } return msg })()}

)} {/* Actions */}
) } function CreateRoundSkeleton() { return (
) } export default function CreateRoundPage() { return ( }> ) }