feat: award round reordering, assign-to-first-round, and applicant timeline for award tracks
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m22s

- Add drag-and-drop round reordering on award detail Rounds tab (dnd-kit)
- Replace "Open Voting" with "Assign to First Round" for SEPARATE_POOL awards
- Add reorderAwardRounds mutation (two-phase transaction for unique constraint)
- Add assignToFirstRound mutation (re-runnable, moves/creates ProjectRoundState)
- Extend applicant timeline to show award-specific rounds for SEPARATE_POOL projects
- Hide irrelevant main competition rounds when project is in award track
- Prefix award round labels with award name in timeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 23:42:21 +01:00
parent 1d4e31ddd1
commit daf50831f1
3 changed files with 440 additions and 102 deletions

View File

@@ -1443,7 +1443,7 @@ export const applicantRouter = router({
return { competitionName: null, entries: [] }
}
// Get all rounds ordered by sortOrder
// Get all rounds ordered by sortOrder (including award rounds in same competition)
const rounds = await ctx.prisma.round.findMany({
where: { competitionId: competition.id },
orderBy: { sortOrder: 'asc' },
@@ -1454,6 +1454,8 @@ export const applicantRouter = router({
status: true,
windowOpenAt: true,
windowCloseAt: true,
specialAwardId: true,
specialAward: { select: { name: true } },
},
})
@@ -1482,12 +1484,29 @@ export const applicantRouter = router({
const liveFinalRounds = rounds.filter((r) => r.roundType === 'LIVE_FINAL')
const deliberationRounds = rounds.filter((r) => r.roundType === 'DELIBERATION')
// Check if this project is in any SEPARATE_POOL award track
const projectAwardRoundIds = new Set(
rounds.filter((r) => r.specialAwardId && stateMap.has(r.id)).map((r) => r.id)
)
const projectAwardIds = new Set(
rounds.filter((r) => r.specialAwardId && stateMap.has(r.id)).map((r) => r.specialAwardId!)
)
const isInAwardTrack = projectAwardRoundIds.size > 0
// Process visible rounds: hide FILTERING, LIVE_FINAL, DELIBERATION always.
// Also hide MENTORING unless the project is actually participating in it.
// For award rounds: only show ones the project is in. For main rounds after
// the split point: hide if project isn't in them and is in an award track.
const visibleRounds = rounds.filter(
(r) => {
if (r.roundType === 'FILTERING' || r.roundType === 'LIVE_FINAL' || r.roundType === 'DELIBERATION') return false
if (r.roundType === 'MENTORING' && !stateMap.has(r.id)) return false
// Award round that project is NOT in → hide
if (r.specialAwardId && !stateMap.has(r.id)) return false
// Award round for a different award → hide
if (r.specialAwardId && !projectAwardIds.has(r.specialAwardId)) return false
// Main competition round where project has no state AND project is in award track → hide
if (!r.specialAwardId && isInAwardTrack && !stateMap.has(r.id)) return false
return true
}
)
@@ -1520,7 +1539,7 @@ export const applicantRouter = router({
entries.push({
id: round.id,
label: round.name,
label: round.specialAward ? `${round.specialAward.name}: ${round.name}` : round.name,
roundType: round.roundType,
status: round.status,
windowOpenAt: round.windowOpenAt,