Compare commits
4 Commits
7b98b64c1c
...
771f35c695
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
771f35c695 | ||
|
|
fbeec846a3 | ||
|
|
cfeef9a601 | ||
|
|
fcee8761b9 |
@@ -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
|
||||
|
||||
@@ -582,7 +586,7 @@ export default function RoundDetailPage() {
|
||||
</motion.div>
|
||||
|
||||
{/* ===== STATS BAR — Accent-bordered cards ===== */}
|
||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
||||
<div className={cn("grid gap-3 grid-cols-2", hasJury ? "sm:grid-cols-4" : "sm:grid-cols-3")}>
|
||||
{/* Projects */}
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-[#557f8c] hover:shadow-md transition-shadow">
|
||||
@@ -605,7 +609,8 @@ export default function RoundDetailPage() {
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Jury (with inline group selector) */}
|
||||
{/* Jury (with inline group selector) — only for jury-relevant rounds */}
|
||||
{hasJury && (
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-purple-500 hover:shadow-md transition-shadow">
|
||||
<CardContent className="pt-4 pb-3">
|
||||
@@ -652,9 +657,10 @@ export default function RoundDetailPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Window */}
|
||||
<AnimatedCard index={2}>
|
||||
<AnimatedCard index={hasJury ? 2 : 1}>
|
||||
<Card className="border-l-4 border-l-emerald-500 hover:shadow-md transition-shadow">
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -687,7 +693,7 @@ export default function RoundDetailPage() {
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Advancement */}
|
||||
<AnimatedCard index={3}>
|
||||
<AnimatedCard index={hasJury ? 3 : 2}>
|
||||
<Card className="border-l-4 border-l-amber-500 hover:shadow-md transition-shadow">
|
||||
<CardContent className="pt-4 pb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -967,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
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -1096,6 +1102,7 @@ export default function RoundDetailPage() {
|
||||
open={advanceDialogOpen}
|
||||
onOpenChange={setAdvanceDialogOpen}
|
||||
roundId={roundId}
|
||||
roundType={round?.roundType}
|
||||
projectStates={projectStates}
|
||||
config={config}
|
||||
advanceMutation={advanceMutation}
|
||||
@@ -2500,6 +2507,7 @@ function AdvanceProjectsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
roundId,
|
||||
roundType,
|
||||
projectStates,
|
||||
config,
|
||||
advanceMutation,
|
||||
@@ -2509,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 ?? [])
|
||||
@@ -2528,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'),
|
||||
@@ -2583,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)
|
||||
if (ids.length === 0) return
|
||||
advanceMutation.mutate({
|
||||
@@ -2591,6 +2612,7 @@ function AdvanceProjectsDialog({
|
||||
projectIds: ids,
|
||||
...(targetRoundId ? { targetRoundId } : {}),
|
||||
})
|
||||
}
|
||||
onOpenChange(false)
|
||||
setSelected(new Set())
|
||||
setTargetRoundId('')
|
||||
@@ -2655,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>
|
||||
|
||||
@@ -2690,21 +2716,50 @@ function AdvanceProjectsDialog({
|
||||
</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>
|
||||
{isSimpleAdvance ? (
|
||||
<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}
|
||||
>
|
||||
{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>
|
||||
|
||||
@@ -74,10 +74,22 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
|
||||
description: true,
|
||||
tags: true,
|
||||
teamName: true,
|
||||
projectTags: {
|
||||
select: { tag: { select: { name: true } }, confidence: true },
|
||||
},
|
||||
_count: { select: { assignments: { where: { roundId } } } },
|
||||
},
|
||||
})
|
||||
|
||||
// Enrich projects with tag confidence data for AI matching
|
||||
const projectsWithConfidence = projects.map((p) => ({
|
||||
...p,
|
||||
tagConfidences: p.projectTags.map((pt) => ({
|
||||
name: pt.tag.name,
|
||||
confidence: pt.confidence,
|
||||
})),
|
||||
}))
|
||||
|
||||
const existingAssignments = await prisma.assignment.findMany({
|
||||
where: { roundId },
|
||||
select: { userId: true, projectId: true },
|
||||
@@ -124,7 +136,7 @@ async function runAIAssignmentJob(jobId: string, roundId: string, userId: string
|
||||
|
||||
const result = await generateAIAssignments(
|
||||
jurors,
|
||||
projects,
|
||||
projectsWithConfidence,
|
||||
constraints,
|
||||
userId,
|
||||
roundId,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -263,4 +263,41 @@ export const roundEngineRouter = router({
|
||||
|
||||
return { success: true, removedCount: deleted.count }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Retroactive document check: auto-PASS any PENDING/IN_PROGRESS projects
|
||||
* that already have all required documents uploaded for this round.
|
||||
* Useful for rounds activated before the auto-transition feature was deployed.
|
||||
*/
|
||||
checkDocumentCompletion: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { batchCheckRequirementsAndTransition } = await import('../services/round-engine')
|
||||
|
||||
const projectStates = await ctx.prisma.projectRoundState.findMany({
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
state: { in: ['PENDING', 'IN_PROGRESS'] },
|
||||
},
|
||||
select: { projectId: true },
|
||||
})
|
||||
|
||||
if (projectStates.length === 0) {
|
||||
return { transitionedCount: 0, checkedCount: 0, projectIds: [] }
|
||||
}
|
||||
|
||||
const projectIds = projectStates.map((ps: { projectId: string }) => ps.projectId)
|
||||
const result = await batchCheckRequirementsAndTransition(
|
||||
input.roundId,
|
||||
projectIds,
|
||||
ctx.user.id,
|
||||
ctx.prisma,
|
||||
)
|
||||
|
||||
return {
|
||||
transitionedCount: result.transitionedCount,
|
||||
checkedCount: projectIds.length,
|
||||
projectIds: result.projectIds,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ const ASSIGNMENT_SYSTEM_PROMPT = `You are an expert jury assignment optimizer fo
|
||||
Match jurors to projects based on expertise alignment, workload balance, and coverage requirements.
|
||||
|
||||
## Matching Criteria (Weighted)
|
||||
- Expertise Match (50%): How well juror tags/expertise align with project topics
|
||||
- Expertise Match (50%): How well juror tags/expertise align with project topics. Project tags include a confidence score (0-1) — weight higher-confidence tags more heavily as they are more reliably assigned. A tag with confidence 0.9 is a strong signal; one with 0.5 is uncertain.
|
||||
- Workload Balance (30%): Distribute assignments evenly; prefer jurors below capacity
|
||||
- Minimum Target (20%): Prioritize jurors who haven't reached their minimum assignment count
|
||||
|
||||
@@ -99,6 +99,7 @@ interface ProjectForAssignment {
|
||||
title: string
|
||||
description?: string | null
|
||||
tags: string[]
|
||||
tagConfidences?: Array<{ name: string; confidence: number }>
|
||||
teamName?: string | null
|
||||
_count?: {
|
||||
assignments: number
|
||||
@@ -539,7 +540,7 @@ export function generateFallbackAssignments(
|
||||
|
||||
return {
|
||||
juror,
|
||||
score: calculateExpertiseScore(juror.expertiseTags, project.tags),
|
||||
score: calculateExpertiseScore(juror.expertiseTags, project.tags, project.tagConfidences),
|
||||
loadScore: calculateLoadScore(currentLoad, maxLoad),
|
||||
underMinBonus: calculateUnderMinBonus(currentLoad, minTarget),
|
||||
}
|
||||
@@ -586,24 +587,44 @@ export function generateFallbackAssignments(
|
||||
|
||||
/**
|
||||
* Calculate expertise match score based on tag overlap
|
||||
* When tagConfidences are available, weights matches by confidence
|
||||
*/
|
||||
function calculateExpertiseScore(
|
||||
jurorTags: string[],
|
||||
projectTags: string[]
|
||||
projectTags: string[],
|
||||
tagConfidences?: Array<{ name: string; confidence: number }>
|
||||
): number {
|
||||
if (jurorTags.length === 0 || projectTags.length === 0) {
|
||||
return 0.5 // Neutral score if no tags
|
||||
}
|
||||
|
||||
const jurorTagsLower = new Set(jurorTags.map((t) => t.toLowerCase()))
|
||||
|
||||
// If we have confidence data, use weighted scoring
|
||||
if (tagConfidences && tagConfidences.length > 0) {
|
||||
let weightedMatches = 0
|
||||
let totalWeight = 0
|
||||
|
||||
for (const tc of tagConfidences) {
|
||||
totalWeight += tc.confidence
|
||||
if (jurorTagsLower.has(tc.name.toLowerCase())) {
|
||||
weightedMatches += tc.confidence
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight === 0) return 0.5
|
||||
|
||||
const weightedRatio = weightedMatches / totalWeight
|
||||
const hasExpertise = weightedMatches > 0 ? 0.2 : 0
|
||||
return Math.min(1, weightedRatio * 0.8 + hasExpertise)
|
||||
}
|
||||
|
||||
// Fallback: unweighted matching using flat tags
|
||||
const matchingTags = projectTags.filter((t) =>
|
||||
jurorTagsLower.has(t.toLowerCase())
|
||||
)
|
||||
|
||||
// Score based on percentage of project tags matched
|
||||
const matchRatio = matchingTags.length / projectTags.length
|
||||
|
||||
// Boost for having expertise, even if not all match
|
||||
const hasExpertise = matchingTags.length > 0 ? 0.2 : 0
|
||||
|
||||
return Math.min(1, matchRatio * 0.8 + hasExpertise)
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface AnonymizedProject {
|
||||
anonymousId: string
|
||||
title: string
|
||||
description: string | null
|
||||
tags: string[]
|
||||
tags: Array<{ name: string; confidence: number }>
|
||||
teamName: string | null
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ interface ProjectInput {
|
||||
title: string
|
||||
description?: string | null
|
||||
tags: string[]
|
||||
tagConfidences?: Array<{ name: string; confidence: number }>
|
||||
teamName?: string | null
|
||||
}
|
||||
|
||||
@@ -253,7 +254,9 @@ export function anonymizeForAI(
|
||||
description: project.description
|
||||
? truncateAndSanitize(project.description, DESCRIPTION_LIMITS.ASSIGNMENT)
|
||||
: null,
|
||||
tags: project.tags,
|
||||
tags: project.tagConfidences && project.tagConfidences.length > 0
|
||||
? project.tagConfidences
|
||||
: project.tags.map((t) => ({ name: t, confidence: 1.0 })),
|
||||
teamName: project.teamName ? `Team ${index + 1}` : null,
|
||||
}
|
||||
}
|
||||
@@ -524,7 +527,7 @@ export function validateAnonymization(data: AnonymizationResult): boolean {
|
||||
if (!checkText(project.title)) return false
|
||||
if (!checkText(project.description)) return false
|
||||
for (const tag of project.tags) {
|
||||
if (!checkText(tag)) return false
|
||||
if (!checkText(typeof tag === 'string' ? tag : tag.name)) return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,24 @@ export async function activateRound(
|
||||
detailsJson: { name: round.name, roundType: round.roundType },
|
||||
})
|
||||
|
||||
// Retroactive check: auto-PASS any projects that already have all required docs uploaded
|
||||
// Non-fatal — runs after activation so it never blocks the transition
|
||||
try {
|
||||
const projectStates = await prisma.projectRoundState.findMany({
|
||||
where: { roundId, state: { in: ['PENDING', 'IN_PROGRESS'] } },
|
||||
select: { projectId: true },
|
||||
})
|
||||
if (projectStates.length > 0) {
|
||||
const projectIds = projectStates.map((ps: { projectId: string }) => ps.projectId)
|
||||
const result = await batchCheckRequirementsAndTransition(roundId, projectIds, actorId, prisma)
|
||||
if (result.transitionedCount > 0) {
|
||||
console.log(`[RoundEngine] On activation: auto-passed ${result.transitionedCount} projects with complete documents`)
|
||||
}
|
||||
}
|
||||
} catch (retroError) {
|
||||
console.error('[RoundEngine] Retroactive document check failed (non-fatal):', retroError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
round: { id: updated.id, status: updated.status },
|
||||
@@ -429,6 +447,23 @@ export async function reopenRound(
|
||||
},
|
||||
})
|
||||
|
||||
// Retroactive check: auto-PASS any projects that already have all required docs
|
||||
try {
|
||||
const projectStates = await prisma.projectRoundState.findMany({
|
||||
where: { roundId, state: { in: ['PENDING', 'IN_PROGRESS'] } },
|
||||
select: { projectId: true },
|
||||
})
|
||||
if (projectStates.length > 0) {
|
||||
const projectIds = projectStates.map((ps: { projectId: string }) => ps.projectId)
|
||||
const batchResult = await batchCheckRequirementsAndTransition(roundId, projectIds, actorId, prisma)
|
||||
if (batchResult.transitionedCount > 0) {
|
||||
console.log(`[RoundEngine] On reopen: auto-passed ${batchResult.transitionedCount} projects with complete documents`)
|
||||
}
|
||||
}
|
||||
} catch (retroError) {
|
||||
console.error('[RoundEngine] Retroactive document check on reopen failed (non-fatal):', retroError)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
round: { id: result.updated.id, status: result.updated.status },
|
||||
|
||||
Reference in New Issue
Block a user