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:
@@ -52,6 +52,8 @@ import {
|
||||
Archive,
|
||||
Trash2,
|
||||
Loader2,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
} from 'lucide-react'
|
||||
import { format, isPast, isFuture } from 'date-fns'
|
||||
|
||||
@@ -106,6 +108,7 @@ function RoundsContent() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">Order</TableHead>
|
||||
<TableHead>Round</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Voting Window</TableHead>
|
||||
@@ -115,8 +118,15 @@ function RoundsContent() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{program.rounds.map((round) => (
|
||||
<RoundRow key={round.id} round={round} />
|
||||
{program.rounds.map((round, index) => (
|
||||
<RoundRow
|
||||
key={round.id}
|
||||
round={round}
|
||||
index={index}
|
||||
totalRounds={program.rounds.length}
|
||||
allRoundIds={program.rounds.map((r) => r.id)}
|
||||
programId={program.id}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -133,10 +143,40 @@ function RoundsContent() {
|
||||
)
|
||||
}
|
||||
|
||||
function RoundRow({ round }: { round: any }) {
|
||||
function RoundRow({
|
||||
round,
|
||||
index,
|
||||
totalRounds,
|
||||
allRoundIds,
|
||||
programId,
|
||||
}: {
|
||||
round: any
|
||||
index: number
|
||||
totalRounds: number
|
||||
allRoundIds: string[]
|
||||
programId: string
|
||||
}) {
|
||||
const utils = trpc.useUtils()
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
const reorder = trpc.round.reorder.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.program.list.invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
const moveUp = () => {
|
||||
const ids = [...allRoundIds]
|
||||
;[ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]
|
||||
reorder.mutate({ programId, roundIds: ids })
|
||||
}
|
||||
|
||||
const moveDown = () => {
|
||||
const ids = [...allRoundIds]
|
||||
;[ids[index], ids[index + 1]] = [ids[index + 1], ids[index]]
|
||||
reorder.mutate({ programId, roundIds: ids })
|
||||
}
|
||||
|
||||
const updateStatus = trpc.round.updateStatus.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.program.list.invalidate()
|
||||
@@ -229,6 +269,28 @@ function RoundRow({ round }: { round: any }) {
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={moveUp}
|
||||
disabled={index === 0 || reorder.isPending}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={moveDown}
|
||||
disabled={index === totalRounds - 1 || reorder.isPending}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={`/admin/rounds/${round.id}`}
|
||||
@@ -242,7 +304,7 @@ function RoundRow({ round }: { round: any }) {
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
{round._count?.projects || 0}
|
||||
{round._count?.roundProjects || 0}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -325,9 +387,9 @@ function RoundRow({ round }: { round: any }) {
|
||||
<AlertDialogTitle>Delete Round</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete "{round.name}"? This will
|
||||
permanently delete all {round._count?.projects || 0} projects,{' '}
|
||||
{round._count?.assignments || 0} assignments, and all evaluations
|
||||
in this round. This action cannot be undone.
|
||||
remove {round._count?.roundProjects || 0} project assignments,{' '}
|
||||
{round._count?.assignments || 0} reviewer assignments, and all evaluations
|
||||
in this round. The projects themselves will remain in the program. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user