diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index c66d2ec..b697727 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -91,6 +91,7 @@ import { ProjectStatesTable } from '@/components/admin/round/project-states-tabl // SubmissionWindowManager removed — round dates + file requirements in Config are sufficient import { FileRequirementsEditor } from '@/components/admin/round/file-requirements-editor' import { FilteringDashboard } from '@/components/admin/round/filtering-dashboard' +import { MentoringRoundOverview } from '@/components/admin/round/mentoring-round-overview' import { RankingDashboard } from '@/components/admin/round/ranking-dashboard' import { CoverageReport } from '@/components/admin/assignment/coverage-report' import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet' @@ -582,6 +583,14 @@ export default function RoundDetailPage() { const isFiltering = round?.roundType === 'FILTERING' const isEvaluation = round?.roundType === 'EVALUATION' const isMentoring = round?.roundType === 'MENTORING' + + // Mentor pool size — used by Round Details panel below to replace the + // always-empty "Jury Group" row on MENTORING rounds. + const { data: mentorPool } = trpc.mentor.getMentorPool.useQuery( + {}, + { enabled: isMentoring }, + ) + const mentorPoolSize = mentorPool?.poolSize ?? 0 const hasJury = ['EVALUATION', 'LIVE_FINAL', 'DELIBERATION'].includes(round?.roundType ?? '') const hasAwards = roundAwards.length > 0 const isSimpleAdvance = ['INTAKE', 'SUBMISSION', 'MENTORING'].includes(round?.roundType ?? '') @@ -1469,6 +1478,9 @@ export default function RoundDetailPage() { /> )} + {/* Mentoring-specific stats \u2014 only on MENTORING rounds */} + {isMentoring && } + {/* Round Info + Project Breakdown */}
@@ -1482,7 +1494,9 @@ export default function RoundDetailPage() { { label: 'Status', value: {statusCfg.label} }, { label: 'Position', value: {`Round ${(round.sortOrder ?? 0) + 1}${competition?.rounds ? ` of ${competition.rounds.length}` : ''}`} }, ...(round.purposeKey ? [{ label: 'Purpose', value: {round.purposeKey} }] : []), - { label: 'Jury Group', value: {juryGroup ? juryGroup.name : '\u2014'} }, + isMentoring + ? { label: 'Mentor Pool', value: {mentorPoolSize} member{mentorPoolSize === 1 ? '' : 's'} } + : { label: 'Jury Group', value: {juryGroup ? juryGroup.name : '\u2014'} }, { label: 'Opens', value: {round.windowOpenAt ? new Date(round.windowOpenAt).toLocaleString() : '\u2014'} }, { label: 'Closes', value: {round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleString() : '\u2014'} }, ].map((row, i) => ( diff --git a/src/components/admin/round/mentoring-round-overview.tsx b/src/components/admin/round/mentoring-round-overview.tsx new file mode 100644 index 0000000..992795d --- /dev/null +++ b/src/components/admin/round/mentoring-round-overview.tsx @@ -0,0 +1,217 @@ +'use client' + +import Link from 'next/link' +import { trpc } from '@/lib/trpc/client' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Skeleton } from '@/components/ui/skeleton' +import { + ArrowRight, + Clock, + FileText, + MessageCircle, + Target, + UserCheck, + Users, +} from 'lucide-react' + +interface Props { + roundId: string +} + +function formatRelativeFuture(date: Date | null): { label: string; tone: 'normal' | 'amber' | 'red' } { + if (!date) return { label: '—', tone: 'normal' } + const ms = date.getTime() - Date.now() + if (ms <= 0) return { label: 'Closed', tone: 'red' } + const hours = Math.floor(ms / 3_600_000) + const days = Math.floor(hours / 24) + const tone: 'normal' | 'amber' | 'red' = + hours <= 12 ? 'red' : hours <= 48 ? 'amber' : 'normal' + const label = days > 0 ? `Closes in ${days}d` : `Closes in ${hours}h` + return { label, tone } +} + +function formatRelativePast(date: Date | null): string { + if (!date) return '—' + const ms = Date.now() - date.getTime() + const minutes = Math.floor(ms / 60_000) + const hours = Math.floor(ms / 3_600_000) + const days = Math.floor(ms / 86_400_000) + if (days > 0) return `${days}d ago` + if (hours > 0) return `${hours}h ago` + return `${Math.max(0, minutes)}m ago` +} + +export function MentoringRoundOverview({ roundId }: Props) { + const { data: stats, isLoading: statsLoading } = trpc.mentor.getRoundStats.useQuery( + { roundId }, + { refetchInterval: 30_000 }, + ) + const { data: pool, isLoading: poolLoading } = trpc.mentor.getMentorPool.useQuery({}) + + if (statsLoading || poolLoading) { + return ( +
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+ ) + } + if (!stats || !pool) return null + + const requestedPct = stats.totalProjects + ? Math.round((stats.requestedCount / stats.totalProjects) * 100) + : 0 + const assignedPct = stats.totalProjects + ? Math.round((stats.assignedCount / stats.totalProjects) * 100) + : 0 + + const window = formatRelativeFuture( + stats.requestWindow.deadline ? new Date(stats.requestWindow.deadline) : null, + ) + + const avgLoad = + pool.poolSize > 0 ? (pool.totalCurrentAssignments / pool.poolSize).toFixed(1) : '—' + const lastActivity = stats.workspaceActivity.lastActivityAt + ? new Date(stats.workspaceActivity.lastActivityAt) + : null + + return ( +
+ + + + Requested mentoring + + + +
+ {stats.requestedCount} + + / {stats.totalProjects} + +
+

{requestedPct}% of round

+
+
+ + + + + Mentor assigned + + + +
+
{stats.assignedCount}
+ +
+

+ {assignedPct}% of round{' '} + {stats.awaitingAssignment > 0 && ( + + · {stats.awaitingAssignment} awaiting + + )} +

+
+
+ + + + + Request window + + + +
+ + + {window.label} + +
+

+ {stats.requestWindow.deadline + ? `Closes ${new Date(stats.requestWindow.deadline).toLocaleDateString()} · ${stats.requestWindow.deadlineDays}-day window` + : 'No window deadline set'} +

+
+
+ + + + + Mentor pool + + + View all + + + + +
+
{pool.poolSize}
+ +
+

+ Avg load {avgLoad} ·{' '} + {pool.totalCurrentAssignments} active +

+
+
+ + + + Workspace activity + + +
+
+ +
+
{stats.workspaceActivity.messageCount}
+
messages
+
+
+
+ +
+
{stats.workspaceActivity.fileCount}
+
files
+
+
+
+ +
+
+ {stats.workspaceActivity.milestoneCount} +
+
milestones
+
+
+
+ +
+
{formatRelativePast(lastActivity)}
+
last activity
+
+
+
+
+
+
+ ) +}