diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index 1bd85c4..3e01578 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef, useMemo } from 'react' +import React, { useState, useEffect, useRef, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { cn } from '@/lib/utils' @@ -49,6 +49,7 @@ import { Loader2, RefreshCw, Trophy, + ExternalLink, } from 'lucide-react' import type { RankedProjectEntry } from '@/server/services/ai-ranking' @@ -191,6 +192,9 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran const [topNConceptual, setTopNConceptual] = useState(3) const [includeReject, setIncludeReject] = useState(false) + // ─── Expandable review state ────────────────────────────────────────────── + const [expandedReviews, setExpandedReviews] = useState>(new Set()) + // ─── Sensors ────────────────────────────────────────────────────────────── const sensors = useSensors( useSensor(PointerSensor), @@ -220,6 +224,8 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran { enabled: !!selectedProjectId }, ) + const { data: roundData } = trpc.round.getById.useQuery({ id: roundId }) + // ─── tRPC mutations ─────────────────────────────────────────────────────── const utils = trpc.useUtils() @@ -261,6 +267,19 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran onError: (err) => toast.error(err.message), }) + // ─── evalConfig (advancement counts from round config) ──────────────────── + const evalConfig = useMemo(() => { + if (!roundData?.configJson) return null + try { + const config = roundData.configJson as Record + const advConfig = config.advancementConfig as Record | undefined + return { + startupAdvanceCount: (advConfig?.startupCount ?? config.startupAdvanceCount ?? 0) as number, + conceptAdvanceCount: (advConfig?.conceptCount ?? config.conceptAdvanceCount ?? 0) as number, + } + } catch { return null } + }, [roundData]) + // ─── rankingMap (O(1) lookup) ────────────────────────────────────────────── const rankingMap = useMemo(() => { const map = new Map() @@ -298,6 +317,14 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran } }, [snapshot]) + // ─── sync advance dialog defaults from config ──────────────────────────── + useEffect(() => { + if (evalConfig) { + if (evalConfig.startupAdvanceCount > 0) setTopNStartup(evalConfig.startupAdvanceCount) + if (evalConfig.conceptAdvanceCount > 0) setTopNConceptual(evalConfig.conceptAdvanceCount) + } + }, [evalConfig]) + // ─── handleDragEnd ──────────────────────────────────────────────────────── function handleDragEnd(category: 'STARTUP' | 'BUSINESS_CONCEPT', event: DragEndEvent) { const { active, over } = event @@ -448,6 +475,11 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran {categoryLabels[category]} + {evalConfig && (category === 'STARTUP' ? evalConfig.startupAdvanceCount : evalConfig.conceptAdvanceCount) > 0 && ( + + (Top {category === 'STARTUP' ? evalConfig.startupAdvanceCount : evalConfig.conceptAdvanceCount} advance) + + )} @@ -467,25 +499,43 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran >
- {localOrder[category].map((projectId, index) => ( - - setSelectedProjectId(projectId)} - isSelected={selectedProjectId === projectId} - /> - - ))} + {localOrder[category].map((projectId, index) => { + const advanceCount = category === 'STARTUP' + ? (evalConfig?.startupAdvanceCount ?? 0) + : (evalConfig?.conceptAdvanceCount ?? 0) + const isAdvancing = advanceCount > 0 && index < advanceCount + const isCutoffRow = advanceCount > 0 && index === advanceCount - 1 + + return ( + + + setSelectedProjectId(projectId)} + isSelected={selectedProjectId === projectId} + /> + + {isCutoffRow && ( +
+
+ + Advancement cutoff — Top {advanceCount} + +
+
+ )} + + ) + })}
@@ -617,6 +667,17 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran {selectedProjectId ? `ID: …${selectedProjectId.slice(-8)}` : ''} + {selectedProjectId && ( + + + View Project Page + + )} {detailLoading ? ( @@ -669,37 +730,40 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran } 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'} - - )} + {submitted.map((a) => { + const isExpanded = expandedReviews.has(a.id) + return ( +
setExpandedReviews(prev => { + const next = new Set(prev) + next.has(a.id) ? next.delete(a.id) : next.add(a.id) + return next + })} + > +
+ {a.user?.name ?? a.user?.email ?? 'Unknown'} +
+ {a.evaluation?.binaryDecision != null && ( + + {a.evaluation.binaryDecision ? 'Yes' : 'No'} + + )} + Score: {a.evaluation?.globalScore?.toFixed(1) ?? '—'} +
+ {isExpanded && a.evaluation?.feedbackText && ( +

+ {a.evaluation.feedbackText} +

+ )}
- {a.evaluation?.feedbackText && ( -

- {a.evaluation.feedbackText} -

- )} -
- ))} + ) + })}
) })()}