From 461551b4891d9fe406db5457006352e725982fb6 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 5 Mar 2026 15:55:17 +0100 Subject: [PATCH] fix: separate main pipeline from award tracks on rounds page Award-specific rounds (e.g. Spotlight on Africa) were mixed into the main pipeline as a flat list, making them look like sequential steps after Deliberation. Now they render in their own amber-tinted card sections below the main pipeline, each with a header showing award name, pool size, eligibility mode, and status. Co-Authored-By: Claude Opus 4.6 --- src/app/(admin)/admin/rounds/page.tsx | 351 +++++++++++++++----------- 1 file changed, 204 insertions(+), 147 deletions(-) diff --git a/src/app/(admin)/admin/rounds/page.tsx b/src/app/(admin)/admin/rounds/page.tsx index e5f80ab..d49ecea 100644 --- a/src/app/(admin)/admin/rounds/page.tsx +++ b/src/app/(admin)/admin/rounds/page.tsx @@ -41,7 +41,6 @@ import { FileBox, Save, Loader2, - Award, Trophy, ArrowRight, } from 'lucide-react' @@ -151,27 +150,42 @@ export default function RoundsPage() { onError: (err) => toast.error(err.message), }) - const rounds = useMemo(() => { + // Split rounds into main pipeline (no specialAwardId) and award tracks + const mainRounds = useMemo(() => { const all = (compDetail?.rounds ?? []) as RoundWithStats[] - return filterType === 'all' ? all : all.filter((r) => r.roundType === filterType) + const main = all.filter((r) => !r.specialAwardId) + return filterType === 'all' ? main : main.filter((r) => r.roundType === filterType) }, [compDetail?.rounds, filterType]) - // Group awards by their evaluationRoundId - const awardsByRound = useMemo(() => { - const map = new Map() - for (const award of (awards ?? []) as SpecialAwardItem[]) { - if (award.evaluationRoundId) { - const existing = map.get(award.evaluationRoundId) ?? [] - existing.push(award) - map.set(award.evaluationRoundId, existing) + // Group award-track rounds by their specialAwardId, paired with the award metadata + const awardTrackGroups = useMemo(() => { + const allRounds = (compDetail?.rounds ?? []) as RoundWithStats[] + const awardRounds = allRounds.filter((r) => r.specialAwardId) + const groups = new Map() + + for (const round of awardRounds) { + const awardId = round.specialAwardId! + if (!groups.has(awardId)) { + const award = ((awards ?? []) as SpecialAwardItem[]).find((a) => a.id === awardId) + if (!award) continue + groups.set(awardId, { award, rounds: [] }) } + groups.get(awardId)!.rounds.push(round) } - return map - }, [awards]) + return Array.from(groups.values()) + }, [compDetail?.rounds, awards]) const floatingAwards = useMemo(() => { - return ((awards ?? []) as SpecialAwardItem[]).filter((a) => !a.evaluationRoundId) - }, [awards]) + // Awards that have no evaluationRoundId AND no rounds linked via specialAwardId + const awardIdsWithRounds = new Set( + ((compDetail?.rounds ?? []) as RoundWithStats[]) + .filter((r) => r.specialAwardId) + .map((r) => r.specialAwardId!) + ) + return ((awards ?? []) as SpecialAwardItem[]).filter( + (a) => !a.evaluationRoundId && !awardIdsWithRounds.has(a.id) + ) + }, [awards, compDetail?.rounds]) const handleCreateRound = () => { if (!roundForm.name.trim() || !roundForm.roundType || !comp) { @@ -271,8 +285,10 @@ export default function RoundsPage() { const activeFilter = filterType !== 'all' const totalProjects = (compDetail as any)?.distinctProjectCount ?? 0 const allRounds = (compDetail?.rounds ?? []) as RoundWithStats[] + const allMainRounds = allRounds.filter((r) => !r.specialAwardId) + const awardRoundCount = allRounds.length - allMainRounds.length const totalAssignments = allRounds.reduce((s, r) => s + r._count.assignments, 0) - const activeRound = allRounds.find((r) => r.status === 'ROUND_ACTIVE') + const activeRound = allMainRounds.find((r) => r.status === 'ROUND_ACTIVE') return ( @@ -313,7 +329,7 @@ export default function RoundsPage() {
- {allRounds.filter((r) => !r.specialAwardId).length} rounds + {allMainRounds.length} rounds{awardRoundCount > 0 ? ` + ${awardRoundCount} award` : ''} | {totalProjects} projects | @@ -330,12 +346,12 @@ export default function RoundsPage() { )} - {awards && awards.length > 0 && ( + {awardTrackGroups.length > 0 && ( <> | - - {awards.length} awards + + {awardTrackGroups.length} award {awardTrackGroups.length === 1 ? 'track' : 'tracks'} )} @@ -389,7 +405,7 @@ export default function RoundsPage() {
{/* ── Pipeline View ───────────────────────────────────────────── */} - {rounds.length === 0 ? ( + {mainRounds.length === 0 && awardTrackGroups.length === 0 ? (

@@ -397,142 +413,79 @@ export default function RoundsPage() {

) : ( -
- {/* Main pipeline track */} - {rounds.map((round, index) => { - const isLast = index === rounds.length - 1 - const typeColors = ROUND_TYPE_COLORS[round.roundType] ?? ROUND_TYPE_COLORS.INTAKE - const statusStyle = ROUND_STATUS_STYLES[round.status] ?? ROUND_STATUS_STYLES.ROUND_DRAFT - const projectCount = round._count.projectRoundStates - const assignmentCount = round._count.assignments - const roundAwards = awardsByRound.get(round.id) ?? [] +
+ {/* ── Main Competition Pipeline ───────────────────────── */} + {mainRounds.length > 0 && ( +
+ {mainRounds.map((round, index) => ( + + ))} +
+ )} + + {/* ── Award Track Sections ────────────────────────────── */} + {awardTrackGroups.map(({ award, rounds: awardRounds }) => { + const isExclusive = award.eligibilityMode === 'SEPARATE_POOL' + const eligible = award._count.eligibilities + const statusColor = AWARD_STATUS_COLORS[award.status] ?? 'text-gray-500' return ( -
- {/* Round row with pipeline connector */} -
- {/* Left: pipeline track */} -
- {/* Status dot */} - - -
-
- {statusStyle.pulse && ( -
- )} -
- - - {statusStyle.label} - - - {/* Connector line */} - {!isLast && ( -
- )} -
- - {/* Right: round content + awards */} -
-
- {/* Round row */} - -
- {/* Round type indicator */} - - {round.roundType.replace('_', ' ')} - - - {/* Round name */} - - {round.name} - - - {/* Stats cluster */} -
- {round.juryGroup && ( - - - {round.juryGroup.name} - - )} - - - {projectCount} - - {assignmentCount > 0 && ( - {assignmentCount} asgn - )} - {(round.windowOpenAt || round.windowCloseAt) && ( - - - {round.windowOpenAt - ? new Date(round.windowOpenAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) - : ''} - {round.windowOpenAt && round.windowCloseAt ? ' \u2013 ' : ''} - {round.windowCloseAt - ? new Date(round.windowCloseAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) - : ''} - - )} -
- - {/* Status badge (compact) */} - - {statusStyle.label} - - - {/* Arrow */} - -
- - - {/* Awards branching off this round */} - {roundAwards.length > 0 && ( -
- {/* Connector dash */} -
- {/* Award nodes */} -
- {roundAwards.map((award) => ( - - ))} -
-
- )} +
+ {/* Award track header */} + +
+
+
+
+

+ {award.name} +

+
+ {eligible} projects + · + + {isExclusive ? 'Exclusive pool' : 'Parallel'} + + · + + {award.status.replace('_', ' ')} + +
+
+
+ + + {/* Award track rounds */} +
+ {awardRounds.map((round, index) => ( + + ))}
) })} - {/* Floating awards (no evaluationRoundId) */} + {/* Floating awards (no linked rounds) */} {floatingAwards.length > 0 && ( -
+

Unlinked Awards

@@ -685,6 +638,110 @@ export default function RoundsPage() { ) } +// ─── Round Row ─────────────────────────────────────────────────────────────── + +function RoundRow({ round, isLast }: { round: RoundWithStats; isLast: boolean }) { + const typeColors = ROUND_TYPE_COLORS[round.roundType] ?? ROUND_TYPE_COLORS.INTAKE + const statusStyle = ROUND_STATUS_STYLES[round.status] ?? ROUND_STATUS_STYLES.ROUND_DRAFT + const projectCount = round._count.projectRoundStates + const assignmentCount = round._count.assignments + + return ( +
+ {/* Left: pipeline track */} +
+ + +
+
+ {statusStyle.pulse && ( +
+ )} +
+ + + {statusStyle.label} + + + {!isLast && ( +
+ )} +
+ + {/* Right: round content */} +
+ +
+ + {round.roundType.replace('_', ' ')} + + + + {round.name} + + +
+ {round.juryGroup && ( + + + {round.juryGroup.name} + + )} + + + {projectCount} + + {assignmentCount > 0 && ( + {assignmentCount} asgn + )} + {(round.windowOpenAt || round.windowCloseAt) && ( + + + {round.windowOpenAt + ? new Date(round.windowOpenAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + : ''} + {round.windowOpenAt && round.windowCloseAt ? ' \u2013 ' : ''} + {round.windowCloseAt + ? new Date(round.windowCloseAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short' }) + : ''} + + )} +
+ + + {statusStyle.label} + + + +
+ +
+
+ ) +} + // ─── Award Node ────────────────────────────────────────────────────────────── function AwardNode({ award }: { award: SpecialAwardItem }) {