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

@@ -30,26 +30,26 @@ function ImportPageContent() {
const router = useRouter()
const utils = trpc.useUtils()
const searchParams = useSearchParams()
const roundIdParam = searchParams.get('round')
const stageIdParam = searchParams.get('stage')
const [selectedRoundId, setSelectedRoundId] = useState<string>(roundIdParam || '')
const [selectedStageId, setSelectedStageId] = useState<string>(stageIdParam || '')
// Fetch active programs with rounds
// Fetch active programs with stages
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery({
status: 'ACTIVE',
includeRounds: true,
includeStages: true,
})
// Get all rounds from programs
const rounds = programs?.flatMap((p) =>
(p.rounds || []).map((r) => ({
...r,
// Get all stages from programs
const stages = programs?.flatMap((p) =>
((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({
...s,
programId: p.id,
programName: `${p.year} Edition`,
}))
) || []
const selectedRound = rounds.find((r) => r.id === selectedRoundId)
const selectedStage = stages.find((s: { id: string }) => s.id === selectedStageId)
if (loadingPrograms) {
return <ImportPageSkeleton />
@@ -70,44 +70,44 @@ function ImportPageContent() {
<div>
<h1 className="text-2xl font-semibold tracking-tight">Import Projects</h1>
<p className="text-muted-foreground">
Import projects from a CSV file into a round
Import projects from a CSV file into a stage
</p>
</div>
{/* Round selection */}
{!selectedRoundId && (
{/* Stage selection */}
{!selectedStageId && (
<Card>
<CardHeader>
<CardTitle>Select Round</CardTitle>
<CardTitle>Select Stage</CardTitle>
<CardDescription>
Choose the round you want to import projects into
Choose the stage you want to import projects into
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{rounds.length === 0 ? (
{stages.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<AlertCircle className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No Active Rounds</p>
<p className="mt-2 font-medium">No Active Stages</p>
<p className="text-sm text-muted-foreground">
Create a round first before importing projects
Create a stage first before importing projects
</p>
<Button asChild className="mt-4">
<Link href="/admin/rounds/new">Create Round</Link>
<Link href="/admin/rounds/new-pipeline">Create Pipeline</Link>
</Button>
</div>
) : (
<>
<Select value={selectedRoundId} onValueChange={setSelectedRoundId}>
<Select value={selectedStageId} onValueChange={setSelectedStageId}>
<SelectTrigger>
<SelectValue placeholder="Select a round" />
<SelectValue placeholder="Select a stage" />
</SelectTrigger>
<SelectContent>
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
<div className="flex flex-col">
<span>{round.name}</span>
<span>{stage.name}</span>
<span className="text-xs text-muted-foreground">
{round.programName}
{stage.programName}
</span>
</div>
</SelectItem>
@@ -117,11 +117,11 @@ function ImportPageContent() {
<Button
onClick={() => {
if (selectedRoundId) {
router.push(`/admin/projects/import?round=${selectedRoundId}`)
if (selectedStageId) {
router.push(`/admin/projects/import?stage=${selectedStageId}`)
}
}}
disabled={!selectedRoundId}
disabled={!selectedStageId}
>
Continue
</Button>
@@ -132,14 +132,14 @@ function ImportPageContent() {
)}
{/* Import form */}
{selectedRoundId && selectedRound && (
{selectedStageId && selectedStage && (
<div className="space-y-4">
<div className="flex items-center gap-4">
<FileSpreadsheet className="h-8 w-8 text-muted-foreground" />
<div>
<p className="font-medium">Importing into: {selectedRound.name}</p>
<p className="font-medium">Importing into: {selectedStage.name}</p>
<p className="text-sm text-muted-foreground">
{selectedRound.programName}
{selectedStage.programName}
</p>
</div>
<Button
@@ -147,11 +147,11 @@ function ImportPageContent() {
size="sm"
className="ml-auto"
onClick={() => {
setSelectedRoundId('')
setSelectedStageId('')
router.push('/admin/projects/import')
}}
>
Change Round
Change Stage
</Button>
</div>
@@ -172,32 +172,31 @@ function ImportPageContent() {
</TabsList>
<TabsContent value="csv" className="mt-4">
<CSVImportForm
programId={selectedRound.programId}
roundId={selectedRoundId}
roundName={selectedRound.name}
programId={selectedStage.programId}
stageName={selectedStage.name}
onSuccess={() => {
utils.project.list.invalidate()
utils.round.get.invalidate()
utils.program.get.invalidate()
}}
/>
</TabsContent>
<TabsContent value="notion" className="mt-4">
<NotionImportForm
roundId={selectedRoundId}
roundName={selectedRound.name}
programId={selectedStage.programId}
stageName={selectedStage.name}
onSuccess={() => {
utils.project.list.invalidate()
utils.round.get.invalidate()
utils.program.get.invalidate()
}}
/>
</TabsContent>
<TabsContent value="typeform" className="mt-4">
<TypeformImportForm
roundId={selectedRoundId}
roundName={selectedRound.name}
programId={selectedStage.programId}
stageName={selectedStage.name}
onSuccess={() => {
utils.project.list.invalidate()
utils.round.get.invalidate()
utils.program.get.invalidate()
}}
/>
</TabsContent>