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

@@ -31,7 +31,7 @@ export default function ProjectPoolPage() {
const [selectedProgramId, setSelectedProgramId] = useState<string>('')
const [selectedProjects, setSelectedProjects] = useState<string[]>([])
const [assignDialogOpen, setAssignDialogOpen] = useState(false)
const [targetRoundId, setTargetRoundId] = useState<string>('')
const [targetStageId, setTargetStageId] = useState<string>('')
const [searchQuery, setSearchQuery] = useState('')
const [categoryFilter, setCategoryFilter] = useState<'STARTUP' | 'BUSINESS_CONCEPT' | 'all'>('all')
const [currentPage, setCurrentPage] = useState(1)
@@ -50,20 +50,22 @@ export default function ProjectPoolPage() {
{ enabled: !!selectedProgramId }
)
const { data: rounds, isLoading: isLoadingRounds } = trpc.round.listByProgram.useQuery(
{ programId: selectedProgramId },
// Get stages from the selected program (program.list includes rounds/stages)
const { data: selectedProgramData, isLoading: isLoadingStages } = trpc.program.get.useQuery(
{ id: selectedProgramId },
{ enabled: !!selectedProgramId }
)
const stages = (selectedProgramData?.stages || []) as Array<{ id: string; name: string }>
const utils = trpc.useUtils()
const assignMutation = trpc.projectPool.assignToRound.useMutation({
const assignMutation = trpc.projectPool.assignToStage.useMutation({
onSuccess: (result) => {
utils.project.list.invalidate()
utils.round.get.invalidate()
toast.success(`Assigned ${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} to round`)
utils.program.get.invalidate()
toast.success(`Assigned ${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} to stage`)
setSelectedProjects([])
setAssignDialogOpen(false)
setTargetRoundId('')
setTargetStageId('')
refetch()
},
onError: (error) => {
@@ -72,17 +74,17 @@ export default function ProjectPoolPage() {
})
const handleBulkAssign = () => {
if (selectedProjects.length === 0 || !targetRoundId) return
if (selectedProjects.length === 0 || !targetStageId) return
assignMutation.mutate({
projectIds: selectedProjects,
roundId: targetRoundId,
stageId: targetStageId,
})
}
const handleQuickAssign = (projectId: string, roundId: string) => {
const handleQuickAssign = (projectId: string, stageId: string) => {
assignMutation.mutate({
projectIds: [projectId],
roundId,
stageId,
})
}
@@ -109,7 +111,7 @@ export default function ProjectPoolPage() {
<div>
<h1 className="text-2xl font-semibold">Project Pool</h1>
<p className="text-muted-foreground">
Assign unassigned projects to evaluation rounds
Assign unassigned projects to evaluation stages
</p>
</div>
@@ -236,20 +238,20 @@ export default function ProjectPoolPage() {
: '-'}
</td>
<td className="p-3">
{isLoadingRounds ? (
{isLoadingStages ? (
<Skeleton className="h-9 w-[200px]" />
) : (
<Select
onValueChange={(roundId) => handleQuickAssign(project.id, roundId)}
onValueChange={(stageId) => handleQuickAssign(project.id, stageId)}
disabled={assignMutation.isPending}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Assign to round..." />
<SelectValue placeholder="Assign to stage..." />
</SelectTrigger>
<SelectContent>
{rounds?.map((round: { id: string; name: string; sortOrder: number }) => (
<SelectItem key={round.id} value={round.id}>
{round.name}
{stages?.map((stage: { id: string; name: string }) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.name}
</SelectItem>
))}
</SelectContent>
@@ -310,20 +312,20 @@ export default function ProjectPoolPage() {
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Assign Projects to Round</DialogTitle>
<DialogTitle>Assign Projects to Stage</DialogTitle>
<DialogDescription>
Assign {selectedProjects.length} selected project{selectedProjects.length > 1 ? 's' : ''} to:
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<Select value={targetRoundId} onValueChange={setTargetRoundId}>
<Select value={targetStageId} onValueChange={setTargetStageId}>
<SelectTrigger>
<SelectValue placeholder="Select round..." />
<SelectValue placeholder="Select stage..." />
</SelectTrigger>
<SelectContent>
{rounds?.map((round: { id: string; name: string; sortOrder: number }) => (
<SelectItem key={round.id} value={round.id}>
{round.name}
{stages?.map((stage: { id: string; name: string }) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.name}
</SelectItem>
))}
</SelectContent>
@@ -335,7 +337,7 @@ export default function ProjectPoolPage() {
</Button>
<Button
onClick={handleBulkAssign}
disabled={!targetRoundId || assignMutation.isPending}
disabled={!targetStageId || assignMutation.isPending}
>
{assignMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Assign