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) => {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue