Auto-assign projects to first round, auto-filter on close, pipeline UX consolidation

- New projects (admin create, CSV import, public form) auto-assign to program's
  first round (by sortOrder) when no round is specified
- Closing a FILTERING round auto-starts filtering job (configurable via
  autoFilterOnClose setting, defaults to true)
- Add SUBMISSION_RECEIVED notification type for confirming submissions
- Replace separate List/Pipeline toggle with integrated pipeline view below
  the sortable round list
- Add autoFilterOnClose toggle to filtering round type settings UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 15:06:11 +01:00
parent 2a5fa463b3
commit 7b85fd9602
11 changed files with 204 additions and 101 deletions

View File

@@ -64,6 +64,7 @@ const TEAM_NOTIFICATION_OPTIONS = [
{ value: 'ADVANCED_FINAL', label: 'Selected as Finalist', description: 'Congratulates team for being selected as finalist' },
{ value: 'NOT_SELECTED', label: 'Not Selected', description: 'Informs team they were not selected to continue' },
{ value: 'WINNER_ANNOUNCEMENT', label: 'Winner Announcement', description: 'Announces the team as a winner' },
{ value: 'SUBMISSION_RECEIVED', label: 'Submission Received', description: 'Confirms to the team that their submission has been received' },
]
interface PageProps {

View File

@@ -46,6 +46,7 @@ const TEAM_NOTIFICATION_OPTIONS = [
{ value: 'ADVANCED_FINAL', label: 'Selected as Finalist', description: 'Congratulates team for being selected as finalist' },
{ value: 'NOT_SELECTED', label: 'Not Selected', description: 'Informs team they were not selected to continue' },
{ value: 'WINNER_ANNOUNCEMENT', label: 'Winner Announcement', description: 'Announces the team as a winner' },
{ value: 'SUBMISSION_RECEIVED', label: 'Submission Received', description: 'Confirms to the team that their submission has been received' },
]
const createRoundSchema = z.object({

View File

@@ -62,9 +62,6 @@ import {
Trash2,
Loader2,
GripVertical,
ArrowRight,
List,
GitBranchPlus,
} from 'lucide-react'
import { format, isPast, isFuture } from 'date-fns'
import { cn } from '@/lib/utils'
@@ -84,7 +81,7 @@ type RoundData = {
}
}
function RoundsContent({ viewMode }: { viewMode: 'list' | 'pipeline' }) {
function RoundsContent() {
const { data: programs, isLoading } = trpc.program.list.useQuery({
includeRounds: true,
})
@@ -110,45 +107,6 @@ function RoundsContent({ viewMode }: { viewMode: 'list' | 'pipeline' }) {
)
}
if (viewMode === 'pipeline') {
return (
<div className="space-y-6">
{programs.map((program, index) => (
<AnimatedCard key={program.id} index={index}>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg">{program.year} Edition</CardTitle>
<CardDescription>
{program.name} - {program.status}
</CardDescription>
</div>
<Button asChild>
<Link href={`/admin/rounds/new?program=${program.id}`}>
<Plus className="mr-2 h-4 w-4" />
Add Round
</Link>
</Button>
</div>
</CardHeader>
<CardContent>
{(program.rounds && program.rounds.length > 0) ? (
<RoundPipeline rounds={program.rounds} programName={program.name} />
) : (
<div className="text-center py-8 text-muted-foreground">
<Calendar className="mx-auto h-8 w-8 mb-2 opacity-50" />
<p>No rounds created yet</p>
</div>
)}
</CardContent>
</Card>
</AnimatedCard>
))}
</div>
)
}
return (
<div className="space-y-6">
{programs.map((program, index) => (
@@ -271,32 +229,10 @@ function ProgramRounds({ program }: { program: any }) {
</SortableContext>
</DndContext>
{/* Flow visualization */}
{/* Pipeline visualization */}
{rounds.length > 1 && (
<div className="mt-6 pt-4 border-t">
<p className="text-xs text-muted-foreground mb-3 uppercase tracking-wide font-medium">
Project Flow
</p>
<div className="flex items-center gap-2 flex-wrap">
{rounds.map((round, index) => (
<div key={round.id} className="flex items-center gap-2">
<div className="flex items-center gap-2 bg-muted/50 rounded-lg px-3 py-1.5">
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-xs font-bold">
{index}
</span>
<span className="text-sm font-medium truncate max-w-[120px]">
{round.name}
</span>
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
{round._count?.projects || 0}
</Badge>
</div>
{index < rounds.length - 1 && (
<ArrowRight className="h-4 w-4 text-muted-foreground/50" />
)}
</div>
))}
</div>
<RoundPipeline rounds={rounds} programName={program.name} />
</div>
)}
</div>
@@ -711,43 +647,19 @@ function RoundsListSkeleton() {
}
export default function RoundsPage() {
const [viewMode, setViewMode] = useState<'list' | 'pipeline'>('list')
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Rounds</h1>
<p className="text-muted-foreground">
Manage selection rounds and voting periods
</p>
</div>
<div className="flex items-center gap-1 rounded-lg border p-1">
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="sm"
className="h-8 px-3"
onClick={() => setViewMode('list')}
>
<List className="mr-1.5 h-4 w-4" />
List
</Button>
<Button
variant={viewMode === 'pipeline' ? 'default' : 'ghost'}
size="sm"
className="h-8 px-3"
onClick={() => setViewMode('pipeline')}
>
<GitBranchPlus className="mr-1.5 h-4 w-4" />
Pipeline
</Button>
</div>
<div>
<h1 className="text-2xl font-semibold tracking-tight">Rounds</h1>
<p className="text-muted-foreground">
Manage selection rounds and voting periods
</p>
</div>
{/* Content */}
<Suspense fallback={<RoundsListSkeleton />}>
<RoundsContent viewMode={viewMode} />
<RoundsContent />
</Suspense>
</div>
)