'use client' 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 { AnimatedCard } from '@/components/shared/animated-container' import { GeographicSummaryCard } from '@/components/charts/geographic-summary-card' import { useEditionContext } from '@/components/observer/observer-edition-context' import { BarChart3, Globe, Activity, Clock, CheckCircle, ClipboardList, Upload, Users, Trophy, } from 'lucide-react' import { cn } from '@/lib/utils' import { IntakePanel } from '@/components/observer/dashboard/intake-panel' import { FilteringPanel } from '@/components/observer/dashboard/filtering-panel' import { EvaluationPanel } from '@/components/observer/dashboard/evaluation-panel' import { SubmissionPanel } from '@/components/observer/dashboard/submission-panel' import { MentoringPanel } from '@/components/observer/dashboard/mentoring-panel' import { LiveFinalPanel } from '@/components/observer/dashboard/live-final-panel' import { DeliberationPanel } from '@/components/observer/dashboard/deliberation-panel' import { PreviousRoundSection } from '@/components/observer/dashboard/previous-round-section' 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 STATUS_BADGE_VARIANT: Record = { ROUND_ACTIVE: 'default', ROUND_CLOSED: 'secondary', ROUND_DRAFT: 'outline', ROUND_ARCHIVED: 'secondary', } const CATEGORY_ICONS: Record = { round: { icon: Clock, color: 'text-teal-500' }, evaluation: { icon: CheckCircle, color: 'text-blue-500' }, project: { icon: ClipboardList, color: 'text-emerald-500' }, file: { icon: Upload, color: 'text-violet-500' }, deliberation: { icon: Users, color: 'text-amber-500' }, system: { icon: Activity, color: 'text-slate-400' }, } function RoundPanel({ roundType, roundId, programId }: { roundType: string; roundId: string; programId: string }) { switch (roundType) { case 'INTAKE': return case 'FILTERING': return case 'EVALUATION': return case 'SUBMISSION': return case 'MENTORING': return case 'LIVE_FINAL': return case 'DELIBERATION': return default: return (

Select a round to view details.

) } } type RoundOverviewItem = { roundId: string roundName: string roundType: string roundStatus: string totalProjects: number completionRate: number specialAwardId?: string | null specialAwardName?: string | null } function RoundNode({ round, isSelected, onClick, }: { round: RoundOverviewItem isSelected: boolean onClick: () => void }) { const isActive = round.roundStatus === 'ROUND_ACTIVE' return ( ) } function PipelineView({ rounds, selectedRoundId, onSelectRound, }: { rounds: RoundOverviewItem[] selectedRoundId: string onSelectRound: (id: string) => void }) { // Split main pipeline from award tracks const mainRounds = rounds.filter((r) => !r.specialAwardId) const awardGroups = new Map() for (const r of rounds) { if (!r.specialAwardId) continue if (!awardGroups.has(r.specialAwardId)) { awardGroups.set(r.specialAwardId, { name: r.specialAwardName ?? 'Special Award', rounds: [] }) } awardGroups.get(r.specialAwardId)!.rounds.push(r) } return (
{/* Main Competition Pipeline */} {mainRounds.length > 0 && (
{mainRounds.map((round, idx) => (
onSelectRound(round.roundId)} /> {idx < mainRounds.length - 1 && (
)}
))}
)} {/* Award Tracks */} {awardGroups.size > 0 && (
{Array.from(awardGroups.entries()).map(([awardId, group]) => (

{group.name}

Award Track
{group.rounds.map((round, idx) => (
onSelectRound(round.roundId)} /> {idx < group.rounds.length - 1 && (
)}
))}
))}
)}
) } export function ObserverDashboardContent({ userName }: { userName?: string }) { const { programs, selectedProgramId, selectedRoundId, setSelectedRoundId, selectedRoundType, rounds, activeRoundId, } = useEditionContext() 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: geoData } = trpc.analytics.getGeographicDistribution.useQuery( { programId: selectedProgramId }, { enabled: !!selectedProgramId, refetchInterval: 30_000 }, ) const { data: activityFeed } = trpc.analytics.getActivityFeed.useQuery( { limit: 15, roundId: selectedRoundId || undefined }, { refetchInterval: 30_000 }, ) const countryCount = geoData ? geoData.length : 0 const avgScore = stats ? computeAvgScore(stats.scoreDistribution) : '—' return (
{/* Header */}

Dashboard

Welcome, {userName || 'Observer'}

{/* Stats Strip */} {statsLoading ? ( ) : stats ? (
{[ { value: stats.projectCount, label: 'Projects' }, { value: stats.activeRoundName ?? `${stats.activeRoundCount} Active`, label: 'Active Rounds', isText: !!stats.activeRoundName }, { value: avgScore, label: 'Avg Score' }, { value: `${stats.completionRate}%`, label: 'Completion' }, { value: stats.jurorCount, label: 'Jurors' }, { value: countryCount, label: 'Countries' }, ].map((stat) => (

{stat.value}

{stat.label}

))}
) : null} {/* Clickable Pipeline */}
Competition Pipeline
Click a round to view its details
{overviewLoading || !competitionId ? (
{[...Array(4)].map((_, i) => ( ))}
) : roundOverview && roundOverview.rounds.length > 0 ? ( ) : (

No round data available for this competition.

)}
{/* Main Content: Round Panel + Activity Feed */}
{/* Left: Round-specific panel */}
{selectedRoundId && selectedRoundType ? ( ) : (

Select a round from the pipeline above.

)}
{/* Right: Activity Feed */}
Activity Feed
Recent platform events
{activityFeed && activityFeed.length > 0 ? (
{activityFeed.slice(0, 8).map((item) => { const iconDef = CATEGORY_ICONS[item.category ?? 'system'] ?? CATEGORY_ICONS.system const IconComponent = iconDef.icon const iconColor = iconDef.color return (

{item.description}

{relativeTime(item.createdAt)}
) })}
) : (
{[...Array(6)].map((_, i) => (
))}
)}
{/* Previous Round Comparison */} {selectedRoundId && ( )} {/* Full-width Map */} {selectedProgramId ? ( ) : ( Project Origins Geographic distribution of projects )}
) }