'use client' import { useState, Fragment } from 'react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { StatusBadge } from '@/components/shared/status-badge' import { AnimatedCard } from '@/components/shared/animated-container' import { GeographicSummaryCard } from '@/components/charts/geographic-summary-card' import { useEditionContext } from '@/components/observer/observer-edition-context' import { ClipboardList, BarChart3, TrendingUp, CheckCircle2, Users, Globe, ChevronRight, Activity, ChevronDown, ChevronUp, ArrowRight, Lock, Clock, CheckCircle, XCircle, } from 'lucide-react' import { cn } from '@/lib/utils' function relativeTime(date: Date | string): string { const now = Date.now() const then = new Date(date).getTime() const diff = Math.floor((now - then) / 1000) if (diff < 60) return `${diff}s ago` if (diff < 3600) return `${Math.floor(diff / 60)}m ago` if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` return `${Math.floor(diff / 86400)}d ago` } function computeAvgScore(scoreDistribution: { label: string; count: number }[]): string { const midpoints: Record = { '9-10': 9.5, '7-8': 7.5, '5-6': 5.5, '3-4': 3.5, '1-2': 1.5, } let total = 0 let weightedSum = 0 for (const b of scoreDistribution) { const mid = midpoints[b.label] if (mid !== undefined) { weightedSum += mid * b.count total += b.count } } if (total === 0) return '—' return (weightedSum / total).toFixed(1) } const ACTIVITY_ICONS: Record = { ROUND_ACTIVATED: { icon: Clock, color: 'text-emerald-500' }, ROUND_CLOSED: { icon: Lock, color: 'text-slate-500' }, 'round.reopened': { icon: Clock, color: 'text-emerald-500' }, 'round.closed': { icon: Lock, color: 'text-slate-500' }, EVALUATION_SUBMITTED: { icon: CheckCircle, color: 'text-blue-500' }, ASSIGNMENT_CREATED: { icon: ArrowRight, color: 'text-violet-500' }, PROJECT_ADVANCED: { icon: ArrowRight, color: 'text-teal-500' }, PROJECT_REJECTED: { icon: XCircle, color: 'text-rose-500' }, RESULT_LOCKED: { icon: Lock, color: 'text-amber-500' }, } function humanizeActivity(item: { eventType: string; actorName?: string | null; details?: Record | null }): string { const actor = item.actorName ?? 'System' const details = item.details ?? {} const projectName = (details.projectTitle ?? details.projectName ?? '') as string const roundName = (details.roundName ?? '') as string switch (item.eventType) { case 'EVALUATION_SUBMITTED': return projectName ? `${actor} submitted a review for ${projectName}` : `${actor} submitted a review` case 'ROUND_ACTIVATED': case 'round.reopened': return roundName ? `${roundName} was opened` : 'A round was opened' case 'ROUND_CLOSED': case 'round.closed': return roundName ? `${roundName} was closed` : 'A round was closed' case 'ASSIGNMENT_CREATED': return projectName ? `${projectName} was assigned to a juror` : 'A project was assigned' case 'PROJECT_ADVANCED': return projectName ? `${projectName} advanced${roundName ? ` to ${roundName}` : ''}` : 'A project advanced' case 'PROJECT_REJECTED': return projectName ? `${projectName} was rejected` : 'A project was rejected' case 'RESULT_LOCKED': return roundName ? `Results locked for ${roundName}` : 'Results were locked' default: return `${actor}: ${item.eventType.replace(/_/g, ' ').toLowerCase()}` } } const STATUS_BADGE_VARIANT: Record = { ROUND_ACTIVE: 'default', ROUND_CLOSED: 'secondary', ROUND_DRAFT: 'outline', ROUND_ARCHIVED: 'secondary', } export function ObserverDashboardContent({ userName }: { userName?: string }) { const { programs, selectedProgramId, activeRoundId } = useEditionContext() const [expandedJurorId, setExpandedJurorId] = useState(null) const roundIdParam = activeRoundId || undefined const { data: stats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery( { roundId: roundIdParam }, { refetchInterval: 30_000 }, ) const selectedProgram = programs.find((p) => p.id === selectedProgramId) const competitionId = (selectedProgram?.rounds ?? [])[0]?.competitionId as string | undefined const { data: roundOverview, isLoading: overviewLoading } = trpc.analytics.getRoundCompletionOverview.useQuery( { competitionId: competitionId! }, { enabled: !!competitionId, refetchInterval: 30_000 }, ) const { data: jurorWorkload } = trpc.analytics.getJurorWorkload.useQuery( { programId: selectedProgramId || undefined }, { enabled: !!selectedProgramId, refetchInterval: 30_000 }, ) const { data: geoData } = trpc.analytics.getGeographicDistribution.useQuery( { programId: selectedProgramId }, { enabled: !!selectedProgramId, refetchInterval: 30_000 }, ) const { data: projectsData } = trpc.analytics.getAllProjects.useQuery( { perPage: 10 }, { refetchInterval: 30_000 }, ) const { data: activityFeed } = trpc.analytics.getActivityFeed.useQuery( { limit: 10 }, { refetchInterval: 30_000 }, ) const countryCount = geoData ? geoData.length : 0 const avgScore = stats ? computeAvgScore(stats.scoreDistribution) : '—' const topJurors = (jurorWorkload ?? []).slice(0, 5) const scoreColors: Record = { '9-10': '#053d57', '7-8': '#1e7a8a', '5-6': '#557f8c', '3-4': '#c4453a', '1-2': '#de0f1e', } const maxScoreCount = stats ? Math.max(...stats.scoreDistribution.map((b) => b.count), 1) : 1 return (
{/* Header */}

