Consolidated round management, AI filtering enhancements, MinIO storage restructure
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
- Fix STAGE_ACTIVE bug in assignment router (now ROUND_ACTIVE)
- Add evaluation form CRUD (getForm + upsertForm endpoints)
- Add advanceProjects mutation for manual project advancement
- Rewrite round detail page: 7-tab consolidated interface
- Add filtering rules UI with full CRUD (field-based, document check, AI screening)
- Add pageCount field to ProjectFile for document page limit filtering
- Enhance AI filtering: per-file page limits, category/region-aware guidelines
- Restructure MinIO paths: {ProjectName}/{RoundName}/{timestamp}-{file}
- Update dashboard and pool page links from /admin/competitions to /admin/rounds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -227,6 +227,137 @@ export const roundRouter = router({
|
||||
return existing
|
||||
}),
|
||||
|
||||
// =========================================================================
|
||||
// Project Advancement (Manual Only)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Advance PASSED projects from one round to the next.
|
||||
* This is ALWAYS manual — no auto-advancement after AI filtering.
|
||||
* Admin must explicitly trigger this after reviewing results.
|
||||
*/
|
||||
advanceProjects: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
roundId: z.string(),
|
||||
targetRoundId: z.string().optional(),
|
||||
projectIds: z.array(z.string()).optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { roundId, targetRoundId, projectIds } = input
|
||||
|
||||
// Get current round with competition context
|
||||
const currentRound = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: roundId },
|
||||
select: { id: true, name: true, competitionId: true, sortOrder: true },
|
||||
})
|
||||
|
||||
// Determine target round
|
||||
let targetRound: { id: string; name: string }
|
||||
if (targetRoundId) {
|
||||
targetRound = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: targetRoundId },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
} else {
|
||||
// Find next round in same competition by sortOrder
|
||||
const nextRound = await ctx.prisma.round.findFirst({
|
||||
where: {
|
||||
competitionId: currentRound.competitionId,
|
||||
sortOrder: { gt: currentRound.sortOrder },
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
if (!nextRound) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'No subsequent round exists in this competition. Create the next round first.',
|
||||
})
|
||||
}
|
||||
targetRound = nextRound
|
||||
}
|
||||
|
||||
// Determine which projects to advance
|
||||
let idsToAdvance: string[]
|
||||
if (projectIds && projectIds.length > 0) {
|
||||
idsToAdvance = projectIds
|
||||
} else {
|
||||
// Default: all PASSED projects in current round
|
||||
const passedStates = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { roundId, state: 'PASSED' },
|
||||
select: { projectId: true },
|
||||
})
|
||||
idsToAdvance = passedStates.map((s) => s.projectId)
|
||||
}
|
||||
|
||||
if (idsToAdvance.length === 0) {
|
||||
return { advancedCount: 0, targetRoundId: targetRound.id, targetRoundName: targetRound.name }
|
||||
}
|
||||
|
||||
// Transaction: create entries in target round + mark current as COMPLETED
|
||||
await ctx.prisma.$transaction(async (tx) => {
|
||||
// Create ProjectRoundState in target round
|
||||
await tx.projectRoundState.createMany({
|
||||
data: idsToAdvance.map((projectId) => ({
|
||||
projectId,
|
||||
roundId: targetRound.id,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
|
||||
// Mark current round states as COMPLETED
|
||||
await tx.projectRoundState.updateMany({
|
||||
where: {
|
||||
roundId,
|
||||
projectId: { in: idsToAdvance },
|
||||
state: 'PASSED',
|
||||
},
|
||||
data: { state: 'COMPLETED' },
|
||||
})
|
||||
|
||||
// Update project status to ASSIGNED
|
||||
await tx.project.updateMany({
|
||||
where: { id: { in: idsToAdvance } },
|
||||
data: { status: 'ASSIGNED' },
|
||||
})
|
||||
|
||||
// Status history
|
||||
await tx.projectStatusHistory.createMany({
|
||||
data: idsToAdvance.map((projectId) => ({
|
||||
projectId,
|
||||
status: 'ASSIGNED',
|
||||
changedBy: ctx.user?.id,
|
||||
})),
|
||||
})
|
||||
})
|
||||
|
||||
// Audit
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'ADVANCE_PROJECTS',
|
||||
entityType: 'Round',
|
||||
entityId: roundId,
|
||||
detailsJson: {
|
||||
fromRound: currentRound.name,
|
||||
toRound: targetRound.name,
|
||||
targetRoundId: targetRound.id,
|
||||
projectCount: idsToAdvance.length,
|
||||
projectIds: idsToAdvance,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return {
|
||||
advancedCount: idsToAdvance.length,
|
||||
targetRoundId: targetRound.id,
|
||||
targetRoundName: targetRound.name,
|
||||
}
|
||||
}),
|
||||
|
||||
// =========================================================================
|
||||
// Submission Window Management
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user