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:
@@ -33,14 +33,6 @@ import {
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from '@/components/ui/sheet'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
@@ -57,7 +49,6 @@ import {
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Sparkles,
|
||||
Trophy,
|
||||
ExternalLink,
|
||||
ChevronDown,
|
||||
Settings2,
|
||||
@@ -251,14 +242,6 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
const initialized = useRef(false)
|
||||
const pendingReorderCount = useRef(0)
|
||||
|
||||
// ─── Advance dialog state ─────────────────────────────────────────────────
|
||||
const [advanceDialogOpen, setAdvanceDialogOpen] = useState(false)
|
||||
const [advanceMode, setAdvanceMode] = useState<'top_n' | 'threshold'>('top_n')
|
||||
const [topNStartup, setTopNStartup] = useState(3)
|
||||
const [topNConceptual, setTopNConceptual] = useState(3)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(5)
|
||||
const [includeReject, setIncludeReject] = useState(false)
|
||||
|
||||
// ─── Export state ──────────────────────────────────────────────────────────
|
||||
const [exportLoading, setExportLoading] = useState(false)
|
||||
|
||||
@@ -349,28 +332,6 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
},
|
||||
})
|
||||
|
||||
const advanceMutation = trpc.round.advanceProjects.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Advanced ${data.advancedCount} project(s) to ${data.targetRoundName}`)
|
||||
void utils.roundEngine.getProjectStates.invalidate({ roundId })
|
||||
setAdvanceDialogOpen(false)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const batchRejectMutation = trpc.roundEngine.batchTransition.useMutation({
|
||||
onSuccess: (data) => {
|
||||
// MEMORY.md: use .length, not direct value comparison
|
||||
toast.success(`Rejected ${data.succeeded.length} project(s)`)
|
||||
if (data.failed.length > 0) {
|
||||
toast.warning(`${data.failed.length} project(s) could not be rejected`)
|
||||
}
|
||||
void utils.roundEngine.getProjectStates.invalidate({ roundId })
|
||||
setAdvanceDialogOpen(false)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
// ─── evalConfig (advancement counts from round config) ────────────────────
|
||||
const evalConfig = useMemo(() => {
|
||||
if (!roundData?.configJson) return null
|
||||
@@ -518,14 +479,6 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
// Derive ranking mode from criteria text
|
||||
const isFormulaMode = !localCriteriaText.trim()
|
||||
|
||||
// ─── sync advance dialog defaults from config ────────────────────────────
|
||||
useEffect(() => {
|
||||
if (evalConfig) {
|
||||
if (evalConfig.startupAdvanceCount > 0) setTopNStartup(evalConfig.startupAdvanceCount)
|
||||
if (evalConfig.conceptAdvanceCount > 0) setTopNConceptual(evalConfig.conceptAdvanceCount)
|
||||
}
|
||||
}, [evalConfig])
|
||||
|
||||
// ─── handleDragEnd ────────────────────────────────────────────────────────
|
||||
function handleDragEnd(category: 'STARTUP' | 'BUSINESS_CONCEPT', event: DragEndEvent) {
|
||||
const { active, over } = event
|
||||
@@ -546,50 +499,6 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Compute threshold-based project IDs ──────────────────────────────────
|
||||
const thresholdAdvanceIds = useMemo(() => {
|
||||
if (advanceMode !== 'threshold') return { ids: [] as string[], startupCount: 0, conceptCount: 0 }
|
||||
const ids: string[] = []
|
||||
let startupCount = 0
|
||||
let conceptCount = 0
|
||||
for (const cat of ['STARTUP', 'BUSINESS_CONCEPT'] as const) {
|
||||
for (const projectId of localOrder[cat]) {
|
||||
const entry = rankingMap.get(projectId)
|
||||
if (entry?.avgGlobalScore != null && entry.avgGlobalScore >= scoreThreshold) {
|
||||
ids.push(projectId)
|
||||
if (cat === 'STARTUP') startupCount++
|
||||
else conceptCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ids, startupCount, conceptCount }
|
||||
}, [advanceMode, scoreThreshold, localOrder, rankingMap])
|
||||
|
||||
// ─── handleAdvance ────────────────────────────────────────────────────────
|
||||
function handleAdvance() {
|
||||
let advanceIds: string[]
|
||||
if (advanceMode === 'threshold') {
|
||||
advanceIds = thresholdAdvanceIds.ids
|
||||
} else {
|
||||
advanceIds = [
|
||||
...localOrder.STARTUP.slice(0, topNStartup),
|
||||
...localOrder.BUSINESS_CONCEPT.slice(0, topNConceptual),
|
||||
]
|
||||
}
|
||||
const advanceSet = new Set(advanceIds)
|
||||
|
||||
advanceMutation.mutate({ roundId, projectIds: advanceIds })
|
||||
|
||||
if (includeReject) {
|
||||
const rejectIds = [...localOrder.STARTUP, ...localOrder.BUSINESS_CONCEPT].filter(
|
||||
(id) => !advanceSet.has(id),
|
||||
)
|
||||
if (rejectIds.length > 0) {
|
||||
batchRejectMutation.mutate({ projectIds: rejectIds, roundId, newState: 'REJECTED' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── handleExport ──────────────────────────────────────────────────────────
|
||||
async function handleExportScores() {
|
||||
setExportLoading(true)
|
||||
@@ -758,18 +667,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={saveReorderMutation.isPending || advanceMutation.isPending || !latestSnapshotId}
|
||||
onClick={() => setAdvanceDialogOpen(true)}
|
||||
className="bg-[#053d57] hover:bg-[#053d57]/90"
|
||||
>
|
||||
{advanceMutation.isPending ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Advancing...</>
|
||||
) : (
|
||||
<><Trophy className="h-4 w-4 mr-2" /> Advance Top N</>
|
||||
)}
|
||||
</Button>
|
||||
{/* Advance Top N removed — use Finalization tab instead */}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -1039,164 +937,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Advance dialog */}
|
||||
<Dialog open={advanceDialogOpen} onOpenChange={setAdvanceDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Advance Projects</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose how to select which projects advance to the next round.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Mode toggle */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={advanceMode === 'top_n' ? 'default' : 'outline'}
|
||||
onClick={() => setAdvanceMode('top_n')}
|
||||
className={advanceMode === 'top_n' ? 'bg-[#053d57] hover:bg-[#053d57]/90' : ''}
|
||||
>
|
||||
Top N per category
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={advanceMode === 'threshold' ? 'default' : 'outline'}
|
||||
onClick={() => setAdvanceMode('threshold')}
|
||||
className={advanceMode === 'threshold' ? 'bg-[#053d57] hover:bg-[#053d57]/90' : ''}
|
||||
>
|
||||
Score threshold
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{advanceMode === 'top_n' ? (
|
||||
<>
|
||||
{/* Top N for STARTUP */}
|
||||
{localOrder.STARTUP.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-40 text-sm">Startups to advance</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={localOrder.STARTUP.length}
|
||||
value={topNStartup}
|
||||
onChange={(e) =>
|
||||
setTopNStartup(
|
||||
Math.max(0, Math.min(localOrder.STARTUP.length, parseInt(e.target.value) || 0)),
|
||||
)
|
||||
}
|
||||
className="w-24"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">of {localOrder.STARTUP.length}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top N for BUSINESS_CONCEPT */}
|
||||
{localOrder.BUSINESS_CONCEPT.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-40 text-sm">Concepts to advance</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={localOrder.BUSINESS_CONCEPT.length}
|
||||
value={topNConceptual}
|
||||
onChange={(e) =>
|
||||
setTopNConceptual(
|
||||
Math.max(0, Math.min(localOrder.BUSINESS_CONCEPT.length, parseInt(e.target.value) || 0)),
|
||||
)
|
||||
}
|
||||
className="w-24"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">of {localOrder.BUSINESS_CONCEPT.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="w-40 text-sm">Minimum avg score</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
step={0.1}
|
||||
value={scoreThreshold}
|
||||
onChange={(e) => setScoreThreshold(Math.max(0, Math.min(10, parseFloat(e.target.value) || 5)))}
|
||||
className="w-24"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">out of 10</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
All projects with an average global score at or above this threshold will advance, regardless of category.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Optional: also batch-reject non-advanced */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="includeReject"
|
||||
checked={includeReject}
|
||||
onChange={(e) => setIncludeReject(e.target.checked)}
|
||||
className="h-4 w-4 accent-[#de0f1e]"
|
||||
/>
|
||||
<Label htmlFor="includeReject" className="text-sm cursor-pointer">
|
||||
Also batch-reject non-advanced projects
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{(() => {
|
||||
const advCount = advanceMode === 'top_n'
|
||||
? topNStartup + topNConceptual
|
||||
: thresholdAdvanceIds.ids.length
|
||||
const totalProjects = localOrder.STARTUP.length + localOrder.BUSINESS_CONCEPT.length
|
||||
return (
|
||||
<div className="text-xs text-muted-foreground bg-muted/50 rounded-md p-3">
|
||||
<p>
|
||||
Advancing: {advCount} project{advCount !== 1 ? 's' : ''}
|
||||
{advanceMode === 'threshold' && (
|
||||
<> ({thresholdAdvanceIds.startupCount} startups, {thresholdAdvanceIds.conceptCount} concepts)</>
|
||||
)}
|
||||
</p>
|
||||
{includeReject && (
|
||||
<p>Rejecting: {totalProjects - advCount} project{totalProjects - advCount !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setAdvanceDialogOpen(false)}
|
||||
disabled={advanceMutation.isPending || batchRejectMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdvance}
|
||||
disabled={
|
||||
advanceMutation.isPending ||
|
||||
batchRejectMutation.isPending ||
|
||||
(advanceMode === 'top_n' ? topNStartup + topNConceptual === 0 : thresholdAdvanceIds.ids.length === 0)
|
||||
}
|
||||
className="bg-[#053d57] hover:bg-[#053d57]/90"
|
||||
>
|
||||
{advanceMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Advancing...
|
||||
</>
|
||||
) : (
|
||||
`Advance ${advanceMode === 'top_n' ? topNStartup + topNConceptual : thresholdAdvanceIds.ids.length} Project${(advanceMode === 'top_n' ? topNStartup + topNConceptual : thresholdAdvanceIds.ids.length) !== 1 ? 's' : ''}`
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Advance dialog removed — use Finalization tab */}
|
||||
|
||||
{/* Side panel Sheet */}
|
||||
<Sheet
|
||||
|
||||
Reference in New Issue
Block a user