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: { roundId: input.roundId, 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: { round: { include: { program: { select: { name: true, year: true } }, }, }, 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: { round: { include: { program: { select: { id: true, name: true, year: true } }, }, }, 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 = { ...(input.roundId && { project: { roundId: input.roundId } }), ...(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, status: true, oceanIssue: true, competitionCategory: true, }, }, 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), } }), })