2026-01-30 13:41:32 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
|
|
|
|
import { router, mentorProcedure, adminProcedure } from '../trpc'
|
|
|
|
|
import { MentorAssignmentMethod } from '@prisma/client'
|
|
|
|
|
import {
|
|
|
|
|
getAIMentorSuggestions,
|
|
|
|
|
getRoundRobinMentor,
|
|
|
|
|
} from '../services/mentor-matching'
|
|
|
|
|
|
|
|
|
|
export const mentorRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Get AI-suggested mentor matches for a project
|
|
|
|
|
*/
|
|
|
|
|
getSuggestions: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
limit: z.number().min(1).max(10).default(5),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
// Verify project exists
|
|
|
|
|
const project = await ctx.prisma.project.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.projectId },
|
|
|
|
|
include: {
|
|
|
|
|
mentorAssignment: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (project.mentorAssignment) {
|
|
|
|
|
return {
|
|
|
|
|
currentMentor: project.mentorAssignment,
|
|
|
|
|
suggestions: [],
|
|
|
|
|
message: 'Project already has a mentor assigned',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const suggestions = await getAIMentorSuggestions(
|
|
|
|
|
ctx.prisma,
|
|
|
|
|
input.projectId,
|
|
|
|
|
input.limit
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Enrich with mentor details
|
|
|
|
|
const enrichedSuggestions = await Promise.all(
|
|
|
|
|
suggestions.map(async (suggestion) => {
|
|
|
|
|
const mentor = await ctx.prisma.user.findUnique({
|
|
|
|
|
where: { id: suggestion.mentorId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
mentorAssignments: {
|
|
|
|
|
select: { id: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...suggestion,
|
|
|
|
|
mentor: mentor
|
|
|
|
|
? {
|
|
|
|
|
id: mentor.id,
|
|
|
|
|
name: mentor.name,
|
|
|
|
|
email: mentor.email,
|
|
|
|
|
expertiseTags: mentor.expertiseTags,
|
|
|
|
|
assignmentCount: mentor.mentorAssignments.length,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
currentMentor: null,
|
|
|
|
|
suggestions: enrichedSuggestions.filter((s) => s.mentor !== null),
|
|
|
|
|
message: null,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Manually assign a mentor to a project
|
|
|
|
|
*/
|
|
|
|
|
assign: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
mentorId: z.string(),
|
|
|
|
|
method: z.nativeEnum(MentorAssignmentMethod).default('MANUAL'),
|
|
|
|
|
aiConfidenceScore: z.number().optional(),
|
|
|
|
|
expertiseMatchScore: z.number().optional(),
|
|
|
|
|
aiReasoning: z.string().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify project exists and doesn't have a mentor
|
|
|
|
|
const project = await ctx.prisma.project.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.projectId },
|
|
|
|
|
include: { mentorAssignment: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (project.mentorAssignment) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'CONFLICT',
|
|
|
|
|
message: 'Project already has a mentor assigned',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify mentor exists
|
|
|
|
|
const mentor = await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.mentorId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create assignment
|
|
|
|
|
const assignment = await ctx.prisma.mentorAssignment.create({
|
|
|
|
|
data: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
mentorId: input.mentorId,
|
|
|
|
|
method: input.method,
|
|
|
|
|
assignedBy: ctx.user.id,
|
|
|
|
|
aiConfidenceScore: input.aiConfidenceScore,
|
|
|
|
|
expertiseMatchScore: input.expertiseMatchScore,
|
|
|
|
|
aiReasoning: input.aiReasoning,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
mentor: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
project: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create audit log
|
|
|
|
|
await ctx.prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'MENTOR_ASSIGN',
|
|
|
|
|
entityType: 'MentorAssignment',
|
|
|
|
|
entityId: assignment.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
projectTitle: assignment.project.title,
|
|
|
|
|
mentorId: input.mentorId,
|
|
|
|
|
mentorName: assignment.mentor.name,
|
|
|
|
|
method: input.method,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return assignment
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Auto-assign a mentor using AI or round-robin
|
|
|
|
|
*/
|
|
|
|
|
autoAssign: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
useAI: z.boolean().default(true),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify project exists and doesn't have a mentor
|
|
|
|
|
const project = await ctx.prisma.project.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.projectId },
|
|
|
|
|
include: { mentorAssignment: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (project.mentorAssignment) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'CONFLICT',
|
|
|
|
|
message: 'Project already has a mentor assigned',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mentorId: string | null = null
|
|
|
|
|
let method: MentorAssignmentMethod = 'ALGORITHM'
|
|
|
|
|
let aiConfidenceScore: number | undefined
|
|
|
|
|
let expertiseMatchScore: number | undefined
|
|
|
|
|
let aiReasoning: string | undefined
|
|
|
|
|
|
|
|
|
|
if (input.useAI) {
|
|
|
|
|
// Try AI matching first
|
|
|
|
|
const suggestions = await getAIMentorSuggestions(ctx.prisma, input.projectId, 1)
|
|
|
|
|
|
|
|
|
|
if (suggestions.length > 0) {
|
|
|
|
|
const best = suggestions[0]
|
|
|
|
|
mentorId = best.mentorId
|
|
|
|
|
method = 'AI_AUTO'
|
|
|
|
|
aiConfidenceScore = best.confidenceScore
|
|
|
|
|
expertiseMatchScore = best.expertiseMatchScore
|
|
|
|
|
aiReasoning = best.reasoning
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to round-robin
|
|
|
|
|
if (!mentorId) {
|
|
|
|
|
mentorId = await getRoundRobinMentor(ctx.prisma)
|
|
|
|
|
method = 'ALGORITHM'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!mentorId) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'NOT_FOUND',
|
|
|
|
|
message: 'No available mentors found',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create assignment
|
|
|
|
|
const assignment = await ctx.prisma.mentorAssignment.create({
|
|
|
|
|
data: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
mentorId,
|
|
|
|
|
method,
|
|
|
|
|
assignedBy: ctx.user.id,
|
|
|
|
|
aiConfidenceScore,
|
|
|
|
|
expertiseMatchScore,
|
|
|
|
|
aiReasoning,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
mentor: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
project: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create audit log
|
|
|
|
|
await ctx.prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'MENTOR_AUTO_ASSIGN',
|
|
|
|
|
entityType: 'MentorAssignment',
|
|
|
|
|
entityId: assignment.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
projectTitle: assignment.project.title,
|
|
|
|
|
mentorId,
|
|
|
|
|
mentorName: assignment.mentor.name,
|
|
|
|
|
method,
|
|
|
|
|
aiConfidenceScore,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return assignment
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove mentor assignment
|
|
|
|
|
*/
|
|
|
|
|
unassign: adminProcedure
|
|
|
|
|
.input(z.object({ projectId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const assignment = await ctx.prisma.mentorAssignment.findUnique({
|
|
|
|
|
where: { projectId: input.projectId },
|
|
|
|
|
include: {
|
|
|
|
|
mentor: { select: { id: true, name: true } },
|
|
|
|
|
project: { select: { id: true, title: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!assignment) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'NOT_FOUND',
|
|
|
|
|
message: 'No mentor assignment found for this project',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await ctx.prisma.mentorAssignment.delete({
|
|
|
|
|
where: { projectId: input.projectId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create audit log
|
|
|
|
|
await ctx.prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'MENTOR_UNASSIGN',
|
|
|
|
|
entityType: 'MentorAssignment',
|
|
|
|
|
entityId: assignment.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
projectTitle: assignment.project.title,
|
|
|
|
|
mentorId: assignment.mentor.id,
|
|
|
|
|
mentorName: assignment.mentor.name,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk auto-assign mentors to projects without one
|
|
|
|
|
*/
|
|
|
|
|
bulkAutoAssign: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
roundId: z.string(),
|
|
|
|
|
useAI: z.boolean().default(true),
|
|
|
|
|
maxAssignments: z.number().min(1).max(100).default(50),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Get projects without mentors
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: {
|
2026-02-02 22:33:55 +01:00
|
|
|
roundProjects: { some: { roundId: input.roundId } },
|
2026-01-30 13:41:32 +01:00
|
|
|
mentorAssignment: null,
|
|
|
|
|
wantsMentorship: true,
|
|
|
|
|
},
|
|
|
|
|
select: { id: true },
|
|
|
|
|
take: input.maxAssignments,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (projects.length === 0) {
|
|
|
|
|
return {
|
|
|
|
|
assigned: 0,
|
|
|
|
|
failed: 0,
|
|
|
|
|
message: 'No projects need mentor assignment',
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let assigned = 0
|
|
|
|
|
let failed = 0
|
|
|
|
|
|
|
|
|
|
for (const project of projects) {
|
|
|
|
|
try {
|
|
|
|
|
let mentorId: string | null = null
|
|
|
|
|
let method: MentorAssignmentMethod = 'ALGORITHM'
|
|
|
|
|
let aiConfidenceScore: number | undefined
|
|
|
|
|
let expertiseMatchScore: number | undefined
|
|
|
|
|
let aiReasoning: string | undefined
|
|
|
|
|
|
|
|
|
|
if (input.useAI) {
|
|
|
|
|
const suggestions = await getAIMentorSuggestions(ctx.prisma, project.id, 1)
|
|
|
|
|
if (suggestions.length > 0) {
|
|
|
|
|
const best = suggestions[0]
|
|
|
|
|
mentorId = best.mentorId
|
|
|
|
|
method = 'AI_AUTO'
|
|
|
|
|
aiConfidenceScore = best.confidenceScore
|
|
|
|
|
expertiseMatchScore = best.expertiseMatchScore
|
|
|
|
|
aiReasoning = best.reasoning
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!mentorId) {
|
|
|
|
|
mentorId = await getRoundRobinMentor(ctx.prisma)
|
|
|
|
|
method = 'ALGORITHM'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mentorId) {
|
|
|
|
|
await ctx.prisma.mentorAssignment.create({
|
|
|
|
|
data: {
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
mentorId,
|
|
|
|
|
method,
|
|
|
|
|
assignedBy: ctx.user.id,
|
|
|
|
|
aiConfidenceScore,
|
|
|
|
|
expertiseMatchScore,
|
|
|
|
|
aiReasoning,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
assigned++
|
|
|
|
|
} else {
|
|
|
|
|
failed++
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
failed++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create audit log
|
|
|
|
|
await ctx.prisma.auditLog.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'MENTOR_BULK_ASSIGN',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: input.roundId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
assigned,
|
|
|
|
|
failed,
|
|
|
|
|
useAI: input.useAI,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
assigned,
|
|
|
|
|
failed,
|
|
|
|
|
message: `Assigned ${assigned} mentor(s), ${failed} failed`,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get mentor's assigned projects
|
|
|
|
|
*/
|
|
|
|
|
getMyProjects: mentorProcedure.query(async ({ ctx }) => {
|
|
|
|
|
const assignments = await ctx.prisma.mentorAssignment.findMany({
|
|
|
|
|
where: { mentorId: ctx.user.id },
|
|
|
|
|
include: {
|
|
|
|
|
project: {
|
|
|
|
|
include: {
|
2026-02-02 22:33:55 +01:00
|
|
|
program: { select: { name: true, year: true } },
|
|
|
|
|
roundProjects: {
|
2026-01-30 13:41:32 +01:00
|
|
|
include: {
|
2026-02-02 22:33:55 +01:00
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { name: true, year: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
2026-02-02 22:33:55 +01:00
|
|
|
orderBy: { addedAt: 'desc' },
|
|
|
|
|
take: 1,
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
teamMembers: {
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { assignedAt: 'desc' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return assignments
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get detailed project info for a mentor's assigned project
|
|
|
|
|
*/
|
|
|
|
|
getProjectDetail: mentorProcedure
|
|
|
|
|
.input(z.object({ projectId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
// Verify the mentor is assigned to this project
|
|
|
|
|
const assignment = await ctx.prisma.mentorAssignment.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
projectId: input.projectId,
|
|
|
|
|
mentorId: ctx.user.id,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Allow admins to access any project
|
|
|
|
|
const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)
|
|
|
|
|
|
|
|
|
|
if (!assignment && !isAdmin) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'You are not assigned to mentor this project',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const project = await ctx.prisma.project.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.projectId },
|
|
|
|
|
include: {
|
2026-02-02 22:33:55 +01:00
|
|
|
program: { select: { id: true, name: true, year: true } },
|
|
|
|
|
roundProjects: {
|
2026-01-30 13:41:32 +01:00
|
|
|
include: {
|
2026-02-02 22:33:55 +01:00
|
|
|
round: {
|
|
|
|
|
include: {
|
|
|
|
|
program: { select: { id: true, name: true, year: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
2026-02-02 22:33:55 +01:00
|
|
|
orderBy: { addedAt: 'desc' },
|
|
|
|
|
take: 1,
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
teamMembers: {
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
phoneNumber: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { role: 'asc' },
|
|
|
|
|
},
|
|
|
|
|
files: {
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
},
|
|
|
|
|
mentorAssignment: {
|
|
|
|
|
include: {
|
|
|
|
|
mentor: {
|
|
|
|
|
select: { id: true, name: true, email: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...project,
|
|
|
|
|
assignedAt: assignment?.assignedAt,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List all mentor assignments (admin)
|
|
|
|
|
*/
|
|
|
|
|
listAssignments: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
roundId: z.string().optional(),
|
|
|
|
|
mentorId: z.string().optional(),
|
|
|
|
|
page: z.number().min(1).default(1),
|
|
|
|
|
perPage: z.number().min(1).max(100).default(20),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const where = {
|
2026-02-02 22:33:55 +01:00
|
|
|
...(input.roundId && { project: { roundProjects: { some: { roundId: input.roundId } } } }),
|
2026-01-30 13:41:32 +01:00
|
|
|
...(input.mentorId && { mentorId: input.mentorId }),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [assignments, total] = await Promise.all([
|
|
|
|
|
ctx.prisma.mentorAssignment.findMany({
|
|
|
|
|
where,
|
|
|
|
|
include: {
|
|
|
|
|
project: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
teamName: true,
|
|
|
|
|
oceanIssue: true,
|
|
|
|
|
competitionCategory: true,
|
2026-02-02 22:33:55 +01:00
|
|
|
roundProjects: {
|
|
|
|
|
select: { status: true },
|
|
|
|
|
take: 1,
|
|
|
|
|
},
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
mentor: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { assignedAt: 'desc' },
|
|
|
|
|
skip: (input.page - 1) * input.perPage,
|
|
|
|
|
take: input.perPage,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.mentorAssignment.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
assignments,
|
|
|
|
|
total,
|
|
|
|
|
page: input.page,
|
|
|
|
|
perPage: input.perPage,
|
|
|
|
|
totalPages: Math.ceil(total / input.perPage),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
})
|