Pool, competition & round pages overhaul: deep-link context, inline project management, AI filtering UX, email toggle
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m30s

- Pool page: auto-select program from edition context, URL params for roundId/competitionId deep-linking, unassigned toggle, round badges column
- Competition detail: rich round cards with project counts, dates, jury info, status badges replacing flat list
- Round detail: readiness checklist, embedded assignment dashboard, file requirements in config tab, notifyOnEntry toggle
- ProjectStatesTable: search input, project links, quick-add dialog, pool links with context params
- FilteringDashboard: expandable rows with AI reasoning inline, quick override buttons, search, clickable stats
- Backend: notifyOnEntry in round configJson triggers announcement emails on project assignment via existing email infra

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 08:23:40 +01:00
parent 7f334ed095
commit 845554fdb8
7 changed files with 1197 additions and 496 deletions

View File

@@ -40,13 +40,14 @@ import {
ChevronDown,
Layers,
Users,
FileBox,
FolderKanban,
ClipboardList,
Settings,
MoreHorizontal,
Archive,
Loader2,
Plus,
CalendarDays,
} from 'lucide-react'
import { CompetitionTimeline } from '@/components/admin/competition/competition-timeline'
@@ -298,10 +299,12 @@ export default function CompetitionDetailPage() {
<Card>
<CardContent className="pt-4 pb-3">
<div className="flex items-center gap-2">
<FileBox className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-medium">Windows</span>
<FolderKanban className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-medium">Projects</span>
</div>
<p className="text-2xl font-bold mt-1">{competition.submissionWindows.length}</p>
<p className="text-2xl font-bold mt-1">
{competition.rounds.reduce((sum: number, r: any) => sum + (r._count?.projectRoundStates ?? 0), 0)}
</p>
</CardContent>
</Card>
<Card>
@@ -349,39 +352,93 @@ export default function CompetitionDetailPage() {
</CardContent>
</Card>
) : (
<div className="space-y-2">
{competition.rounds.map((round, index) => (
<Link
key={round.id}
href={`/admin/competitions/${competitionId}/rounds/${round.id}` as Route}
>
<Card className="hover:shadow-sm transition-shadow cursor-pointer">
<CardContent className="flex items-center gap-3 py-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted text-sm font-bold shrink-0">
{index + 1}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{round.name}</p>
</div>
<Badge
variant="secondary"
className={cn(
'text-[10px] shrink-0',
roundTypeColors[round.roundType] ?? 'bg-gray-100 text-gray-700'
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
{competition.rounds.map((round: any, index: number) => {
const projectCount = round._count?.projectRoundStates ?? 0
const assignmentCount = round._count?.assignments ?? 0
const statusLabel = round.status.replace('ROUND_', '')
const statusColors: Record<string, string> = {
DRAFT: 'bg-gray-100 text-gray-600',
ACTIVE: 'bg-emerald-100 text-emerald-700',
CLOSED: 'bg-blue-100 text-blue-700',
ARCHIVED: 'bg-muted text-muted-foreground',
}
return (
<Link
key={round.id}
href={`/admin/competitions/${competitionId}/rounds/${round.id}` as Route}
>
<Card className="hover:shadow-md transition-shadow cursor-pointer h-full">
<CardContent className="pt-4 pb-3 space-y-3">
{/* Top: number + name + badges */}
<div className="flex items-start gap-2.5">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0 mt-0.5">
{index + 1}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold truncate">{round.name}</p>
<div className="flex flex-wrap gap-1.5 mt-1">
<Badge
variant="secondary"
className={cn(
'text-[10px]',
roundTypeColors[round.roundType] ?? 'bg-gray-100 text-gray-700'
)}
>
{round.roundType.replace('_', ' ')}
</Badge>
<Badge
variant="outline"
className={cn('text-[10px]', statusColors[statusLabel])}
>
{statusLabel}
</Badge>
</div>
</div>
</div>
{/* Stats row */}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Layers className="h-3.5 w-3.5" />
<span>{projectCount} project{projectCount !== 1 ? 's' : ''}</span>
</div>
{(round.roundType === 'EVALUATION' || round.roundType === 'FILTERING') && (
<div className="flex items-center gap-1.5 text-muted-foreground">
<ClipboardList className="h-3.5 w-3.5" />
<span>{assignmentCount} assignment{assignmentCount !== 1 ? 's' : ''}</span>
</div>
)}
</div>
{/* Dates */}
{(round.windowOpenAt || round.windowCloseAt) && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<CalendarDays className="h-3.5 w-3.5 shrink-0" />
<span>
{round.windowOpenAt
? new Date(round.windowOpenAt).toLocaleDateString()
: '?'}
{' \u2014 '}
{round.windowCloseAt
? new Date(round.windowCloseAt).toLocaleDateString()
: '?'}
</span>
</div>
)}
>
{round.roundType.replace('_', ' ')}
</Badge>
<Badge
variant="outline"
className="text-[10px] shrink-0 hidden sm:inline-flex"
>
{round.status.replace('ROUND_', '')}
</Badge>
</CardContent>
</Card>
</Link>
))}
{/* Jury group */}
{round.juryGroup && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Users className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{round.juryGroup.name}</span>
</div>
)}
</CardContent>
</Card>
</Link>
)
})}
</div>
)}
</TabsContent>