Decouple projects from rounds with RoundProject join table

Projects now exist at the program level instead of being locked to a
single round. A new RoundProject join table enables many-to-many
relationships with per-round status tracking. Rounds have sortOrder
for configurable progression paths.

- Add RoundProject model, programId on Project, sortOrder on Round
- Migration preserves existing data (roundId -> RoundProject entries)
- Update all routers to query through RoundProject join
- Add assign/remove/advance/reorder round endpoints
- Add Assign, Advance, Remove Projects dialogs on round detail page
- Add round reorder controls (up/down arrows) on rounds list
- Show all rounds on project detail page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 22:33:55 +01:00
parent 0d2bc4db7e
commit fd5e5222da
52 changed files with 1892 additions and 326 deletions

View File

@@ -335,6 +335,7 @@ model Program {
// Relations
rounds Round[]
projects Project[]
learningResources LearningResource[]
partners Partner[]
applicationForms ApplicationForm[]
@@ -351,6 +352,7 @@ model Round {
slug String? @unique // URL-friendly identifier for public submissions
status RoundStatus @default(DRAFT)
roundType RoundType @default(EVALUATION)
sortOrder Int @default(0) // Progression order within program
// Submission window (for applicant portal)
submissionDeadline DateTime? // Deadline for project submissions
@@ -375,7 +377,7 @@ model Round {
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
projects Project[]
roundProjects RoundProject[]
assignments Assignment[]
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
@@ -419,13 +421,12 @@ model EvaluationForm {
model Project {
id String @id @default(cuid())
roundId String
programId String
// Core fields
title String
teamName String?
description String? @db.Text
status ProjectStatus @default(SUBMITTED)
// Competition category
competitionCategory CompetitionCategory?
@@ -474,7 +475,8 @@ model Project {
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
files ProjectFile[]
assignments Assignment[]
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
@@ -485,8 +487,7 @@ model Project {
awardVotes AwardVote[]
wonAwards SpecialAward[] @relation("AwardWinner")
@@index([roundId])
@@index([status])
@@index([programId])
@@index([tags])
@@index([submissionSource])
@@index([submittedByUserId])
@@ -495,6 +496,23 @@ model Project {
@@index([country])
}
model RoundProject {
id String @id @default(cuid())
roundId String
projectId String
status ProjectStatus @default(SUBMITTED)
addedAt DateTime @default(now())
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([status])
}
model ProjectFile {
id String @id @default(cuid())
projectId String