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

@@ -16,7 +16,6 @@ import {
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
@@ -34,6 +33,7 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ArrowLeft, Loader2, AlertCircle } from 'lucide-react'
const createRoundSchema = z.object({
@@ -58,6 +58,8 @@ function CreateRoundContent() {
const router = useRouter()
const searchParams = useSearchParams()
const programIdParam = searchParams.get('program')
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery()
@@ -82,7 +84,9 @@ function CreateRoundContent() {
await createRound.mutateAsync({
programId: data.programId,
name: data.name,
roundType,
requiredReviews: data.requiredReviews,
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ? new Date(data.votingStartAt) : undefined,
votingEndAt: data.votingEndAt ? new Date(data.votingEndAt) : undefined,
})
@@ -218,6 +222,14 @@ function CreateRoundContent() {
</CardContent>
</Card>
{/* Round Type & Settings */}
<RoundTypeSettings
roundType={roundType}
onRoundTypeChange={setRoundType}
settings={roundSettings}
onSettingsChange={setRoundSettings}
/>
<Card>
<CardHeader>
<CardTitle className="text-lg">Voting Window</CardTitle>