'use client' import { useState, useEffect, useRef, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { cn } from '@/lib/utils' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { AnimatePresence, motion } from 'motion/react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from '@/components/ui/sheet' import { GripVertical, BarChart3, Loader2, RefreshCw, } from 'lucide-react' import type { RankedProjectEntry } from '@/server/services/ai-ranking' // ─── Types ──────────────────────────────────────────────────────────────────── type RankingDashboardProps = { competitionId: string roundId: string } type SortableProjectRowProps = { projectId: string currentRank: number entry: RankedProjectEntry | undefined onSelect: () => void isSelected: boolean } // ─── Sub-component: SortableProjectRow ──────────────────────────────────────── function SortableProjectRow({ projectId, currentRank, entry, onSelect, isSelected, }: SortableProjectRowProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: projectId }) const style = { transform: CSS.Transform.toString(transform), transition, } // isOverridden: current position differs from AI-assigned rank const isOverridden = entry !== undefined && currentRank !== entry.rank return (
{/* Drag handle */} {/* Rank badge */} {isOverridden ? ( #{currentRank} (override) ) : ( #{currentRank} )} {/* Project identifier */}

Project …{projectId.slice(-6)}

{/* Stats */} {entry && (
{Math.round(entry.compositeScore * 100)}% {entry.avgGlobalScore !== null && ( Avg {entry.avgGlobalScore.toFixed(1)} )} Pass {Math.round(entry.passRate * 100)}% {entry.evaluatorCount} juror{entry.evaluatorCount !== 1 ? 's' : ''}
)}
) } // ─── Main component ──────────────────────────────────────────────────────────── export function RankingDashboard({ competitionId: _competitionId, roundId }: RankingDashboardProps) { // ─── State ──────────────────────────────────────────────────────────────── const [selectedProjectId, setSelectedProjectId] = useState(null) const [localOrder, setLocalOrder] = useState>({ STARTUP: [], BUSINESS_CONCEPT: [], }) const initialized = useRef(false) // ─── Sensors ────────────────────────────────────────────────────────────── const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ) // ─── tRPC queries ───────────────────────────────────────────────────────── const { data: snapshots, isLoading: snapshotsLoading } = trpc.ranking.listSnapshots.useQuery( { roundId }, { refetchInterval: 30_000 }, ) const latestSnapshotId = snapshots?.[0]?.id ?? null const latestSnapshot = snapshots?.[0] ?? null const { data: snapshot, isLoading: snapshotLoading } = trpc.ranking.getSnapshot.useQuery( { snapshotId: latestSnapshotId! }, { enabled: !!latestSnapshotId }, ) const { data: projectDetail, isLoading: detailLoading } = trpc.project.getFullDetail.useQuery( { id: selectedProjectId! }, { enabled: !!selectedProjectId }, ) // ─── tRPC mutations ─────────────────────────────────────────────────────── const utils = trpc.useUtils() const saveReorderMutation = trpc.ranking.saveReorder.useMutation({ onError: (err) => toast.error(`Failed to save order: ${err.message}`), // Do NOT invalidate getSnapshot — would reset localOrder }) const triggerRankMutation = trpc.ranking.triggerAutoRank.useMutation({ onSuccess: () => { toast.success('Ranking complete. Reload to see results.') initialized.current = false // allow re-init on next snapshot load void utils.ranking.listSnapshots.invalidate({ roundId }) }, onError: (err) => toast.error(err.message), }) // ─── rankingMap (O(1) lookup) ────────────────────────────────────────────── const rankingMap = useMemo(() => { const map = new Map() if (!snapshot) return map const startup = (snapshot.startupRankingJson ?? []) as unknown as RankedProjectEntry[] const concept = (snapshot.conceptRankingJson ?? []) as unknown as RankedProjectEntry[] ;[...startup, ...concept].forEach((entry) => map.set(entry.projectId, entry)) return map }, [snapshot]) // ─── localOrder init (once, with useRef guard) ──────────────────────────── useEffect(() => { if (!initialized.current && snapshot) { const startup = (snapshot.startupRankingJson ?? []) as unknown as RankedProjectEntry[] const concept = (snapshot.conceptRankingJson ?? []) as unknown as RankedProjectEntry[] setLocalOrder({ STARTUP: startup.map((r) => r.projectId), BUSINESS_CONCEPT: concept.map((r) => r.projectId), }) initialized.current = true } }, [snapshot]) // ─── handleDragEnd ──────────────────────────────────────────────────────── function handleDragEnd(category: 'STARTUP' | 'BUSINESS_CONCEPT', event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id) return setLocalOrder((prev) => { const ids = prev[category] const newIds = arrayMove( ids, ids.indexOf(active.id as string), ids.indexOf(over.id as string), ) saveReorderMutation.mutate({ snapshotId: latestSnapshotId!, category, orderedProjectIds: newIds, }) return { ...prev, [category]: newIds } }) } // ─── Loading state ──────────────────────────────────────────────────────── if (snapshotsLoading || snapshotLoading) { return (
) } // ─── Empty state ────────────────────────────────────────────────────────── if (!latestSnapshotId) { return (

