diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
index b444a35..e3da6d1 100644
--- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx
+++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
@@ -1265,17 +1265,32 @@ export default function RoundDetailPage() {
Project Management
-
-
)
}
diff --git a/src/components/admin/round/project-states-table.tsx b/src/components/admin/round/project-states-table.tsx
index b80ed64..dc50087 100644
--- a/src/components/admin/round/project-states-table.tsx
+++ b/src/components/admin/round/project-states-table.tsx
@@ -785,7 +785,7 @@ function QuickAddDialog({
* Create New: form to create a project and assign it directly to the round.
* From Pool: search existing projects not yet in this round and assign them.
*/
-function AddProjectDialog({
+export function AddProjectDialog({
open,
onOpenChange,
roundId,
diff --git a/src/server/routers/mentor.ts b/src/server/routers/mentor.ts
index 3af18b8..bc09876 100644
--- a/src/server/routers/mentor.ts
+++ b/src/server/routers/mentor.ts
@@ -370,6 +370,25 @@ export const mentorRouter = router({
where: { id: input.projectId },
})
+ // Gate: the project MUST be in a MENTORING round (any status, including
+ // DRAFT, ACTIVE, or CLOSED). We do not allow mentor assignment for
+ // projects that aren't part of a mentoring round — those should be
+ // added to a mentoring round first.
+ const inMentoringRound = await ctx.prisma.projectRoundState.findFirst({
+ where: {
+ projectId: input.projectId,
+ round: { roundType: 'MENTORING' },
+ },
+ select: { id: true },
+ })
+ if (!inMentoringRound) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message:
+ 'This project is not in a mentoring round. Add it to a mentoring round first, then assign mentors.',
+ })
+ }
+
// Verify mentor exists
const mentor = await ctx.prisma.user.findUniqueOrThrow({
where: { id: input.mentorId },
@@ -704,7 +723,13 @@ export const mentorRouter = router({
}
const projects = await ctx.prisma.project.findMany({
- where: { id: { in: input.projectIds } },
+ where: {
+ id: { in: input.projectIds },
+ // Gate: only projects that are in some MENTORING round (any status)
+ projectRoundStates: {
+ some: { round: { roundType: 'MENTORING' } },
+ },
+ },
select: {
id: true,
title: true,
@@ -718,6 +743,16 @@ export const mentorRouter = router({
},
})
+ if (projects.length === 0) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message:
+ 'None of the selected projects are in a mentoring round. Add them to a mentoring round first.',
+ })
+ }
+
+ const ineligibleCount = input.projectIds.length - projects.length
+
// Track per-mentor (for emails) and per-project (for team intros) state.
const perMentor = new Map<
string,
@@ -884,6 +919,7 @@ export const mentorRouter = router({
return {
totalAssigned,
totalSkipped,
+ ineligibleProjectCount: ineligibleCount,
touchedProjectCount: touchedProjectIds.size,
perMentor: Array.from(perMentor.entries()).map(([id, b]) => ({
mentorId: id,
diff --git a/tests/unit/multi-mentor-assignment.test.ts b/tests/unit/multi-mentor-assignment.test.ts
index bae675c..8dfdd28 100644
--- a/tests/unit/multi-mentor-assignment.test.ts
+++ b/tests/unit/multi-mentor-assignment.test.ts
@@ -42,6 +42,37 @@ async function createUserWithRoles(
})
}
+/**
+ * mentor.assign and mentor.bulkAssign now require the project to be enrolled
+ * in some MENTORING round. This helper sets up the minimum: one competition
+ * + one MENTORING round + one ProjectRoundState linking the project.
+ */
+async function attachToMentoringRound(programId: string, projectId: string) {
+ const compSlug = `comp-${uid()}`
+ const competition = await prisma.competition.create({
+ data: {
+ name: `Comp ${compSlug}`,
+ slug: compSlug,
+ programId,
+ status: 'ACTIVE',
+ },
+ })
+ const round = await prisma.round.create({
+ data: {
+ name: `Mentoring ${uid()}`,
+ slug: `mentoring-${uid()}`,
+ roundType: 'MENTORING',
+ sortOrder: 1,
+ status: 'ROUND_ACTIVE',
+ competitionId: competition.id,
+ },
+ })
+ await prisma.projectRoundState.create({
+ data: { roundId: round.id, projectId },
+ })
+ return { competitionId: competition.id, roundId: round.id }
+}
+
describe('mentor.assign — stacking + per-team email idempotency', () => {
const programIds: string[] = []
const userIds: string[] = []
@@ -62,6 +93,7 @@ describe('mentor.assign — stacking + per-team email idempotency', () => {
const program = await createTestProgram({ name: `assign-stack-${uid()}` })
programIds.push(program.id)
const project = await createTestProject(program.id, { title: 'Stacking Project' })
+ await attachToMentoringRound(program.id, project.id)
const m1 = await createUserWithRoles('MENTOR', ['MENTOR'], { name: 'M1' })
const m2 = await createUserWithRoles('MENTOR', ['MENTOR'], { name: 'M2' })
@@ -93,6 +125,7 @@ describe('mentor.assign — stacking + per-team email idempotency', () => {
const program = await createTestProgram({ name: `assign-dup-${uid()}` })
programIds.push(program.id)
const project = await createTestProject(program.id, { title: 'Dup Project' })
+ await attachToMentoringRound(program.id, project.id)
const mentor = await createUserWithRoles('MENTOR', ['MENTOR'])
userIds.push(mentor.id)
@@ -114,7 +147,9 @@ describe('mentor.assign — stacking + per-team email idempotency', () => {
const program = await createTestProgram({ name: `assign-email-${uid()}` })
programIds.push(program.id)
const project1 = await createTestProject(program.id, { title: 'Project Alpha' })
+ await attachToMentoringRound(program.id, project1.id)
const project2 = await createTestProject(program.id, { title: 'Project Beta' })
+ await attachToMentoringRound(program.id, project2.id)
const mentor = await createUserWithRoles('MENTOR', ['MENTOR'])
userIds.push(mentor.id)
@@ -144,6 +179,7 @@ describe('mentor.assign — stacking + per-team email idempotency', () => {
const program = await createTestProgram({ name: `assign-comentor-${uid()}` })
programIds.push(program.id)
const project = await createTestProject(program.id, { title: 'Co-mentor Project' })
+ await attachToMentoringRound(program.id, project.id)
const m1 = await createUserWithRoles('MENTOR', ['MENTOR'], { name: 'Co-1' })
const m2 = await createUserWithRoles('MENTOR', ['MENTOR'], { name: 'Co-2' })
userIds.push(m1.id, m2.id)
@@ -169,6 +205,7 @@ describe('mentor.assign — stacking + per-team email idempotency', () => {
const program = await createTestProgram({ name: `assign-redrop-${uid()}` })
programIds.push(program.id)
const project = await createTestProject(program.id, { title: 'Re-assign Project' })
+ await attachToMentoringRound(program.id, project.id)
const mentor = await createUserWithRoles('MENTOR', ['MENTOR'])
userIds.push(mentor.id)