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:
@@ -180,7 +180,7 @@ function LiveVotingContent({ roundId }: { roundId: string }) {
|
||||
if (storedOrder.length > 0) {
|
||||
setProjectOrder(storedOrder)
|
||||
} else {
|
||||
setProjectOrder(sessionData.round.projects.map((p) => p.id))
|
||||
setProjectOrder(sessionData.round.roundProjects.map((rp) => rp.project.id))
|
||||
}
|
||||
}
|
||||
}, [sessionData])
|
||||
@@ -253,7 +253,7 @@ function LiveVotingContent({ roundId }: { roundId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
const projects = sessionData.round.projects
|
||||
const projects = sessionData.round.roundProjects.map((rp) => rp.project)
|
||||
const sortedProjects = projectOrder
|
||||
.map((id) => projects.find((p) => p.id === id))
|
||||
.filter((p): p is Project => !!p)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, use } from 'react'
|
||||
import { Suspense, use, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
@@ -44,8 +44,14 @@ import {
|
||||
Filter,
|
||||
Trash2,
|
||||
Loader2,
|
||||
Plus,
|
||||
ArrowRightCircle,
|
||||
Minus,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { AssignProjectsDialog } from '@/components/admin/assign-projects-dialog'
|
||||
import { AdvanceProjectsDialog } from '@/components/admin/advance-projects-dialog'
|
||||
import { RemoveProjectsDialog } from '@/components/admin/remove-projects-dialog'
|
||||
import { format, formatDistanceToNow, isPast, isFuture } from 'date-fns'
|
||||
|
||||
interface PageProps {
|
||||
@@ -54,6 +60,9 @@ interface PageProps {
|
||||
|
||||
function RoundDetailContent({ roundId }: { roundId: string }) {
|
||||
const router = useRouter()
|
||||
const [assignOpen, setAssignOpen] = useState(false)
|
||||
const [advanceOpen, setAdvanceOpen] = useState(false)
|
||||
const [removeOpen, setRemoveOpen] = useState(false)
|
||||
|
||||
const { data: round, isLoading } = trpc.round.get.useQuery({ id: roundId })
|
||||
const { data: progress } = trpc.round.getProgress.useQuery({ id: roundId })
|
||||
@@ -235,7 +244,7 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{round._count.projects}</div>
|
||||
<div className="text-2xl font-bold">{round._count.roundProjects}</div>
|
||||
<Button variant="link" size="sm" className="px-0" asChild>
|
||||
<Link href={`/admin/projects?round=${round.id}`}>View projects</Link>
|
||||
</Button>
|
||||
@@ -423,9 +432,43 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
|
||||
View Projects
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setAssignOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Assign Projects
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setAdvanceOpen(true)}>
|
||||
<ArrowRightCircle className="mr-2 h-4 w-4" />
|
||||
Advance Projects
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setRemoveOpen(true)}>
|
||||
<Minus className="mr-2 h-4 w-4" />
|
||||
Remove Projects
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dialogs */}
|
||||
<AssignProjectsDialog
|
||||
roundId={roundId}
|
||||
programId={round.program.id}
|
||||
open={assignOpen}
|
||||
onOpenChange={setAssignOpen}
|
||||
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
|
||||
/>
|
||||
<AdvanceProjectsDialog
|
||||
roundId={roundId}
|
||||
programId={round.program.id}
|
||||
open={advanceOpen}
|
||||
onOpenChange={setAdvanceOpen}
|
||||
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
|
||||
/>
|
||||
<RemoveProjectsDialog
|
||||
roundId={roundId}
|
||||
open={removeOpen}
|
||||
onOpenChange={setRemoveOpen}
|
||||
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user