Round system redesign: Phases 1-7 complete

Full pipeline/track/stage architecture replacing the legacy round system.

Schema: 11 new models (Pipeline, Track, Stage, StageTransition,
ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor,
OverrideAction, AudienceVoter) + 8 new enums.

Backend: 9 new routers (pipeline, stage, routing, stageFiltering,
stageAssignment, cohort, live, decision, award) + 6 new services
(stage-engine, routing-engine, stage-filtering, stage-assignment,
stage-notifications, live-control).

Frontend: Pipeline wizard (17 components), jury stage pages (7),
applicant pipeline pages (3), public stage pages (2), admin pipeline
pages (5), shared stage components (3), SSE route, live hook.

Phase 6 refit: 23 routers/services migrated from roundId to stageId,
all frontend components refitted. Deleted round.ts (985 lines),
roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx,
10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs.

Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing,
TypeScript 0 errors, Next.js build succeeds, 13 integrity checks,
legacy symbol sweep clean, auto-seed on first Docker startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 13:57:09 +01:00
parent 8a328357e3
commit 331b67dae0
256 changed files with 29117 additions and 21424 deletions

View File

@@ -48,7 +48,7 @@ import { useDebouncedCallback } from 'use-debounce'
const PER_PAGE_OPTIONS = [10, 20, 50]
export function ObserverDashboardContent({ userName }: { userName?: string }) {
const [selectedRoundId, setSelectedRoundId] = useState<string>('all')
const [selectedStageId, setSelectedStageId] = useState<string>('all')
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
@@ -65,8 +65,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
debouncedSetSearch(value)
}
const handleRoundChange = (value: string) => {
setSelectedRoundId(value)
const handleStageChange = (value: string) => {
setSelectedStageId(value)
setPage(1)
}
@@ -75,38 +75,38 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
setPage(1)
}
// Fetch programs/rounds for the filter dropdown
const { data: programs } = trpc.program.list.useQuery({ includeRounds: true })
// Fetch programs/stages for the filter dropdown
const { data: programs } = trpc.program.list.useQuery({ includeStages: true })
const rounds = programs?.flatMap((p) =>
p.rounds.map((r) => ({
id: r.id,
name: r.name,
const stages = programs?.flatMap((p) =>
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
id: s.id,
name: s.name,
programName: `${p.year} Edition`,
status: r.status,
status: s.status,
}))
) || []
// Fetch dashboard stats
const roundIdParam = selectedRoundId !== 'all' ? selectedRoundId : undefined
const stageIdParam = selectedStageId !== 'all' ? selectedStageId : undefined
const { data: stats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery(
{ roundId: roundIdParam }
{ stageId: stageIdParam }
)
// Fetch projects
const { data: projectsData, isLoading: projectsLoading } = trpc.analytics.getAllProjects.useQuery({
roundId: roundIdParam,
stageId: stageIdParam,
search: debouncedSearch || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
page,
perPage,
})
// Fetch recent rounds for jury completion
const { data: recentRoundsData } = trpc.program.list.useQuery({ includeRounds: true })
const recentRounds = recentRoundsData?.flatMap((p) =>
p.rounds.map((r) => ({
...r,
// Fetch recent stages for jury completion
const { data: recentStagesData } = trpc.program.list.useQuery({ includeStages: true })
const recentStages = recentStagesData?.flatMap((p) =>
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
...s,
programName: `${p.year} Edition`,
}))
)?.slice(0, 5) || []
@@ -141,18 +141,18 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</div>
</div>
{/* Round Filter */}
{/* Stage Filter */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<label className="text-sm font-medium">Filter by Round:</label>
<Select value={selectedRoundId} onValueChange={handleRoundChange}>
<label className="text-sm font-medium">Filter by Stage:</label>
<Select value={selectedStageId} onValueChange={handleStageChange}>
<SelectTrigger className="w-full sm:w-[300px]">
<SelectValue placeholder="All Rounds" />
<SelectValue placeholder="All Stages" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Rounds</SelectItem>
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
<SelectItem value="all">All Stages</SelectItem>
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.programName} - {stage.name}
</SelectItem>
))}
</SelectContent>
@@ -184,7 +184,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<p className="text-sm font-medium text-muted-foreground">Programs</p>
<p className="text-2xl font-bold mt-1">{stats.programCount}</p>
<p className="text-xs text-muted-foreground mt-1">
{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}
{stats.activeStageCount} active round{stats.activeStageCount !== 1 ? 's' : ''}
</p>
</div>
<div className="rounded-xl bg-blue-50 p-3">
@@ -203,7 +203,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<p className="text-sm font-medium text-muted-foreground">Projects</p>
<p className="text-2xl font-bold mt-1">{stats.projectCount}</p>
<p className="text-xs text-muted-foreground mt-1">
{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}
{selectedStageId !== 'all' ? 'In selected stage' : 'Across all stages'}
</p>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
@@ -341,7 +341,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<TableCell className="max-w-[150px] truncate">{project.teamName || '-'}</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs whitespace-nowrap">
{project.roundName}
{project.stageName}
</Badge>
</TableCell>
<TableCell>
@@ -375,7 +375,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
)}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<Badge variant="outline" className="text-xs">
{project.roundName}
{project.stageName}
</Badge>
<div className="flex gap-3">
<span>Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'}</span>
@@ -465,8 +465,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</AnimatedCard>
)}
{/* Recent Rounds */}
{recentRounds.length > 0 && (
{/* Recent Stages */}
{recentStages.length > 0 && (
<AnimatedCard index={6}>
<Card>
<CardHeader>
@@ -474,40 +474,40 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<div className="rounded-lg bg-violet-500/10 p-1.5">
<BarChart3 className="h-4 w-4 text-violet-500" />
</div>
Recent Rounds
Recent Stages
</CardTitle>
<CardDescription>Overview of the latest voting rounds</CardDescription>
<CardDescription>Overview of the latest evaluation stages</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentRounds.map((round) => (
{recentStages.map((stage) => (
<div
key={round.id}
key={stage.id}
className="flex items-center justify-between rounded-lg border p-4 transition-all hover:shadow-sm"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">{round.name}</p>
<p className="font-medium">{stage.name}</p>
<Badge
variant={
round.status === 'ACTIVE'
stage.status === 'STAGE_ACTIVE'
? 'default'
: round.status === 'CLOSED'
: stage.status === 'STAGE_CLOSED'
? 'secondary'
: 'outline'
}
>
{round.status}
{stage.status === 'STAGE_ACTIVE' ? 'Active' : stage.status === 'STAGE_CLOSED' ? 'Closed' : stage.status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{round.programName}
{stage.programName}
</p>
</div>
<div className="text-right text-sm">
<p>{round._count?.projects || 0} projects</p>
<p>{stage._count?.projects || 0} projects</p>
<p className="text-muted-foreground">
{round._count?.assignments || 0} assignments
{stage._count?.assignments || 0} assignments
</p>
</div>
</div>