Add auto-pass & advance for intake rounds (no manual marking needed)
Build and Push Docker Image / build (push) Failing after 7s Details

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 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-02-16 19:09:23 +01:00
parent f731f96a0a
commit e547d2bd03
2 changed files with 102 additions and 36 deletions

View File

@ -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 && (
<button
onClick={() => passedCount > 0
onClick={() => (isSimpleAdvance || passedCount > 0)
? setAdvanceDialogOpen(true)
: toast.info('Mark projects as "Passed" first in the Projects tab')}
className={cn(
'flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left',
passedCount > 0
(isSimpleAdvance || passedCount > 0)
? 'border-l-4 border-l-emerald-500 bg-emerald-50/30'
: 'border-dashed opacity-60',
)}
>
<ArrowRight className={cn('h-5 w-5 mt-0.5 shrink-0', passedCount > 0 ? 'text-emerald-600' : 'text-muted-foreground')} />
<ArrowRight className={cn('h-5 w-5 mt-0.5 shrink-0', (isSimpleAdvance || passedCount > 0) ? 'text-emerald-600' : 'text-muted-foreground')} />
<div>
<p className="text-sm font-medium">Advance Projects</p>
<p className="text-xs text-muted-foreground mt-0.5">
{passedCount > 0
? `Move ${passedCount} passed project(s) to the next round`
: 'Mark projects as "Passed" first, then advance'}
{isSimpleAdvance
? `Advance all ${projectCount} project(s) to the next round`
: passedCount > 0
? `Move ${passedCount} passed project(s) to the next round`
: 'Mark projects as "Passed" first, then advance'}
</p>
</div>
{passedCount > 0 && (
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{passedCount}</Badge>
)}
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{isSimpleAdvance ? projectCount : passedCount}</Badge>
</button>
)}
@ -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<string, unknown>
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 (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle>Advance Projects</DialogTitle>
<DialogDescription>
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.`
}
</DialogDescription>
</DialogHeader>
@ -2692,21 +2716,50 @@ function AdvanceProjectsDialog({
</div>
)}
<div className="flex-1 overflow-y-auto space-y-4 py-2">
{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')}
</div>
{isSimpleAdvance ? (
/* Simple mode for INTAKE/SUBMISSION/MENTORING — no per-project selection needed */
<div className="py-4 space-y-3">
<div className="rounded-lg border bg-muted/30 p-4 text-center space-y-1">
<p className="text-3xl font-bold">{totalProjectCount}</p>
<p className="text-sm text-muted-foreground">projects will be advanced</p>
</div>
{pendingCount > 0 && (
<div className="rounded-md border border-blue-200 bg-blue-50 px-3 py-2">
<p className="text-xs text-blue-700">
{pendingCount} pending project{pendingCount !== 1 ? 's' : ''} will be automatically marked as passed and advanced.
{passedProjects.length > 0 && ` ${passedProjects.length} already passed.`}
</p>
</div>
)}
</div>
) : (
/* Detailed mode for jury/evaluation rounds — per-project selection */
<div className="flex-1 overflow-y-auto space-y-4 py-2">
{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')}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={handleClose}>Cancel</Button>
<Button
onClick={handleAdvance}
disabled={selected.size === 0 || advanceMutation.isPending}
>
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
</Button>
{isSimpleAdvance ? (
<Button
onClick={() => handleAdvance(true)}
disabled={totalProjectCount === 0 || advanceMutation.isPending}
>
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
Advance All {totalProjectCount} Project{totalProjectCount !== 1 ? 's' : ''}
</Button>
) : (
<Button
onClick={() => handleAdvance()}
disabled={selected.size === 0 || advanceMutation.isPending}
>
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -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,
}