No ranking available yet

Run ranking from the Config tab to generate results, or trigger it now.

) } // ─── Main content ───────────────────────────────────────────────────────── const categoryLabels: Record<'STARTUP' | 'BUSINESS_CONCEPT', string> = { STARTUP: 'Startups', BUSINESS_CONCEPT: 'Business Concepts', } return ( <>
{/* Header card */}
Latest Ranking Snapshot {latestSnapshot && ( Created{' '} {new Date(latestSnapshot.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short', })} {latestSnapshot.triggeredBy?.name && ` by ${latestSnapshot.triggeredBy.name}`} {' · '} {latestSnapshot.triggerType} {latestSnapshot.criteriaText && ( Criteria: {latestSnapshot.criteriaText.slice(0, 120)} {latestSnapshot.criteriaText.length > 120 ? '…' : ''} )} )}
{/* Per-category sections */} {(['STARTUP', 'BUSINESS_CONCEPT'] as const).map((category) => ( {categoryLabels[category]} {localOrder[category].length === 0 ? (

No {category === 'STARTUP' ? 'startup' : 'business concept'} projects ranked.

) : ( handleDragEnd(category, event)} >
{localOrder[category].map((projectId, index) => ( setSelectedProjectId(projectId)} isSelected={selectedProjectId === projectId} /> ))}
)}
))}
{/* Side panel Sheet */} { if (!open) setSelectedProjectId(null) }} > {projectDetail?.project.title ?? 'Project Details'} {selectedProjectId ? `ID: …${selectedProjectId.slice(-8)}` : ''} {detailLoading ? (
) : projectDetail ? (
{/* Stats summary */} {projectDetail.stats && (

Avg Score

{projectDetail.stats.averageGlobalScore?.toFixed(1) ?? '—'}

Pass Rate

{projectDetail.stats.totalEvaluations > 0 ? `${Math.round((projectDetail.stats.yesVotes / projectDetail.stats.totalEvaluations) * 100)}%` : '—'}

Evaluators

{projectDetail.stats.totalEvaluations}

)} {/* Per-juror evaluations */}

Juror Evaluations

{(() => { const submitted = projectDetail.assignments.filter( (a) => a.evaluation?.status === 'SUBMITTED' && a.round.id === roundId, ) if (submitted.length === 0) { return (

No submitted evaluations for this round.

) } return (
{submitted.map((a) => (

{a.user.name ?? a.user.email}

{a.evaluation?.globalScore !== null && a.evaluation?.globalScore !== undefined && ( Score: {a.evaluation.globalScore.toFixed(1)} )} {a.evaluation?.binaryDecision !== null && a.evaluation?.binaryDecision !== undefined && ( {a.evaluation.binaryDecision ? 'Yes' : 'No'} )}
{a.evaluation?.feedbackText && (

{a.evaluation.feedbackText}

)}
))}
) })()}
) : null}
) }