'use client' import { useState, useEffect } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' 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 { ClipboardList, BarChart3, TrendingUp, CheckCircle2, Users, Globe, ChevronRight, Activity, RefreshCw, } 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_DOT_COLORS: Record = { ROUND_ACTIVATED: 'bg-emerald-500', ROUND_CLOSED: 'bg-slate-500', EVALUATION_SUBMITTED: 'bg-blue-500', ASSIGNMENT_CREATED: 'bg-violet-500', PROJECT_ADVANCED: 'bg-teal-500', PROJECT_REJECTED: 'bg-rose-500', RESULT_LOCKED: 'bg-amber-500', } const STATUS_BADGE_VARIANT: Record = { ROUND_ACTIVE: 'default', ROUND_CLOSED: 'secondary', ROUND_DRAFT: 'outline', ROUND_ARCHIVED: 'secondary', } export function ObserverDashboardContent({ userName }: { userName?: string }) { const [selectedProgramId, setSelectedProgramId] = useState('') const [selectedRoundId, setSelectedRoundId] = useState('') const { data: programs } = trpc.program.list.useQuery( { includeStages: true }, { refetchInterval: 30_000 }, ) useEffect(() => { if (programs && programs.length > 0 && !selectedProgramId) { const firstProgram = programs[0] setSelectedProgramId(firstProgram.id) const firstRound = (firstProgram.rounds ?? [])[0] if (firstRound) setSelectedRoundId(firstRound.id) } }, [programs, selectedProgramId]) const roundIdParam = selectedRoundId || 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'}

Auto-refresh
{/* 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) => (
{juror.name ?? 'Unknown'} {juror.completionRate}%

{juror.completed} / {juror.assigned} evaluations

))}
) : (
{[...Array(5)].map((_, i) => (
))}
)}
{/* Project Origins */} {selectedProgramId ? ( ) : ( Project Origins Geographic distribution of projects )}
{/* Bottom Row */}
{/* Recent Projects Table */}
Recent Projects
Latest project activity
{projectsData && projectsData.projects.length > 0 ? ( <> Project Status Score {projectsData.projects.map((project) => ( {project.title} {project.teamName && (

{project.teamName}

)}
{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.map((item) => (

{item.eventType.replace(/_/g, ' ').toLowerCase().replace(/^\w/, (c) => c.toUpperCase())} {item.entityType && ( — {item.entityType.replace(/_/g, ' ').toLowerCase()} )}

{item.actorName && (

by {item.actorName}

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