'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 { Progress } from '@/components/ui/progress' import { Skeleton } from '@/components/ui/skeleton' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Button } from '@/components/ui/button' import { StatusBadge } from '@/components/shared/status-badge' import { FolderKanban, ClipboardList, Users, CheckCircle2, Eye, BarChart3, Search, ChevronLeft, ChevronRight, ArrowUpDown, ArrowUp, ArrowDown, } from 'lucide-react' import { cn } from '@/lib/utils' import { AnimatedCard } from '@/components/shared/animated-container' import { RoundTypeStatsCards } from '@/components/observer/round-type-stats' import { useDebouncedCallback } from 'use-debounce' const PER_PAGE_OPTIONS = [10, 20, 50] export function ObserverDashboardContent({ userName }: { userName?: string }) { const [selectedRoundId, setSelectedRoundId] = useState('all') const [search, setSearch] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [sortBy, setSortBy] = useState<'title' | 'score' | 'evaluations'>('title') const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc') const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(20) const debouncedSetSearch = useDebouncedCallback((value: string) => { setDebouncedSearch(value) setPage(1) }, 300) const handleSearchChange = (value: string) => { setSearch(value) debouncedSetSearch(value) } const handleRoundChange = (value: string) => { setSelectedRoundId(value) setPage(1) } const handleStatusChange = (value: string) => { setStatusFilter(value) setPage(1) } const handleSort = (column: 'title' | 'score' | 'evaluations') => { if (sortBy === column) { setSortDir(sortDir === 'asc' ? 'desc' : 'asc') } else { setSortBy(column) setSortDir(column === 'title' ? 'asc' : 'desc') } setPage(1) } const SortIcon = ({ column }: { column: 'title' | 'score' | 'evaluations' }) => { if (sortBy !== column) return return sortDir === 'asc' ? : } // Fetch programs/rounds for the filter dropdown const { data: programs } = trpc.program.list.useQuery({ includeStages: true }) const rounds = programs?.flatMap((p) => (p.rounds ?? []).map((r: { id: string; name: string; status: string; roundType?: string }) => ({ id: r.id, name: r.name, programName: `${p.year} Edition`, status: r.status, roundType: r.roundType, })) ) || [] // Default to the active round useEffect(() => { if (rounds.length && selectedRoundId === 'all') { const active = rounds.find((r) => r.status === 'ROUND_ACTIVE') if (active) setSelectedRoundId(active.id) } }, [rounds.length]) // eslint-disable-line react-hooks/exhaustive-deps // Fetch dashboard stats const roundIdParam = selectedRoundId !== 'all' ? selectedRoundId : undefined const { data: stats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery( { roundId: roundIdParam } ) // Fetch projects const { data: projectsData, isLoading: projectsLoading } = trpc.analytics.getAllProjects.useQuery({ roundId: roundIdParam, search: debouncedSearch || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, sortBy, sortDir, page, perPage, }) // Recent rounds for jury completion (reuse existing programs data) const recentRounds = rounds.slice(0, 5) return (
{/* Header */}

Dashboard

Welcome, {userName || 'Observer'}

{/* Observer Notice */}

Observer Mode

Read-Only

You have read-only access to view platform statistics and reports.

{/* Round Filter */}
{/* Stats Grid */} {statsLoading ? (
{[...Array(4)].map((_, i) => ( ))}
) : stats ? (
{/* Universal stats: Programs + Projects */}

Programs

{stats.programCount}

{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}

Projects

{stats.projectCount}

{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}

{/* Round-type-aware stats */} {selectedRoundId !== 'all' ? ( ) : (

Jury Members

{stats.jurorCount}

Active members

Evaluations

{stats.submittedEvaluations}

{stats.completionRate}% completion rate

)}
) : null} {/* Projects Table */}
All Projects
{projectsData ? `${projectsData.total} project${projectsData.total !== 1 ? 's' : ''} found` : 'Loading projects...'}
{/* Search & Filter Bar */}
handleSearchChange(e.target.value)} className="pl-10" />
{projectsLoading ? (
{[...Array(5)].map((_, i) => ( ))}
) : projectsData && projectsData.projects.length > 0 ? ( <> {/* Desktop Table */}
Team Round Status {projectsData.projects.map((project) => ( {project.title} {project.teamName || '-'} {project.roundName} {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'} {project.evaluationCount} ))}
{/* Mobile Cards */}
{projectsData.projects.map((project) => (

{project.title}

{project.teamName && (

{project.teamName}

)}
{project.roundName}
Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'} {project.evaluationCount} eval{project.evaluationCount !== 1 ? 's' : ''}
))}
{/* Pagination */} {projectsData.totalPages > 1 && (

Page {projectsData.page} of {projectsData.totalPages}

)} ) : (

{debouncedSearch || statusFilter !== 'all' ? 'No projects match your filters' : 'No projects found'}

)}
{/* Score Distribution */} {stats && stats.scoreDistribution.some((b) => b.count > 0) && (
Score Distribution
Distribution of global scores across evaluations
{(() => { const maxCount = Math.max(...stats.scoreDistribution.map((b) => b.count), 1) // Score-based colors: high scores = brand dark blue, low = brand red const scoreColors: Record = { '9-10': '#053d57', '7-8': '#1e7a8a', '5-6': '#557f8c', '3-4': '#c4453a', '1-2': '#de0f1e', } return stats.scoreDistribution.map((bucket) => (
{bucket.label}
0 ? (bucket.count / maxCount) * 100 : 0}%`, backgroundColor: scoreColors[bucket.label] || '#557f8c', }} />
{bucket.count}
)) })()}
)} {/* Recent Rounds */} {recentRounds.length > 0 && (
Recent Rounds
Overview of the latest evaluation rounds
{recentRounds.map((round) => (

{round.name}

{round.roundType && ( {round.roundType.replace(/_/g, ' ')} )} {round.status === 'ROUND_ACTIVE' ? 'Active' : round.status === 'ROUND_CLOSED' ? 'Closed' : round.status}

{round.programName}

))}
)}
) }