fix(mentor): unbreak the mentor pipeline end-to-end
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m42s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m42s
Adding the MENTOR role from /admin/members/[id] only updated React state — the
AlertDialog "Add role" confirmation never called the server, so prod ended up
with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet".
The dialog now awaits updateUser.mutateAsync({ roles }) before closing.
Other corrections in the same area:
- DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall
modals (e.g. Add Project to Round) scroll internally instead of overflowing
past their own rounded background.
- getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both
filter mentorAssignments by droppedAt: null and require
finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what
auto-fill actually processes. The toolbar surfaces hasNoMentors /
hasNoEligible / count / all-assigned as distinct states instead of one
misleading "All eligible projects have a mentor" line.
- New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on
the Projects tab of MENTORING rounds. Lists every project with its active
mentors (multi-mentor aware), filter pills, search, finalist-confirmation
badge, and a per-row link to /admin/projects/[id]/mentor for assigning.
- Applicant team page now lists ALL active mentors (PR8 Task 7) instead of
just mentorAssignments[0].
- Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test
or VITEST=true so test runs can never emit real notifications again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -818,7 +818,7 @@ export const mentorRouter = router({
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
project: {
|
||||
mentorAssignments: { none: {} },
|
||||
mentorAssignments: { none: { droppedAt: null } },
|
||||
// Only assign mentors to projects whose team has confirmed they will
|
||||
// attend the grand finale. This skips PENDING/DECLINED/EXPIRED/SUPERSEDED
|
||||
// confirmations and any project without a confirmation row at all.
|
||||
|
||||
@@ -227,21 +227,120 @@ export const roundRouter = router({
|
||||
where: { id: input.roundId },
|
||||
select: { roundType: true, configJson: true },
|
||||
})
|
||||
if (round.roundType !== 'MENTORING') return { count: 0 }
|
||||
if (round.roundType !== 'MENTORING') {
|
||||
return { count: 0, eligibleTotal: 0, mentorPoolSize: 0 }
|
||||
}
|
||||
const config = (round.configJson ?? {}) as Record<string, unknown>
|
||||
const eligibility = (config.eligibility as string) ?? 'requested_only'
|
||||
if (eligibility === 'admin_selected') return { count: 0 }
|
||||
if (eligibility === 'admin_selected') {
|
||||
return { count: 0, eligibleTotal: 0, mentorPoolSize: 0 }
|
||||
}
|
||||
|
||||
const count = await ctx.prisma.projectRoundState.count({
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
const eligibilityWhere =
|
||||
eligibility === 'requested_only' ? { wantsMentorship: true } : {}
|
||||
|
||||
// Mirror autoAssignBulkForRound's filter exactly so the toolbar count
|
||||
// matches what the auto-fill button will actually process.
|
||||
const autoFillWhere = {
|
||||
mentorAssignments: { none: { droppedAt: null } },
|
||||
finalistConfirmation: { status: 'CONFIRMED' as const },
|
||||
...eligibilityWhere,
|
||||
}
|
||||
const [count, eligibleTotal, mentorPoolSize] = await Promise.all([
|
||||
ctx.prisma.projectRoundState.count({
|
||||
where: { roundId: input.roundId, project: autoFillWhere },
|
||||
}),
|
||||
ctx.prisma.projectRoundState.count({
|
||||
where: {
|
||||
roundId: input.roundId,
|
||||
project: {
|
||||
finalistConfirmation: { status: 'CONFIRMED' as const },
|
||||
...eligibilityWhere,
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.user.count({
|
||||
where: { roles: { has: 'MENTOR' }, status: { not: 'SUSPENDED' } },
|
||||
}),
|
||||
])
|
||||
return { count, eligibleTotal, mentorPoolSize }
|
||||
}),
|
||||
|
||||
/**
|
||||
* List projects in a MENTORING round with their (multi-)mentor assignments.
|
||||
* Drives the per-team assignment table on the round Projects tab so admins
|
||||
* can see who is assigned to whom and add/swap mentors per project.
|
||||
*/
|
||||
listMentoringProjects: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: input.roundId },
|
||||
select: { roundType: true, configJson: true },
|
||||
})
|
||||
if (round.roundType !== 'MENTORING') return { projects: [] }
|
||||
|
||||
const config = (round.configJson ?? {}) as Record<string, unknown>
|
||||
const eligibility = (config.eligibility as string) ?? 'requested_only'
|
||||
|
||||
const states = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
select: {
|
||||
state: true,
|
||||
project: {
|
||||
mentorAssignments: { none: {} },
|
||||
...(eligibility === 'requested_only' ? { wantsMentorship: true } : {}),
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
teamName: true,
|
||||
country: true,
|
||||
wantsMentorship: true,
|
||||
competitionCategory: true,
|
||||
finalistConfirmation: { select: { status: true } },
|
||||
mentorAssignments: {
|
||||
where: { droppedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
method: true,
|
||||
assignedAt: true,
|
||||
mentor: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
orderBy: { assignedAt: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ project: { title: 'asc' } }],
|
||||
})
|
||||
return { count }
|
||||
|
||||
return {
|
||||
eligibility,
|
||||
projects: states.map((s) => {
|
||||
const isEligible =
|
||||
eligibility === 'all_in_round' ||
|
||||
eligibility === 'admin_selected' ||
|
||||
s.project.wantsMentorship
|
||||
return {
|
||||
id: s.project.id,
|
||||
title: s.project.title,
|
||||
teamName: s.project.teamName,
|
||||
country: s.project.country,
|
||||
competitionCategory: s.project.competitionCategory,
|
||||
wantsMentorship: s.project.wantsMentorship,
|
||||
finalistConfirmationStatus:
|
||||
s.project.finalistConfirmation?.status ?? null,
|
||||
isEligible,
|
||||
state: s.state,
|
||||
mentors: s.project.mentorAssignments.map((a) => ({
|
||||
assignmentId: a.id,
|
||||
method: a.method,
|
||||
assignedAt: a.assignedAt,
|
||||
id: a.mentor.id,
|
||||
name: a.mentor.name,
|
||||
email: a.mentor.email,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user