Redesign admin dashboard: pipeline view, round-specific stats, smart actions
Complete rewrite of the admin dashboard replacing the 1056-line monolith with
modular components. Key improvements:
- Competition pipeline: horizontal visualization of all rounds in pipeline order
(by sortOrder, not creation date) with type-specific icons and metrics
- Round-specific stats: stat cards dynamically change based on active round type
(INTAKE shows submissions/docs, FILTERING shows pass/fail/flagged, EVALUATION
shows assignments/completion, fallback shows generic project/jury stats)
- Smart actions: context-aware "Action Required" panel that only flags the next
draft round (not all), only flags unassigned projects for EVALUATION rounds,
includes deadline warnings with severity levels (critical/warning/info)
- Active round panel: detailed view with project state bar, type-specific content,
and deadline countdown
- Extracted components: project list, activity feed, category breakdown, skeleton
Backend enriched with 17 parallel queries building rich PipelineRound data
including per-round project states, eval stats, filtering stats, live session
status, and deliberation counts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:12:28 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import type { Route } from 'next'
|
|
|
|
|
import { cn } from '@/lib/utils'
|
|
|
|
|
import { motion } from 'motion/react'
|
|
|
|
|
import {
|
|
|
|
|
Upload,
|
|
|
|
|
Filter,
|
|
|
|
|
ClipboardCheck,
|
|
|
|
|
FileUp,
|
|
|
|
|
GraduationCap,
|
|
|
|
|
Radio,
|
|
|
|
|
Scale,
|
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
|
|
|
|
|
type PipelineRound = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
slug: string
|
|
|
|
|
roundType:
|
|
|
|
|
| 'INTAKE'
|
|
|
|
|
| 'FILTERING'
|
|
|
|
|
| 'EVALUATION'
|
|
|
|
|
| 'SUBMISSION'
|
|
|
|
|
| 'MENTORING'
|
|
|
|
|
| 'LIVE_FINAL'
|
|
|
|
|
| 'DELIBERATION'
|
|
|
|
|
status:
|
|
|
|
|
| 'ROUND_DRAFT'
|
|
|
|
|
| 'ROUND_ACTIVE'
|
|
|
|
|
| 'ROUND_CLOSED'
|
|
|
|
|
| 'ROUND_ARCHIVED'
|
|
|
|
|
sortOrder: number
|
|
|
|
|
windowOpenAt: Date | null
|
|
|
|
|
windowCloseAt: Date | null
|
|
|
|
|
projectStates: {
|
|
|
|
|
PENDING: number
|
|
|
|
|
IN_PROGRESS: number
|
|
|
|
|
PASSED: number
|
|
|
|
|
REJECTED: number
|
|
|
|
|
COMPLETED: number
|
|
|
|
|
WITHDRAWN: number
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
assignmentCount: number
|
|
|
|
|
evalSubmitted: number
|
|
|
|
|
evalDraft: number
|
|
|
|
|
evalTotal: number
|
|
|
|
|
filteringPassed: number
|
|
|
|
|
filteringRejected: number
|
|
|
|
|
filteringFlagged: number
|
|
|
|
|
filteringTotal: number
|
|
|
|
|
liveSessionStatus: string | null
|
|
|
|
|
deliberationCount: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roundTypeConfig: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ icon: typeof Upload; iconColor: string; iconBg: string }
|
|
|
|
|
> = {
|
|
|
|
|
INTAKE: { icon: Upload, iconColor: 'text-sky-600', iconBg: 'bg-sky-100' },
|
|
|
|
|
FILTERING: {
|
|
|
|
|
icon: Filter,
|
|
|
|
|
iconColor: 'text-amber-600',
|
|
|
|
|
iconBg: 'bg-amber-100',
|
|
|
|
|
},
|
|
|
|
|
EVALUATION: {
|
|
|
|
|
icon: ClipboardCheck,
|
|
|
|
|
iconColor: 'text-violet-600',
|
|
|
|
|
iconBg: 'bg-violet-100',
|
|
|
|
|
},
|
|
|
|
|
SUBMISSION: {
|
|
|
|
|
icon: FileUp,
|
|
|
|
|
iconColor: 'text-blue-600',
|
|
|
|
|
iconBg: 'bg-blue-100',
|
|
|
|
|
},
|
|
|
|
|
MENTORING: {
|
|
|
|
|
icon: GraduationCap,
|
|
|
|
|
iconColor: 'text-teal-600',
|
|
|
|
|
iconBg: 'bg-teal-100',
|
|
|
|
|
},
|
|
|
|
|
LIVE_FINAL: {
|
|
|
|
|
icon: Radio,
|
|
|
|
|
iconColor: 'text-red-600',
|
|
|
|
|
iconBg: 'bg-red-100',
|
|
|
|
|
},
|
|
|
|
|
DELIBERATION: {
|
|
|
|
|
icon: Scale,
|
|
|
|
|
iconColor: 'text-indigo-600',
|
|
|
|
|
iconBg: 'bg-indigo-100',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusStyles: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ container: string; label: string }
|
|
|
|
|
> = {
|
|
|
|
|
ROUND_DRAFT: {
|
|
|
|
|
container:
|
|
|
|
|
'bg-slate-50 border-slate-200 text-slate-400 border-dashed',
|
|
|
|
|
label: 'Draft',
|
|
|
|
|
},
|
|
|
|
|
ROUND_ACTIVE: {
|
|
|
|
|
container:
|
|
|
|
|
'bg-blue-50 border-blue-300 text-blue-700 ring-2 ring-blue-400/30 shadow-lg shadow-blue-500/10',
|
|
|
|
|
label: 'Active',
|
|
|
|
|
},
|
|
|
|
|
ROUND_CLOSED: {
|
|
|
|
|
container: 'bg-emerald-50 border-emerald-200 text-emerald-600',
|
|
|
|
|
label: 'Closed',
|
|
|
|
|
},
|
|
|
|
|
ROUND_ARCHIVED: {
|
|
|
|
|
container: 'bg-slate-50/50 border-slate-100 text-slate-300',
|
|
|
|
|
label: 'Archived',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMetric(round: PipelineRound): string {
|
|
|
|
|
const { roundType, projectStates, filteringTotal, filteringPassed, evalTotal, evalSubmitted, assignmentCount, liveSessionStatus, deliberationCount } = round
|
|
|
|
|
|
|
|
|
|
switch (roundType) {
|
|
|
|
|
case 'INTAKE':
|
|
|
|
|
return `${projectStates.total} submitted`
|
|
|
|
|
case 'FILTERING':
|
|
|
|
|
return filteringTotal > 0
|
|
|
|
|
? `${filteringPassed}/${filteringTotal} passed`
|
|
|
|
|
: `${projectStates.total} to filter`
|
|
|
|
|
case 'EVALUATION':
|
|
|
|
|
return evalTotal > 0
|
|
|
|
|
? `${evalSubmitted}/${evalTotal} evaluated`
|
|
|
|
|
: `${assignmentCount} assignments`
|
|
|
|
|
case 'SUBMISSION':
|
|
|
|
|
return `${projectStates.COMPLETED} submitted`
|
|
|
|
|
case 'MENTORING':
|
2026-02-19 11:11:00 +01:00
|
|
|
return `${projectStates.COMPLETED ?? 0} mentored`
|
|
|
|
|
case 'LIVE_FINAL': {
|
|
|
|
|
const status = liveSessionStatus
|
|
|
|
|
return status ? status.charAt(0) + status.slice(1).toLowerCase() : `${projectStates.total} finalists`
|
|
|
|
|
}
|
Redesign admin dashboard: pipeline view, round-specific stats, smart actions
Complete rewrite of the admin dashboard replacing the 1056-line monolith with
modular components. Key improvements:
- Competition pipeline: horizontal visualization of all rounds in pipeline order
(by sortOrder, not creation date) with type-specific icons and metrics
- Round-specific stats: stat cards dynamically change based on active round type
(INTAKE shows submissions/docs, FILTERING shows pass/fail/flagged, EVALUATION
shows assignments/completion, fallback shows generic project/jury stats)
- Smart actions: context-aware "Action Required" panel that only flags the next
draft round (not all), only flags unassigned projects for EVALUATION rounds,
includes deadline warnings with severity levels (critical/warning/info)
- Active round panel: detailed view with project state bar, type-specific content,
and deadline countdown
- Extracted components: project list, activity feed, category breakdown, skeleton
Backend enriched with 17 parallel queries building rich PipelineRound data
including per-round project states, eval stats, filtering stats, live session
status, and deliberation counts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:12:28 +01:00
|
|
|
case 'DELIBERATION':
|
|
|
|
|
return deliberationCount > 0
|
|
|
|
|
? `${deliberationCount} sessions`
|
|
|
|
|
: 'Not started'
|
|
|
|
|
default:
|
|
|
|
|
return `${projectStates.total} projects`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PipelineRoundNode({
|
|
|
|
|
round,
|
|
|
|
|
index,
|
|
|
|
|
}: {
|
|
|
|
|
round: PipelineRound
|
|
|
|
|
index: number
|
|
|
|
|
}) {
|
|
|
|
|
const typeConfig = roundTypeConfig[round.roundType] ?? roundTypeConfig.INTAKE
|
|
|
|
|
const Icon = typeConfig.icon
|
|
|
|
|
const status = statusStyles[round.status] ?? statusStyles.ROUND_DRAFT
|
|
|
|
|
const isActive = round.status === 'ROUND_ACTIVE'
|
|
|
|
|
const metric = getMetric(round)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 8 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ duration: 0.3, delay: 0.1 + index * 0.06 }}
|
|
|
|
|
>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/admin/rounds/${round.id}` as Route}
|
|
|
|
|
className="group block"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'relative flex flex-col items-center rounded-xl border-2 p-3 transition-all hover:-translate-y-0.5 hover:shadow-md',
|
|
|
|
|
isActive ? 'w-44' : 'w-36',
|
|
|
|
|
status.container
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Active ping indicator */}
|
|
|
|
|
{isActive && (
|
|
|
|
|
<span className="absolute -right-1 -top-1 flex h-3 w-3">
|
|
|
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75" />
|
|
|
|
|
<span className="relative inline-flex h-3 w-3 rounded-full bg-blue-500" />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Icon */}
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex h-8 w-8 items-center justify-center rounded-lg',
|
|
|
|
|
typeConfig.iconBg
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Icon className={cn('h-4 w-4', typeConfig.iconColor)} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Name */}
|
|
|
|
|
<p className="mt-2 text-center text-xs font-semibold leading-tight line-clamp-2 group-hover:text-foreground transition-colors">
|
|
|
|
|
{round.name}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{/* Status label */}
|
|
|
|
|
<span className="mt-1.5 text-[10px] font-medium uppercase tracking-wider opacity-70">
|
|
|
|
|
{status.label}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{/* Metric */}
|
|
|
|
|
<p className="mt-1 text-[11px] font-medium tabular-nums opacity-80">
|
|
|
|
|
{metric}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type { PipelineRound }
|