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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user