From e547d2bd0327dc8c9ac580e6cc823e90bc2d2318 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 16 Feb 2026 19:09:23 +0100 Subject: [PATCH] Add auto-pass & advance for intake rounds (no manual marking needed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For INTAKE, SUBMISSION, and MENTORING rounds, the Advance Projects dialog now shows a simplified "Advance All" flow that auto-passes all pending projects and advances them in one click. Backend accepts autoPassPending flag to bulk-set PENDING→PASSED before advancing. Jury/evaluation rounds keep the existing per-project selection workflow. Co-Authored-By: Claude Opus 4.6 --- .../(admin)/admin/rounds/[roundId]/page.tsx | 123 +++++++++++++----- src/server/routers/round.ts | 15 ++- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index f435eab..256db01 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -326,7 +326,10 @@ export default function RoundDetailPage() { onSuccess: (data) => { utils.round.getById.invalidate({ id: roundId }) utils.roundEngine.getProjectStates.invalidate({ roundId }) - toast.success(`Advanced ${data.advancedCount} project(s) to ${data.targetRoundName}`) + const msg = data.autoPassedCount + ? `Passed ${data.autoPassedCount} and advanced ${data.advancedCount} project(s) to ${data.targetRoundName}` + : `Advanced ${data.advancedCount} project(s) to ${data.targetRoundName}` + toast.success(msg) setAdvanceDialogOpen(false) }, onError: (err) => toast.error(err.message), @@ -378,6 +381,7 @@ export default function RoundDetailPage() { const isEvaluation = round?.roundType === 'EVALUATION' const hasJury = ['EVALUATION', 'LIVE_FINAL', 'DELIBERATION'].includes(round?.roundType ?? '') const hasAwards = hasJury + const isSimpleAdvance = ['INTAKE', 'SUBMISSION', 'MENTORING'].includes(round?.roundType ?? '') const poolLink = `/admin/projects/pool?roundId=${roundId}&competitionId=${competitionId}` as Route @@ -969,28 +973,28 @@ export default function RoundDetailPage() { {/* Advance projects (always visible when projects exist) */} {projectCount > 0 && ( )} @@ -1098,6 +1102,7 @@ export default function RoundDetailPage() { open={advanceDialogOpen} onOpenChange={setAdvanceDialogOpen} roundId={roundId} + roundType={round?.roundType} projectStates={projectStates} config={config} advanceMutation={advanceMutation} @@ -2502,6 +2507,7 @@ function AdvanceProjectsDialog({ open, onOpenChange, roundId, + roundType, projectStates, config, advanceMutation, @@ -2511,12 +2517,15 @@ function AdvanceProjectsDialog({ open: boolean onOpenChange: (open: boolean) => void roundId: string + roundType?: string projectStates: any[] | undefined config: Record - advanceMutation: { mutate: (input: { roundId: string; projectIds?: string[]; targetRoundId?: string }) => void; isPending: boolean } + advanceMutation: { mutate: (input: { roundId: string; projectIds?: string[]; targetRoundId?: string; autoPassPending?: boolean }) => void; isPending: boolean } competitionRounds?: Array<{ id: string; name: string; sortOrder: number; roundType: string }> currentSortOrder?: number }) { + // For non-jury rounds (INTAKE, SUBMISSION, MENTORING), offer a simpler "advance all" flow + const isSimpleAdvance = ['INTAKE', 'SUBMISSION', 'MENTORING'].includes(roundType ?? '') // Target round selector const availableTargets = useMemo(() => (competitionRounds ?? []) @@ -2530,9 +2539,11 @@ function AdvanceProjectsDialog({ if (open && !targetRoundId && availableTargets.length > 0) { setTargetRoundId(availableTargets[0].id) } + const allProjects = projectStates ?? [] + const pendingCount = allProjects.filter((ps: any) => ps.state === 'PENDING').length const passedProjects = useMemo(() => - (projectStates ?? []).filter((ps: any) => ps.state === 'PASSED'), - [projectStates]) + allProjects.filter((ps: any) => ps.state === 'PASSED'), + [allProjects]) const startups = useMemo(() => passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'STARTUP'), @@ -2585,14 +2596,23 @@ function AdvanceProjectsDialog({ }) } - const handleAdvance = () => { - const ids = Array.from(selected) - if (ids.length === 0) return - advanceMutation.mutate({ - roundId, - projectIds: ids, - ...(targetRoundId ? { targetRoundId } : {}), - }) + const handleAdvance = (autoPass?: boolean) => { + if (autoPass) { + // Auto-pass all pending then advance all + advanceMutation.mutate({ + roundId, + autoPassPending: true, + ...(targetRoundId ? { targetRoundId } : {}), + }) + } else { + const ids = Array.from(selected) + if (ids.length === 0) return + advanceMutation.mutate({ + roundId, + projectIds: ids, + ...(targetRoundId ? { targetRoundId } : {}), + }) + } onOpenChange(false) setSelected(new Set()) setTargetRoundId('') @@ -2657,14 +2677,18 @@ function AdvanceProjectsDialog({ ) } + const totalProjectCount = allProjects.length + return ( Advance Projects - Select which passed projects to advance. - {selected.size} of {passedProjects.length} selected. + {isSimpleAdvance + ? `Move all ${totalProjectCount} projects to the next round.` + : `Select which passed projects to advance. ${selected.size} of ${passedProjects.length} selected.` + } @@ -2692,21 +2716,50 @@ function AdvanceProjectsDialog({ )} -
- {renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')} - {renderCategorySection('Business Concept', concepts, conceptCap, 'bg-purple-100 text-purple-700')} - {other.length > 0 && renderCategorySection('Other / Uncategorized', other, 0, 'bg-gray-100 text-gray-700')} -
+ {isSimpleAdvance ? ( + /* Simple mode for INTAKE/SUBMISSION/MENTORING — no per-project selection needed */ +
+
+

{totalProjectCount}

+

projects will be advanced

+
+ {pendingCount > 0 && ( +
+

+ {pendingCount} pending project{pendingCount !== 1 ? 's' : ''} will be automatically marked as passed and advanced. + {passedProjects.length > 0 && ` ${passedProjects.length} already passed.`} +

+
+ )} +
+ ) : ( + /* Detailed mode for jury/evaluation rounds — per-project selection */ +
+ {renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')} + {renderCategorySection('Business Concept', concepts, conceptCap, 'bg-purple-100 text-purple-700')} + {other.length > 0 && renderCategorySection('Other / Uncategorized', other, 0, 'bg-gray-100 text-gray-700')} +
+ )} - + {isSimpleAdvance ? ( + + ) : ( + + )}
diff --git a/src/server/routers/round.ts b/src/server/routers/round.ts index ef5e890..95797c8 100644 --- a/src/server/routers/round.ts +++ b/src/server/routers/round.ts @@ -243,10 +243,11 @@ export const roundRouter = router({ roundId: z.string(), targetRoundId: z.string().optional(), projectIds: z.array(z.string()).optional(), + autoPassPending: z.boolean().optional(), }) ) .mutation(async ({ ctx, input }) => { - const { roundId, targetRoundId, projectIds } = input + const { roundId, targetRoundId, projectIds, autoPassPending } = input // Get current round with competition context const currentRound = await ctx.prisma.round.findUniqueOrThrow({ @@ -280,6 +281,16 @@ export const roundRouter = router({ targetRound = nextRound } + // Auto-pass all PENDING projects first (for intake/bulk workflows) + let autoPassedCount = 0 + if (autoPassPending) { + const result = await ctx.prisma.projectRoundState.updateMany({ + where: { roundId, state: 'PENDING' }, + data: { state: 'PASSED' }, + }) + autoPassedCount = result.count + } + // Determine which projects to advance let idsToAdvance: string[] if (projectIds && projectIds.length > 0) { @@ -346,6 +357,7 @@ export const roundRouter = router({ toRound: targetRound.name, targetRoundId: targetRound.id, projectCount: idsToAdvance.length, + autoPassedCount, projectIds: idsToAdvance, }, ipAddress: ctx.ip, @@ -354,6 +366,7 @@ export const roundRouter = router({ return { advancedCount: idsToAdvance.length, + autoPassedCount, targetRoundId: targetRound.id, targetRoundName: targetRound.name, }