feat: finalization tab respects ranking overrides, grouped by category
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:
2026-03-03 22:10:04 +01:00
parent 43801340f8
commit 050836d522
4 changed files with 182 additions and 363 deletions

View File

@@ -111,14 +111,15 @@ export async function processRoundClose(
let processed = 0
// Pre-compute pass set for EVALUATION rounds using ranking scores + config
// Pre-compute pass set for EVALUATION rounds using ranking scores + config.
// Respects admin drag-reorder overrides stored in reordersJson.
let evaluationPassSet: Set<string> | null = null
if ((round.roundType as RoundType) === 'EVALUATION') {
evaluationPassSet = new Set<string>()
const snapshot = await prisma.rankingSnapshot.findFirst({
where: { roundId },
orderBy: { createdAt: 'desc' as const },
select: { startupRankingJson: true, conceptRankingJson: true },
select: { startupRankingJson: true, conceptRankingJson: true, reordersJson: true },
})
if (snapshot) {
const config = (round.configJson as Record<string, unknown>) ?? {}
@@ -131,21 +132,40 @@ export async function processRoundClose(
const startupRanked = (snapshot.startupRankingJson ?? []) as RankEntry[]
const conceptRanked = (snapshot.conceptRankingJson ?? []) as RankEntry[]
// Apply admin drag-reorder overrides (reordersJson is append-only, latest per category wins)
type ReorderEvent = { category: 'STARTUP' | 'BUSINESS_CONCEPT'; orderedProjectIds: string[] }
const reorders = (snapshot.reordersJson as ReorderEvent[] | null) ?? []
const latestStartupReorder = [...reorders].reverse().find((r) => r.category === 'STARTUP')
const latestConceptReorder = [...reorders].reverse().find((r) => r.category === 'BUSINESS_CONCEPT')
// Build effective order: if admin reordered, use that; otherwise use computed rank order
const effectiveStartup = latestStartupReorder
? latestStartupReorder.orderedProjectIds
: [...startupRanked].sort((a, b) => a.rank - b.rank).map((r) => r.projectId)
const effectiveConcept = latestConceptReorder
? latestConceptReorder.orderedProjectIds
: [...conceptRanked].sort((a, b) => a.rank - b.rank).map((r) => r.projectId)
// Build score lookup for threshold mode
const scoreMap = new Map<string, number>()
for (const r of [...startupRanked, ...conceptRanked]) {
if (r.avgGlobalScore != null) scoreMap.set(r.projectId, r.avgGlobalScore)
}
if (advanceMode === 'threshold') {
for (const r of [...startupRanked, ...conceptRanked]) {
if (r.avgGlobalScore != null && r.avgGlobalScore >= advanceScoreThreshold) {
evaluationPassSet.add(r.projectId)
for (const id of [...effectiveStartup, ...effectiveConcept]) {
const score = scoreMap.get(id)
if (score != null && score >= advanceScoreThreshold) {
evaluationPassSet.add(id)
}
}
} else {
// 'count' mode — top N per category by rank
const sortedStartup = [...startupRanked].sort((a, b) => a.rank - b.rank)
const sortedConcept = [...conceptRanked].sort((a, b) => a.rank - b.rank)
for (let i = 0; i < Math.min(startupAdvanceCount, sortedStartup.length); i++) {
evaluationPassSet.add(sortedStartup[i].projectId)
// 'count' mode — top N per category using effective (possibly reordered) order
for (let i = 0; i < Math.min(startupAdvanceCount, effectiveStartup.length); i++) {
evaluationPassSet.add(effectiveStartup[i])
}
for (let i = 0; i < Math.min(conceptAdvanceCount, sortedConcept.length); i++) {
evaluationPassSet.add(sortedConcept[i].projectId)
for (let i = 0; i < Math.min(conceptAdvanceCount, effectiveConcept.length); i++) {
evaluationPassSet.add(effectiveConcept[i])
}
}
}