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:
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user