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

@@ -76,7 +76,7 @@ type Role = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERV
interface Assignment {
projectId: string
roundId: string
stageId: string
}
interface MemberRow {
@@ -269,7 +269,7 @@ export default function MemberInvitePage() {
} | null>(null)
// Pre-assignment state
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
const [selectedStageId, setSelectedStageId] = useState<string>('')
const utils = trpc.useUtils()
@@ -284,30 +284,27 @@ export default function MemberInvitePage() {
},
})
// Fetch programs with rounds for pre-assignment
// Fetch programs with stages for pre-assignment
const { data: programsData } = trpc.program.list.useQuery({
status: 'ACTIVE',
includeRounds: true,
includeStages: true,
})
// Flatten all rounds from all programs
const rounds = useMemo(() => {
// Flatten all stages from all programs
const stages = useMemo(() => {
if (!programsData) return []
type ProgramWithRounds = typeof programsData[number] & {
rounds?: Array<{ id: string; name: string }>
}
return (programsData as ProgramWithRounds[]).flatMap((program) =>
(program.rounds || []).map((round) => ({
id: round.id,
name: round.name,
return programsData.flatMap((program) =>
((program.stages ?? []) as Array<{ id: string; name: string }>).map((stage: { id: string; name: string }) => ({
id: stage.id,
name: stage.name,
programName: `${program.name} ${program.year}`,
}))
)
}, [programsData])
// Fetch projects for selected round
// Fetch projects for selected stage
const { data: projectsData, isLoading: projectsLoading } = trpc.project.list.useQuery(
{ roundId: selectedRoundId, perPage: 200 },
{ enabled: !!selectedRoundId }
{ stageId: selectedStageId, perPage: 200 },
{ enabled: !!selectedStageId }
)
const projects = projectsData?.projects || []
@@ -355,7 +352,7 @@ export default function MemberInvitePage() {
// Per-row project assignment management
const toggleProjectAssignment = (rowId: string, projectId: string) => {
if (!selectedRoundId) return
if (!selectedStageId) return
setRows((prev) =>
prev.map((r) => {
if (r.id !== rowId) return r
@@ -363,7 +360,7 @@ export default function MemberInvitePage() {
if (existing) {
return { ...r, assignments: r.assignments.filter((a) => a.projectId !== projectId) }
} else {
return { ...r, assignments: [...r.assignments, { projectId, roundId: selectedRoundId }] }
return { ...r, assignments: [...r.assignments, { projectId, stageId: selectedStageId }] }
}
})
)
@@ -587,21 +584,21 @@ export default function MemberInvitePage() {
<div className="flex-1 min-w-0">
<Label className="text-sm font-medium">Pre-assign Projects (Optional)</Label>
<p className="text-xs text-muted-foreground">
Select a round to assign projects to jury members before they onboard
Select a stage to assign projects to jury members before they onboard
</p>
</div>
<Select
value={selectedRoundId || 'none'}
onValueChange={(v) => setSelectedRoundId(v === 'none' ? '' : v)}
value={selectedStageId || 'none'}
onValueChange={(v) => setSelectedStageId(v === 'none' ? '' : v)}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select round" />
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No pre-assignment</SelectItem>
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.programName} - {stage.name}
</SelectItem>
))}
</SelectContent>
@@ -683,7 +680,7 @@ export default function MemberInvitePage() {
/>
{/* Per-member project pre-assignment (only for jury members) */}
{row.role === 'JURY_MEMBER' && selectedRoundId && (
{row.role === 'JURY_MEMBER' && selectedStageId && (
<Collapsible className="space-y-2">
<CollapsibleTrigger asChild>
<Button