feat: finalization tab respects ranking overrides, grouped by category
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m2s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m2s
- processRoundClose now applies reordersJson drag-reorder overrides when building the evaluation pass set (was ignoring admin reorders) - Finalization tab groups proposed outcomes by category (Startup/Concept) with per-group pass/reject/total counts - Added category filter dropdown alongside the existing outcome filter - Removed legacy "Advance Top N" button and dialog from ranking page (replaced by the finalization workflow) - Fix project edit status defaultValue showing empty placeholder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -76,6 +76,7 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [filterOutcome, setFilterOutcome] = useState<'all' | 'PASSED' | 'REJECTED' | 'none'>('all')
|
||||
const [filterCategory, setFilterCategory] = useState<'all' | 'STARTUP' | 'BUSINESS_CONCEPT'>('all')
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
const [emailSectionOpen, setEmailSectionOpen] = useState(false)
|
||||
const [advancementMessage, setAdvancementMessage] = useState('')
|
||||
@@ -135,9 +136,32 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
||||
(filterOutcome === 'none' && !p.proposedOutcome) ||
|
||||
p.proposedOutcome === filterOutcome
|
||||
|
||||
return matchesSearch && matchesFilter
|
||||
const matchesCategory =
|
||||
filterCategory === 'all' || p.category === filterCategory
|
||||
|
||||
return matchesSearch && matchesFilter && matchesCategory
|
||||
})
|
||||
}, [summary, search, filterOutcome])
|
||||
}, [summary, search, filterOutcome, filterCategory])
|
||||
|
||||
// Check if we have multiple categories (to decide whether to group)
|
||||
const hasMultipleCategories = useMemo(() => {
|
||||
if (!summary) return false
|
||||
const cats = new Set(summary.projects.map((p) => p.category).filter(Boolean))
|
||||
return cats.size > 1
|
||||
}, [summary])
|
||||
|
||||
// Group filtered projects by category, sorted by rank within each group
|
||||
const groupedProjects = useMemo(() => {
|
||||
if (!hasMultipleCategories || filterCategory !== 'all') return null
|
||||
const groups: { category: string; label: string; projects: typeof filteredProjects }[] = []
|
||||
const startups = filteredProjects.filter((p) => p.category === 'STARTUP')
|
||||
const concepts = filteredProjects.filter((p) => p.category === 'BUSINESS_CONCEPT')
|
||||
const other = filteredProjects.filter((p) => p.category !== 'STARTUP' && p.category !== 'BUSINESS_CONCEPT')
|
||||
if (startups.length > 0) groups.push({ category: 'STARTUP', label: 'Startups', projects: startups })
|
||||
if (concepts.length > 0) groups.push({ category: 'BUSINESS_CONCEPT', label: 'Business Concepts', projects: concepts })
|
||||
if (other.length > 0) groups.push({ category: 'OTHER', label: 'Other', projects: other })
|
||||
return groups
|
||||
}, [filteredProjects, hasMultipleCategories, filterCategory])
|
||||
|
||||
// Counts
|
||||
const passedCount = summary?.projects.filter((p) => p.proposedOutcome === 'PASSED').length ?? 0
|
||||
@@ -163,6 +187,93 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
||||
batchUpdate.mutate({ roundId, outcomes })
|
||||
}
|
||||
|
||||
// Column count for colSpan
|
||||
const colCount = (summary?.isFinalized ? 0 : 1) + 4 + (summary?.roundType === 'EVALUATION' ? 1 : 0) + 1
|
||||
|
||||
// Shared row renderer
|
||||
const renderProjectRow = (project: (typeof filteredProjects)[number]) => (
|
||||
<tr key={project.id} className="border-b last:border-0 hover:bg-muted/30">
|
||||
{!summary?.isFinalized && (
|
||||
<td className="px-3 py-2.5">
|
||||
<Checkbox
|
||||
checked={selectedIds.has(project.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const next = new Set(selectedIds)
|
||||
if (checked) next.add(project.id)
|
||||
else next.delete(project.id)
|
||||
setSelectedIds(next)
|
||||
}}
|
||||
aria-label={`Select ${project.title}`}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-3 py-2.5">
|
||||
<div className="font-medium truncate max-w-[200px]">{project.title}</div>
|
||||
{project.teamName && (
|
||||
<div className="text-xs text-muted-foreground truncate">{project.teamName}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 hidden sm:table-cell text-muted-foreground">
|
||||
{project.category === 'STARTUP' ? 'Startup' : project.category === 'BUSINESS_CONCEPT' ? 'Concept' : project.category ?? '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 hidden md:table-cell text-muted-foreground">
|
||||
{project.country ?? '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
<Badge variant="secondary" className={cn('text-xs', stateLabelColors[project.currentState] ?? '')}>
|
||||
{project.currentState.replace('_', ' ')}
|
||||
</Badge>
|
||||
</td>
|
||||
{summary?.roundType === 'EVALUATION' && (
|
||||
<td className="px-3 py-2.5 text-center hidden lg:table-cell text-muted-foreground">
|
||||
{project.evaluationScore != null
|
||||
? `${project.evaluationScore.toFixed(1)} (#${project.rankPosition ?? '-'})`
|
||||
: '-'}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
{summary?.isFinalized ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
project.proposedOutcome === 'PASSED' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700',
|
||||
)}
|
||||
>
|
||||
{project.proposedOutcome === 'PASSED' ? 'Advanced' : 'Rejected'}
|
||||
</Badge>
|
||||
) : (
|
||||
<Select
|
||||
value={project.proposedOutcome ?? 'undecided'}
|
||||
onValueChange={(v) => {
|
||||
if (v === 'undecided') return
|
||||
updateOutcome.mutate({
|
||||
roundId,
|
||||
projectId: project.id,
|
||||
proposedOutcome: v as 'PASSED' | 'REJECTED',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'h-8 w-[130px] text-xs mx-auto',
|
||||
project.proposedOutcome === 'PASSED' && 'border-green-300 bg-green-50 text-green-700',
|
||||
project.proposedOutcome === 'REJECTED' && 'border-red-300 bg-red-50 text-red-700',
|
||||
)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="undecided" disabled>Undecided</SelectItem>
|
||||
<SelectItem value="PASSED">Pass</SelectItem>
|
||||
<SelectItem value="REJECTED">Reject</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -362,12 +473,24 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
||||
<SelectValue placeholder="Filter by outcome" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All projects</SelectItem>
|
||||
<SelectItem value="all">All outcomes</SelectItem>
|
||||
<SelectItem value="PASSED">Proposed: Pass</SelectItem>
|
||||
<SelectItem value="REJECTED">Proposed: Reject</SelectItem>
|
||||
<SelectItem value="none">Undecided</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{hasMultipleCategories && (
|
||||
<Select value={filterCategory} onValueChange={(v) => setFilterCategory(v as typeof filterCategory)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Filter by category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All categories</SelectItem>
|
||||
<SelectItem value="STARTUP">Startups</SelectItem>
|
||||
<SelectItem value="BUSINESS_CONCEPT">Business Concepts</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bulk actions */}
|
||||
@@ -422,95 +545,30 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredProjects.map((project) => (
|
||||
<tr key={project.id} className="border-b last:border-0 hover:bg-muted/30">
|
||||
{!summary.isFinalized && (
|
||||
<td className="px-3 py-2.5">
|
||||
<Checkbox
|
||||
checked={selectedIds.has(project.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const next = new Set(selectedIds)
|
||||
if (checked) next.add(project.id)
|
||||
else next.delete(project.id)
|
||||
setSelectedIds(next)
|
||||
}}
|
||||
aria-label={`Select ${project.title}`}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-3 py-2.5">
|
||||
<div className="font-medium truncate max-w-[200px]">{project.title}</div>
|
||||
{project.teamName && (
|
||||
<div className="text-xs text-muted-foreground truncate">{project.teamName}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 hidden sm:table-cell text-muted-foreground">
|
||||
{project.category === 'STARTUP' ? 'Startup' : project.category === 'BUSINESS_CONCEPT' ? 'Concept' : project.category ?? '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 hidden md:table-cell text-muted-foreground">
|
||||
{project.country ?? '-'}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn('text-xs', stateLabelColors[project.currentState] ?? '')}
|
||||
>
|
||||
{project.currentState.replace('_', ' ')}
|
||||
</Badge>
|
||||
</td>
|
||||
{summary.roundType === 'EVALUATION' && (
|
||||
<td className="px-3 py-2.5 text-center hidden lg:table-cell text-muted-foreground">
|
||||
{project.evaluationScore != null
|
||||
? `${project.evaluationScore.toFixed(1)} (#${project.rankPosition ?? '-'})`
|
||||
: '-'}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
{summary.isFinalized ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
project.proposedOutcome === 'PASSED' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700',
|
||||
)}
|
||||
{groupedProjects ? (
|
||||
groupedProjects.map((group) => (
|
||||
<React.Fragment key={group.category}>
|
||||
<tr className="bg-muted/70">
|
||||
<td
|
||||
colSpan={colCount}
|
||||
className="px-3 py-2 font-semibold text-sm tracking-wide uppercase text-muted-foreground"
|
||||
>
|
||||
{project.proposedOutcome === 'PASSED' ? 'Advanced' : 'Rejected'}
|
||||
</Badge>
|
||||
) : (
|
||||
<Select
|
||||
value={project.proposedOutcome ?? 'undecided'}
|
||||
onValueChange={(v) => {
|
||||
if (v === 'undecided') return
|
||||
updateOutcome.mutate({
|
||||
roundId,
|
||||
projectId: project.id,
|
||||
proposedOutcome: v as 'PASSED' | 'REJECTED',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'h-8 w-[130px] text-xs mx-auto',
|
||||
project.proposedOutcome === 'PASSED' && 'border-green-300 bg-green-50 text-green-700',
|
||||
project.proposedOutcome === 'REJECTED' && 'border-red-300 bg-red-50 text-red-700',
|
||||
)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="undecided" disabled>Undecided</SelectItem>
|
||||
<SelectItem value="PASSED">Pass</SelectItem>
|
||||
<SelectItem value="REJECTED">Reject</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{group.label}
|
||||
<span className="ml-2 text-xs font-normal normal-case">
|
||||
({group.projects.filter((p) => p.proposedOutcome === 'PASSED').length} pass, {group.projects.filter((p) => p.proposedOutcome === 'REJECTED').length} reject, {group.projects.length} total)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{group.projects.map((project) => renderProjectRow(project))}
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
filteredProjects.map((project) => renderProjectRow(project))
|
||||
)}
|
||||
{filteredProjects.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={summary.isFinalized ? 6 : 7}
|
||||
colSpan={colCount}
|
||||
className="px-3 py-8 text-center text-muted-foreground"
|
||||
>
|
||||
No projects match your search/filter
|
||||
|
||||
Reference in New Issue
Block a user