'use client' import { useState, useEffect, useCallback, useTransition } from 'react' import { useRouter } from 'next/navigation' import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' import { useDebouncedCallback } from 'use-debounce' import { trpc } from '@/lib/trpc/client' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' import { Slider } from '@/components/ui/slider' import { Switch } from '@/components/ui/switch' import { Badge } from '@/components/ui/badge' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Loader2, Save, Send, CheckCircle2, AlertCircle, Clock, Star, ThumbsUp, ThumbsDown, } from 'lucide-react' import { cn } from '@/lib/utils' // Define criterion type from the evaluation form JSON interface Criterion { id: string label: string description?: string scale: number // max value (e.g., 5 or 10) weight?: number required?: boolean } interface EvaluationFormProps { assignmentId: string evaluationId: string | null projectTitle: string criteria: Criterion[] initialData?: { criterionScoresJson: Record | null globalScore: number | null binaryDecision: boolean | null feedbackText: string | null status: string } isVotingOpen: boolean deadline?: Date | null } const createEvaluationSchema = (criteria: Criterion[]) => z.object({ criterionScores: z.record(z.number()), globalScore: z.number().int().min(1).max(10), binaryDecision: z.boolean(), feedbackText: z.string().min(10, 'Please provide at least 10 characters of feedback'), }) type EvaluationFormData = z.infer> export function EvaluationForm({ assignmentId, evaluationId, projectTitle, criteria, initialData, isVotingOpen, deadline, }: EvaluationFormProps) { const router = useRouter() const [isPending, startTransition] = useTransition() const [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle') const [lastSaved, setLastSaved] = useState(null) // Initialize criterion scores with existing data or defaults const defaultCriterionScores: Record = {} criteria.forEach((c) => { defaultCriterionScores[c.id] = initialData?.criterionScoresJson?.[c.id] ?? Math.ceil(c.scale / 2) }) const form = useForm({ resolver: zodResolver(createEvaluationSchema(criteria)), defaultValues: { criterionScores: defaultCriterionScores, globalScore: initialData?.globalScore ?? 5, binaryDecision: initialData?.binaryDecision ?? false, feedbackText: initialData?.feedbackText ?? '', }, mode: 'onChange', }) const { watch, handleSubmit, control, formState } = form const { errors, isValid, isDirty } = formState // tRPC mutations const startEvaluation = trpc.evaluation.start.useMutation() const autosave = trpc.evaluation.autosave.useMutation() const submit = trpc.evaluation.submit.useMutation() const utils = trpc.useUtils() // State to track the current evaluation ID (might be created on first autosave) const [currentEvaluationId, setCurrentEvaluationId] = useState(evaluationId) // Create evaluation if it doesn't exist useEffect(() => { if (!currentEvaluationId && isVotingOpen) { startEvaluation.mutate( { assignmentId }, { onSuccess: (data) => { setCurrentEvaluationId(data.id) }, } ) } }, [assignmentId, currentEvaluationId, isVotingOpen]) // Debounced autosave function const debouncedAutosave = useDebouncedCallback( async (data: EvaluationFormData) => { if (!currentEvaluationId || !isVotingOpen) return setAutosaveStatus('saving') try { await autosave.mutateAsync({ id: currentEvaluationId, criterionScoresJson: data.criterionScores, globalScore: data.globalScore, binaryDecision: data.binaryDecision, feedbackText: data.feedbackText, }) setAutosaveStatus('saved') setLastSaved(new Date()) // Reset to idle after a few seconds setTimeout(() => setAutosaveStatus('idle'), 3000) } catch (error) { console.error('Autosave failed:', error) setAutosaveStatus('error') } }, 3000 // 3 second debounce ) // Watch form values and trigger autosave const watchedValues = watch() useEffect(() => { if (isDirty && isVotingOpen) { debouncedAutosave(watchedValues) } }, [watchedValues, isDirty, isVotingOpen, debouncedAutosave]) // Submit handler const onSubmit = async (data: EvaluationFormData) => { if (!currentEvaluationId) return try { await submit.mutateAsync({ id: currentEvaluationId, criterionScoresJson: data.criterionScores, globalScore: data.globalScore, binaryDecision: data.binaryDecision, feedbackText: data.feedbackText, }) // Invalidate queries and redirect utils.assignment.myAssignments.invalidate() startTransition(() => { router.push(`/jury/projects/${assignmentId.split('-')[0]}/evaluation`) router.refresh() }) } catch (error) { console.error('Submit failed:', error) } } const isSubmitted = initialData?.status === 'SUBMITTED' || initialData?.status === 'LOCKED' const isReadOnly = isSubmitted || !isVotingOpen return (
{/* Status bar */}

{projectTitle}

{!isReadOnly && (
Submit Evaluation? Once submitted, you cannot edit your evaluation. Please review your scores and feedback before confirming. Cancel {submit.isPending ? ( ) : null} Confirm Submit
)} {isReadOnly && ( {isSubmitted ? 'Submitted' : 'Read Only'} )}
{/* Criteria scoring */} {criteria.length > 0 && ( Evaluation Criteria Rate the project on each criterion below {criteria.map((criterion) => ( ))} )} {/* Global score */} Overall Score Rate the project overall on a scale of 1 to 10 (
Poor {field.value} Excellent
field.onChange(v[0])} disabled={isReadOnly} className="py-4" />
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => ( ))}
)} />
{/* Binary decision */} Recommendation Do you recommend this project to advance to the next round? (
)} />
{/* Feedback text */} Written Feedback Provide constructive feedback for this project (minimum 10 characters) (