'use client' import { useState, useEffect } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { FileSpreadsheet, Download, BarChart3, Users, ClipboardList, CheckCircle2, TrendingUp, GitCompare, UserCheck, Globe, Layers, Trophy, ArrowRight, Hash, } from 'lucide-react' import { formatDateOnly } from '@/lib/utils' import { ScoreDistributionChart, EvaluationTimelineChart, StatusBreakdownChart, JurorWorkloadChart, ProjectRankingsChart, CriteriaScoresChart, GeographicDistribution, CrossStageComparisonChart, JurorConsistencyChart, DiversityMetricsChart, } from '@/components/charts' import { ExportPdfButton } from '@/components/shared/export-pdf-button' import { AnimatedCard } from '@/components/shared/animated-container' function ReportsOverview() { const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true }) const { data: dashStats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery() // Flatten stages from all programs const rounds = programs?.flatMap(p => ((p.stages ?? []) as Array<{ id: string; name: string; status: string; votingEndAt?: string | Date | null }>).map((s: { id: string; name: string; status: string; votingEndAt?: string | Date | null }) => ({ ...s, programId: p.id, programName: `${p.year} Edition` })) ) || [] // Project reporting scope (default: latest program, all rounds) const [selectedValue, setSelectedValue] = useState(null) useEffect(() => { if (programs?.length && !selectedValue) { setSelectedValue(`all:${programs[0].id}`) } }, [programs, selectedValue]) const scopeInput = parseSelection(selectedValue) const hasScope = !!scopeInput.roundId || !!scopeInput.programId const { data: projectRankings, isLoading: projectsLoading } = trpc.analytics.getProjectRankings.useQuery( { ...scopeInput, limit: 5000 }, { enabled: hasScope } ) if (isLoading || statsLoading) { return (
{[...Array(4)].map((_, i) => ( ))}
) } const totalPrograms = dashStats?.programCount ?? programs?.length ?? 0 const totalProjects = dashStats?.projectCount ?? 0 const activeRounds = dashStats?.activeRoundCount ?? rounds.filter((r: { status: string }) => r.status === 'ROUND_ACTIVE').length const jurorCount = dashStats?.jurorCount ?? 0 const submittedEvaluations = dashStats?.submittedEvaluations ?? 0 const totalAssignments = dashStats?.totalAssignments ?? 0 const completionRate = dashStats?.completionRate ?? 0 return (
{/* Quick Stats */}

Programs

{totalPrograms}

{activeRounds} active round{activeRounds !== 1 ? 's' : ''}

Total Projects

{totalProjects}

Across all programs

Jury Members

{jurorCount}

Active jurors

Evaluations

{submittedEvaluations}

{totalAssignments > 0 ? `${completionRate}% completion rate` : 'No assignments yet'}

{/* Score Distribution (if any evaluations exist) */} {dashStats?.scoreDistribution && dashStats.scoreDistribution.some(b => b.count > 0) && (
Score Distribution
Overall score distribution across all evaluations
{dashStats.scoreDistribution.map((bucket) => { const maxCount = Math.max(...dashStats.scoreDistribution.map(b => b.count), 1) return (
{bucket.label}
{bucket.count}
) })}
)} {/* Project Reports Summary */}
Project Reports
Summary dashboard — optionally filter to a specific round
{projectsLoading ? ( ) : projectRankings?.length ? ( <> {/* Summary stats row */} {(() => { const evaluated = projectRankings.filter(p => p.averageScore !== null) const scores = evaluated.map(p => p.averageScore as number) const avgScore = scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : 0 const minScore = scores.length ? Math.min(...scores) : 0 const maxScore = scores.length ? Math.max(...scores) : 0 const evalPercent = projectRankings.length ? Math.round((evaluated.length / projectRankings.length) * 100) : 0 const statusCounts = projectRankings.reduce((acc, p) => { acc[p.status] = (acc[p.status] || 0) + 1 return acc }, {} as Record) return ( <>

Total Projects

{projectRankings.length}

Avg Score

{avgScore ? avgScore.toFixed(1) : '-'}

Evaluated

{evalPercent}%

Score Range

{scores.length ? `${minScore.toFixed(1)}–${maxScore.toFixed(1)}` : '-'}

{/* Status breakdown chips */} {Object.keys(statusCounts).length > 0 && (
{Object.entries(statusCounts) .sort(([, a], [, b]) => b - a) .map(([status, count]) => ( {formatStatusLabel(status)} {count} ))}
)} {/* Top 10 ranked table */}

Top 10 by Average Score

# Project Team Avg Evals Status {projectRankings.slice(0, 10).map((p, idx) => ( {idx + 1} {p.title} {p.teamName || '-'} {p.averageScore === null ? '-' : p.averageScore.toFixed(2)} {p.evaluationCount} {formatStatusLabel(p.status)} ))}
{/* Link to full analytics */} {projectRankings.length > 10 && (
)} ) })()} ) : (

No project report data available for the selected scope yet.

)}
{/* Round exports (still available) */}
Round Exports
Download round-level evaluations and results
{rounds.length === 0 ? (

No rounds created yet.

) : ( Round Program Status Export {rounds.map((round) => (

{round.name}

{round.votingEndAt && (

Ends: {formatDateOnly(round.votingEndAt)}

)}
{round.programName} {round.status?.replace('ROUND_', '') || round.status}
))}
)}
) } // Parse selection value: "all:programId" for edition-wide, or roundId function parseSelection(value: string | null): { roundId?: string; programId?: string } { if (!value) return {} if (value.startsWith('all:')) return { programId: value.slice(4) } return { roundId: value } } // Map raw DB status to display-friendly labels function formatStatusLabel(status: string): string { const labels: Record = { ELIGIBLE: 'Special Award', ASSIGNED: 'Assigned', PENDING: 'Pending', IN_PROGRESS: 'In Progress', PASSED: 'Passed', REJECTED: 'Rejected', COMPLETED: 'Completed', WITHDRAWN: 'Withdrawn', } return labels[status] ?? status } // Find the best default round: active > last closed > first function findDefaultRound(rounds: Array<{ id: string; status?: string }>): string | undefined { const active = rounds.find(r => r.status === 'ROUND_ACTIVE') if (active) return active.id const closed = [...rounds].reverse().find(r => r.status === 'ROUND_CLOSED') if (closed) return closed.id return rounds[0]?.id } function StageAnalytics() { const [selectedValue, setSelectedValue] = useState(null) const { data: programs, isLoading: roundsLoading } = trpc.program.list.useQuery({ includeStages: true }) // Flatten stages from all programs with program name const rounds = programs?.flatMap(p => ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ ...s, programId: p.id, programName: `${p.year} Edition` })) ) || [] // Set default selected stage — prefer active round useEffect(() => { if (rounds.length && !selectedValue) { setSelectedValue(findDefaultRound(rounds) ?? rounds[0].id) } }, [rounds.length, selectedValue]) const queryInput = parseSelection(selectedValue) const hasSelection = !!queryInput.roundId || !!queryInput.programId const { data: scoreDistribution, isLoading: scoreLoading } = trpc.analytics.getScoreDistribution.useQuery( queryInput, { enabled: hasSelection } ) const { data: timeline, isLoading: timelineLoading } = trpc.analytics.getEvaluationTimeline.useQuery( queryInput, { enabled: hasSelection } ) const { data: statusBreakdown, isLoading: statusLoading } = trpc.analytics.getStatusBreakdown.useQuery( queryInput, { enabled: hasSelection } ) const { data: jurorWorkload, isLoading: workloadLoading } = trpc.analytics.getJurorWorkload.useQuery( queryInput, { enabled: hasSelection } ) const { data: projectRankings, isLoading: rankingsLoading } = trpc.analytics.getProjectRankings.useQuery( { ...queryInput, limit: 15 }, { enabled: hasSelection } ) const { data: criteriaScores, isLoading: criteriaLoading } = trpc.analytics.getCriteriaScores.useQuery( queryInput, { enabled: hasSelection } ) const selectedRound = rounds.find((r) => r.id === selectedValue) const geoInput = queryInput.programId ? { programId: queryInput.programId } : { programId: selectedRound?.programId || '', roundId: queryInput.roundId } const { data: geoData, isLoading: geoLoading } = trpc.analytics.getGeographicDistribution.useQuery( geoInput, { enabled: hasSelection && !!(geoInput.programId || geoInput.roundId) } ) if (roundsLoading) { return (
) } if (!rounds?.length) { return (

No rounds available

Create a round to view analytics

) } return (
{/* Round Selector */}
{hasSelection && (
{/* Row 1: Score Distribution & Status Breakdown */}
{scoreLoading ? ( ) : scoreDistribution ? ( ) : null} {statusLoading ? ( ) : statusBreakdown ? ( ) : null}
{/* Row 2: Evaluation Timeline */} {timelineLoading ? ( ) : timeline?.length ? ( ) : (

No evaluation data available yet

)} {/* Row 3: Criteria Scores */} {criteriaLoading ? ( ) : criteriaScores?.length ? ( ) : null} {/* Row 4: Juror Workload */} {workloadLoading ? ( ) : jurorWorkload?.length ? ( ) : (

No juror assignments yet

)} {/* Row 5: Project Rankings */} {rankingsLoading ? ( ) : projectRankings?.length ? ( ) : (

No project scores available yet

)} {/* Row 6: Geographic Distribution */} {geoLoading ? ( ) : geoData?.length ? ( ) : null}
)}
) } function CrossStageTab() { const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap((p) => ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ id: s.id, name: s.name, programName: `${p.year} Edition` })) ) || [] const [selectedRoundIds, setSelectedRoundIds] = useState([]) const { data: comparison, isLoading: comparisonLoading } = trpc.analytics.getCrossRoundComparison.useQuery( { roundIds: selectedRoundIds }, { enabled: selectedRoundIds.length >= 2 } ) const toggleRound = (roundId: string) => { setSelectedRoundIds((prev) => prev.includes(roundId) ? prev.filter((id) => id !== roundId) : [...prev, roundId] ) } if (programsLoading) { return } return (
{/* Stage selector */} Select Stages to Compare Choose at least 2 stages to compare metrics side by side
{stages.map((stage) => { const isSelected = selectedRoundIds.includes(stage.id) return ( toggleRound(stage.id)} > {stage.name} ) })}
{selectedRoundIds.length < 2 && (

Select at least 2 stages to enable comparison

)}
{/* Comparison charts */} {comparisonLoading && selectedRoundIds.length >= 2 && (
)} {comparison && ( } /> )}
) } function JurorConsistencyTab() { const [selectedValue, setSelectedValue] = useState(null) const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap((p) => ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programId: p.id, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (stages.length && !selectedValue) { setSelectedValue(findDefaultRound(stages) ?? stages[0].id) } }, [stages.length, selectedValue]) const queryInput = parseSelection(selectedValue) const hasSelection = !!queryInput.roundId || !!queryInput.programId const { data: consistency, isLoading: consistencyLoading } = trpc.analytics.getJurorConsistency.useQuery( queryInput, { enabled: hasSelection } ) if (programsLoading) { return } return (
{consistencyLoading && } {consistency && ( }} /> )}
) } function DiversityTab() { const [selectedValue, setSelectedValue] = useState(null) const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap((p) => ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programId: p.id, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (stages.length && !selectedValue) { setSelectedValue(findDefaultRound(stages) ?? stages[0].id) } }, [stages.length, selectedValue]) const queryInput = parseSelection(selectedValue) const hasSelection = !!queryInput.roundId || !!queryInput.programId const { data: diversity, isLoading: diversityLoading } = trpc.analytics.getDiversityMetrics.useQuery( queryInput, { enabled: hasSelection } ) if (programsLoading) { return } return (
{diversityLoading && } {diversity && ( )}
) } function RoundPipelineTab() { const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true }) const rounds = programs?.flatMap(p => ((p.stages ?? []) as Array<{ id: string; name: string; status: string; type?: string }>).map((s) => ({ ...s, programId: p.id, programName: `${p.year} Edition`, })) ) || [] const roundIds = rounds.map(r => r.id) const { data: comparison, isLoading: comparisonLoading } = trpc.analytics.getCrossRoundComparison.useQuery( { roundIds }, { enabled: roundIds.length >= 2 } ) if (isLoading || comparisonLoading) { return (
{[1, 2, 3].map(i => )}
) } if (!rounds.length) { return (

No rounds available

) } const comparisonMap = new Map( (comparison ?? []).map((c: any) => [c.roundId, c]) ) return (
Round Pipeline
Project flow across competition rounds
{rounds.map((round, idx) => { const stats = comparisonMap.get(round.id) as any return (
{idx + 1}

{round.name}

{round.programName}

{stats?.projectCount ?? 0} projects {stats?.evaluationCount ?? 0} evals {round.status?.replace('ROUND_', '') ?? 'DRAFT'}
{stats?.completionRate != null && ( )}
) })}
) } export default function ReportsPage() { const [pdfStageId, setPdfStageId] = useState(null) const { data: pdfPrograms } = trpc.program.list.useQuery({ includeStages: true }) const pdfStages = pdfPrograms?.flatMap((p) => ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (pdfStages.length && !pdfStageId) { setPdfStageId(findDefaultRound(pdfStages) ?? pdfStages[0].id) } }, [pdfStages.length, pdfStageId]) const selectedPdfStage = pdfStages.find((r) => r.id === pdfStageId) return (
{/* Header */}

Reports

View progress, analytics, and export evaluation data

{/* Tabs */}
Overview Analytics Cross-Round Juror Consistency Diversity By Round
{pdfStageId && ( )}
) }