Add auto-pass & advance for intake rounds (no manual marking needed)
Build and Push Docker Image / build (push) Failing after 7s
Details
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:
parent
f731f96a0a
commit
e547d2bd03
|
|
@ -326,7 +326,10 @@ export default function RoundDetailPage() {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
utils.round.getById.invalidate({ id: roundId })
|
utils.round.getById.invalidate({ id: roundId })
|
||||||
utils.roundEngine.getProjectStates.invalidate({ 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)
|
setAdvanceDialogOpen(false)
|
||||||
},
|
},
|
||||||
onError: (err) => toast.error(err.message),
|
onError: (err) => toast.error(err.message),
|
||||||
|
|
@ -378,6 +381,7 @@ export default function RoundDetailPage() {
|
||||||
const isEvaluation = round?.roundType === 'EVALUATION'
|
const isEvaluation = round?.roundType === 'EVALUATION'
|
||||||
const hasJury = ['EVALUATION', 'LIVE_FINAL', 'DELIBERATION'].includes(round?.roundType ?? '')
|
const hasJury = ['EVALUATION', 'LIVE_FINAL', 'DELIBERATION'].includes(round?.roundType ?? '')
|
||||||
const hasAwards = hasJury
|
const hasAwards = hasJury
|
||||||
|
const isSimpleAdvance = ['INTAKE', 'SUBMISSION', 'MENTORING'].includes(round?.roundType ?? '')
|
||||||
|
|
||||||
const poolLink = `/admin/projects/pool?roundId=${roundId}&competitionId=${competitionId}` as Route
|
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) */}
|
{/* Advance projects (always visible when projects exist) */}
|
||||||
{projectCount > 0 && (
|
{projectCount > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => passedCount > 0
|
onClick={() => (isSimpleAdvance || passedCount > 0)
|
||||||
? setAdvanceDialogOpen(true)
|
? setAdvanceDialogOpen(true)
|
||||||
: toast.info('Mark projects as "Passed" first in the Projects tab')}
|
: toast.info('Mark projects as "Passed" first in the Projects tab')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left',
|
'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-l-4 border-l-emerald-500 bg-emerald-50/30'
|
||||||
: 'border-dashed opacity-60',
|
: '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>
|
<div>
|
||||||
<p className="text-sm font-medium">Advance Projects</p>
|
<p className="text-sm font-medium">Advance Projects</p>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{passedCount > 0
|
{isSimpleAdvance
|
||||||
|
? `Advance all ${projectCount} project(s) to the next round`
|
||||||
|
: passedCount > 0
|
||||||
? `Move ${passedCount} passed project(s) to the next round`
|
? `Move ${passedCount} passed project(s) to the next round`
|
||||||
: 'Mark projects as "Passed" first, then advance'}
|
: 'Mark projects as "Passed" first, then advance'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{passedCount > 0 && (
|
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{isSimpleAdvance ? projectCount : passedCount}</Badge>
|
||||||
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{passedCount}</Badge>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1098,6 +1102,7 @@ export default function RoundDetailPage() {
|
||||||
open={advanceDialogOpen}
|
open={advanceDialogOpen}
|
||||||
onOpenChange={setAdvanceDialogOpen}
|
onOpenChange={setAdvanceDialogOpen}
|
||||||
roundId={roundId}
|
roundId={roundId}
|
||||||
|
roundType={round?.roundType}
|
||||||
projectStates={projectStates}
|
projectStates={projectStates}
|
||||||
config={config}
|
config={config}
|
||||||
advanceMutation={advanceMutation}
|
advanceMutation={advanceMutation}
|
||||||
|
|
@ -2502,6 +2507,7 @@ function AdvanceProjectsDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
roundId,
|
roundId,
|
||||||
|
roundType,
|
||||||
projectStates,
|
projectStates,
|
||||||
config,
|
config,
|
||||||
advanceMutation,
|
advanceMutation,
|
||||||
|
|
@ -2511,12 +2517,15 @@ function AdvanceProjectsDialog({
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
roundId: string
|
roundId: string
|
||||||
|
roundType?: string
|
||||||
projectStates: any[] | undefined
|
projectStates: any[] | undefined
|
||||||
config: Record<string, unknown>
|
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 }>
|
competitionRounds?: Array<{ id: string; name: string; sortOrder: number; roundType: string }>
|
||||||
currentSortOrder?: number
|
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
|
// Target round selector
|
||||||
const availableTargets = useMemo(() =>
|
const availableTargets = useMemo(() =>
|
||||||
(competitionRounds ?? [])
|
(competitionRounds ?? [])
|
||||||
|
|
@ -2530,9 +2539,11 @@ function AdvanceProjectsDialog({
|
||||||
if (open && !targetRoundId && availableTargets.length > 0) {
|
if (open && !targetRoundId && availableTargets.length > 0) {
|
||||||
setTargetRoundId(availableTargets[0].id)
|
setTargetRoundId(availableTargets[0].id)
|
||||||
}
|
}
|
||||||
|
const allProjects = projectStates ?? []
|
||||||
|
const pendingCount = allProjects.filter((ps: any) => ps.state === 'PENDING').length
|
||||||
const passedProjects = useMemo(() =>
|
const passedProjects = useMemo(() =>
|
||||||
(projectStates ?? []).filter((ps: any) => ps.state === 'PASSED'),
|
allProjects.filter((ps: any) => ps.state === 'PASSED'),
|
||||||
[projectStates])
|
[allProjects])
|
||||||
|
|
||||||
const startups = useMemo(() =>
|
const startups = useMemo(() =>
|
||||||
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'STARTUP'),
|
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'STARTUP'),
|
||||||
|
|
@ -2585,7 +2596,15 @@ function AdvanceProjectsDialog({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAdvance = () => {
|
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)
|
const ids = Array.from(selected)
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
advanceMutation.mutate({
|
advanceMutation.mutate({
|
||||||
|
|
@ -2593,6 +2612,7 @@ function AdvanceProjectsDialog({
|
||||||
projectIds: ids,
|
projectIds: ids,
|
||||||
...(targetRoundId ? { targetRoundId } : {}),
|
...(targetRoundId ? { targetRoundId } : {}),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
setSelected(new Set())
|
setSelected(new Set())
|
||||||
setTargetRoundId('')
|
setTargetRoundId('')
|
||||||
|
|
@ -2657,14 +2677,18 @@ function AdvanceProjectsDialog({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalProjectCount = allProjects.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleClose}>
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
|
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Advance Projects</DialogTitle>
|
<DialogTitle>Advance Projects</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Select which passed projects to advance.
|
{isSimpleAdvance
|
||||||
{selected.size} of {passedProjects.length} selected.
|
? `Move all ${totalProjectCount} projects to the next round.`
|
||||||
|
: `Select which passed projects to advance. ${selected.size} of ${passedProjects.length} selected.`
|
||||||
|
}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|
@ -2692,21 +2716,50 @@ function AdvanceProjectsDialog({
|
||||||
</div>
|
</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">
|
<div className="flex-1 overflow-y-auto space-y-4 py-2">
|
||||||
{renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')}
|
{renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')}
|
||||||
{renderCategorySection('Business Concept', concepts, conceptCap, 'bg-purple-100 text-purple-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')}
|
{other.length > 0 && renderCategorySection('Other / Uncategorized', other, 0, 'bg-gray-100 text-gray-700')}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={handleClose}>Cancel</Button>
|
<Button variant="outline" onClick={handleClose}>Cancel</Button>
|
||||||
|
{isSimpleAdvance ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAdvance}
|
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}
|
disabled={selected.size === 0 || advanceMutation.isPending}
|
||||||
>
|
>
|
||||||
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
||||||
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
|
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -243,10 +243,11 @@ export const roundRouter = router({
|
||||||
roundId: z.string(),
|
roundId: z.string(),
|
||||||
targetRoundId: z.string().optional(),
|
targetRoundId: z.string().optional(),
|
||||||
projectIds: z.array(z.string()).optional(),
|
projectIds: z.array(z.string()).optional(),
|
||||||
|
autoPassPending: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { roundId, targetRoundId, projectIds } = input
|
const { roundId, targetRoundId, projectIds, autoPassPending } = input
|
||||||
|
|
||||||
// Get current round with competition context
|
// Get current round with competition context
|
||||||
const currentRound = await ctx.prisma.round.findUniqueOrThrow({
|
const currentRound = await ctx.prisma.round.findUniqueOrThrow({
|
||||||
|
|
@ -280,6 +281,16 @@ export const roundRouter = router({
|
||||||
targetRound = nextRound
|
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
|
// Determine which projects to advance
|
||||||
let idsToAdvance: string[]
|
let idsToAdvance: string[]
|
||||||
if (projectIds && projectIds.length > 0) {
|
if (projectIds && projectIds.length > 0) {
|
||||||
|
|
@ -346,6 +357,7 @@ export const roundRouter = router({
|
||||||
toRound: targetRound.name,
|
toRound: targetRound.name,
|
||||||
targetRoundId: targetRound.id,
|
targetRoundId: targetRound.id,
|
||||||
projectCount: idsToAdvance.length,
|
projectCount: idsToAdvance.length,
|
||||||
|
autoPassedCount,
|
||||||
projectIds: idsToAdvance,
|
projectIds: idsToAdvance,
|
||||||
},
|
},
|
||||||
ipAddress: ctx.ip,
|
ipAddress: ctx.ip,
|
||||||
|
|
@ -354,6 +366,7 @@ export const roundRouter = router({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
advancedCount: idsToAdvance.length,
|
advancedCount: idsToAdvance.length,
|
||||||
|
autoPassedCount,
|
||||||
targetRoundId: targetRound.id,
|
targetRoundId: targetRound.id,
|
||||||
targetRoundName: targetRound.name,
|
targetRoundName: targetRound.name,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue