Fix rounds management bugs and invitation flow

- Fix rounds list showing 0 projects by adding _count to program.list query
- Fix round reordering by using correct cache invalidation params
- Fix finalizeResults to auto-advance passed projects to next round
- Fix member list not updating after add/remove by invalidating user.list
- Fix invitation link error page by correcting path from /auth-error to /error
- Add /apply, /verify, /error to public paths in auth config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 22:15:22 +01:00
parent 0277768ed7
commit 03c031a8b6
7 changed files with 159 additions and 73 deletions

View File

@@ -714,11 +714,28 @@ export const filteringRouter = router({
/**
* Finalize filtering results — apply outcomes to project statuses
* PASSED → keep in pool, FILTERED_OUT → set aside (NOT deleted)
* PASSED → mark as ELIGIBLE and advance to next round
* FILTERED_OUT → mark as REJECTED (data preserved)
*/
finalizeResults: adminProcedure
.input(z.object({ roundId: z.string() }))
.mutation(async ({ ctx, input }) => {
// Get current round to find the next one
const currentRound = await ctx.prisma.round.findUniqueOrThrow({
where: { id: input.roundId },
select: { id: true, programId: true, sortOrder: true, name: true },
})
// Find the next round by sortOrder
const nextRound = await ctx.prisma.round.findFirst({
where: {
programId: currentRound.programId,
sortOrder: { gt: currentRound.sortOrder },
},
orderBy: { sortOrder: 'asc' },
select: { id: true, name: true },
})
const results = await ctx.prisma.filteringResult.findMany({
where: { roundId: input.roundId },
})
@@ -732,27 +749,45 @@ export const filteringRouter = router({
.filter((r) => (r.finalOutcome || r.outcome) === 'PASSED')
.map((r) => r.projectId)
// Update RoundProject statuses
await ctx.prisma.$transaction([
// Filtered out projects get REJECTED status (data preserved)
...(filteredOutIds.length > 0
? [
ctx.prisma.roundProject.updateMany({
where: { roundId: input.roundId, projectId: { in: filteredOutIds } },
data: { status: 'REJECTED' },
}),
]
: []),
// Passed projects get ELIGIBLE status
...(passedIds.length > 0
? [
ctx.prisma.roundProject.updateMany({
where: { roundId: input.roundId, projectId: { in: passedIds } },
data: { status: 'ELIGIBLE' },
}),
]
: []),
])
// Build transaction operations
const operations: Prisma.PrismaPromise<unknown>[] = []
// Filtered out projects get REJECTED status (data preserved)
if (filteredOutIds.length > 0) {
operations.push(
ctx.prisma.roundProject.updateMany({
where: { roundId: input.roundId, projectId: { in: filteredOutIds } },
data: { status: 'REJECTED' },
})
)
}
// Passed projects get ELIGIBLE status
if (passedIds.length > 0) {
operations.push(
ctx.prisma.roundProject.updateMany({
where: { roundId: input.roundId, projectId: { in: passedIds } },
data: { status: 'ELIGIBLE' },
})
)
// If there's a next round, advance passed projects to it
if (nextRound) {
operations.push(
ctx.prisma.roundProject.createMany({
data: passedIds.map((projectId) => ({
roundId: nextRound.id,
projectId,
status: 'SUBMITTED' as const,
})),
skipDuplicates: true,
})
)
}
}
// Execute all operations in a transaction
await ctx.prisma.$transaction(operations)
await logAudit({
userId: ctx.user.id,
@@ -763,10 +798,16 @@ export const filteringRouter = router({
action: 'FINALIZE_FILTERING',
passed: passedIds.length,
filteredOut: filteredOutIds.length,
advancedToRound: nextRound?.name || null,
},
})
return { passed: passedIds.length, filteredOut: filteredOutIds.length }
return {
passed: passedIds.length,
filteredOut: filteredOutIds.length,
advancedToRoundId: nextRound?.id || null,
advancedToRoundName: nextRound?.name || null,
}
}),
/**

View File

@@ -21,7 +21,12 @@ export const programRouter = router({
select: { rounds: true },
},
rounds: input?.includeRounds ? {
orderBy: { createdAt: 'asc' },
orderBy: { sortOrder: 'asc' },
include: {
_count: {
select: { roundProjects: true, assignments: true },
},
},
} : false,
},
})