'use client'
import { useState } 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,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
import {
ScoreDistributionChart,
EvaluationTimelineChart,
StatusBreakdownChart,
JurorWorkloadChart,
ProjectRankingsChart,
CriteriaScoresChart,
GeographicDistribution,
CrossRoundComparisonChart,
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({ includeRounds: true })
const { data: dashStats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery()
// Flatten rounds from all programs
const rounds = programs?.flatMap(p => p.rounds.map(r => ({ ...r, programId: p.id, programName: `${p.year} Edition` }))) || []
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) => r.status === 'ACTIVE').length
const jurorCount = dashStats?.jurorCount ?? 0
const submittedEvaluations = dashStats?.submittedEvaluations ?? 0
const totalEvaluations = dashStats?.totalEvaluations ?? 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}
{totalEvaluations > 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}
)
})}
)}
{/* Rounds Table */}
Round Reports
View progress and export data for each round
{rounds.length === 0 ? (
No rounds created yet. Round-specific reports will appear here once rounds are set up.
) : (
Round
Program
Projects
Status
Export
{rounds.map((round) => (
{round.name}
{round.votingEndAt && (
Ends: {formatDateOnly(round.votingEndAt)}
)}
{round.programName}
-
{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 }
}
function RoundAnalytics() {
const [selectedValue, setSelectedValue] = useState(null)
const { data: programs, isLoading: roundsLoading } = trpc.program.list.useQuery({ includeRounds: true })
// Flatten rounds from all programs with program name
const rounds = programs?.flatMap(p => p.rounds.map(r => ({ ...r, programId: p.id, programName: `${p.year} Edition` }))) || []
// Set default selected round
if (rounds.length && !selectedValue) {
setSelectedValue(rounds[0].id)
}
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 CrossRoundTab() {
const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeRounds: true })
const rounds = programs?.flatMap(p =>
p.rounds.map(r => ({ id: r.id, name: r.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 (
{/* Round selector */}
Select Rounds to Compare
Choose at least 2 rounds to compare metrics side by side
{rounds.map((round) => {
const isSelected = selectedRoundIds.includes(round.id)
return (
toggleRound(round.id)}
>
{round.programName} - {round.name}
)
})}
{selectedRoundIds.length < 2 && (
Select at least 2 rounds 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({ includeRounds: true })
const rounds = programs?.flatMap(p =>
p.rounds.map(r => ({ id: r.id, name: r.name, programId: p.id, programName: `${p.year} Edition` }))
) || []
if (rounds.length && !selectedValue) {
setSelectedValue(rounds[0].id)
}
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({ includeRounds: true })
const rounds = programs?.flatMap(p =>
p.rounds.map(r => ({ id: r.id, name: r.name, programId: p.id, programName: `${p.year} Edition` }))
) || []
if (rounds.length && !selectedValue) {
setSelectedValue(rounds[0].id)
}
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 && (
)}
)
}
export default function ReportsPage() {
const [pdfRoundId, setPdfRoundId] = useState(null)
const { data: pdfPrograms } = trpc.program.list.useQuery({ includeRounds: true })
const pdfRounds = pdfPrograms?.flatMap((p) =>
p.rounds.map((r) => ({ id: r.id, name: r.name, programName: `${p.year} Edition` }))
) || []
if (pdfRounds.length && !pdfRoundId) {
setPdfRoundId(pdfRounds[0].id)
}
const selectedPdfRound = pdfRounds.find((r) => r.id === pdfRoundId)
return (
{/* Header */}
Reports
View progress, analytics, and export evaluation data
{/* Tabs */}
Overview
Analytics
Cross-Round
Juror Consistency
Diversity
{pdfRoundId && (
)}
)
}