Dashboard

Welcome, {userName || 'Observer'}

{/* Six Stat Tiles */} {statsLoading ? (
{[...Array(6)].map((_, i) => ( ))}
) : stats ? (

{stats.projectCount}

Total Projects

{stats.activeRoundCount}

Active Rounds

{avgScore}

Avg Score

{stats.completionRate}%

Completion

{stats.jurorCount}

Active Jurors

{countryCount}

Countries

) : null} {/* Pipeline */}
Competition Pipeline
Round-by-round progression overview
{overviewLoading || !competitionId ? (
{[...Array(4)].map((_, i) => ( ))}
) : roundOverview && roundOverview.rounds.length > 0 ? (
{roundOverview.rounds.map((round, idx) => (

{round.roundName}

{round.roundType.replace(/_/g, ' ')} {round.roundStatus === 'ROUND_ACTIVE' ? 'Active' : round.roundStatus === 'ROUND_CLOSED' ? 'Closed' : round.roundStatus === 'ROUND_DRAFT' ? 'Draft' : round.roundStatus === 'ROUND_ARCHIVED' ? 'Archived' : round.roundStatus}

{round.totalProjects} project{round.totalProjects !== 1 ? 's' : ''}

{round.completionRate}% complete

{idx < roundOverview.rounds.length - 1 && (
)}
))}
) : (

No round data available for this competition.

)} {/* Middle Row */}
{/* Score Distribution */}
Score Distribution
Evaluation score buckets
{stats ? (
{stats.scoreDistribution.map((bucket) => (
{bucket.label}
0 ? (bucket.count / maxScoreCount) * 100 : 0}%`, backgroundColor: scoreColors[bucket.label] ?? '#557f8c', }} />
{bucket.count}
))}
) : (
{[...Array(5)].map((_, i) => ( ))}
)} {/* Juror Workload */}
Juror Workload
Top 5 jurors by assignment
{topJurors.length > 0 ? (
{topJurors.map((juror) => { const isExpanded = expandedJurorId === juror.id return (
{isExpanded && juror.projects && (
{juror.projects.map((proj: { id: string; title: string; evalStatus: string }) => ( {proj.title} ))}
)}
) })}
) : (
{[...Array(5)].map((_, i) => (
))}
)}
{/* Project Origins */} {selectedProgramId ? ( ) : ( Project Origins Geographic distribution of projects )}
{/* Bottom Row */}
{/* Recent Projects Table */}
Recently Reviewed
Latest project reviews
{projectsData && projectsData.projects.length > 0 ? ( <> Project Status Score {projectsData.projects.map((project) => ( {project.title} {project.teamName && (

{project.teamName}

)}
{project.evaluationCount > 0 && project.averageScore !== null ? project.averageScore.toFixed(1) : '—'}
))}
View All
) : (
{[...Array(5)].map((_, i) => ( ))}
)}
{/* Activity Feed */}
Activity Feed
Recent platform events
{activityFeed && activityFeed.length > 0 ? (
{activityFeed.slice(0, 5).map((item) => { const iconDef = ACTIVITY_ICONS[item.eventType] const IconComponent = iconDef?.icon ?? Activity const iconColor = iconDef?.color ?? 'text-slate-400' return (

{humanizeActivity(item)}

{relativeTime(item.createdAt)}
) })}
) : (
{[...Array(6)].map((_, i) => (
))}
)}
) }