From d440b5f2740edc30cd2435db646e0ce6f0576e81 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 May 2026 17:07:11 +0200 Subject: [PATCH] feat(mentor): show co-mentors on workspace page (PR8 Task 9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds mentor.getProjectMentors({ projectId }) — returns all active MentorAssignment rows for a project, authorized to any mentor on it - Workspace page header surfaces "You + N co-mentor(s): names…" so each mentor knows the team composition without having to ask the admin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../mentor/workspace/[projectId]/page.tsx | 57 ++++++++++++++++++- src/server/routers/mentor.ts | 44 ++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/src/app/(mentor)/mentor/workspace/[projectId]/page.tsx b/src/app/(mentor)/mentor/workspace/[projectId]/page.tsx index ddc7a11..986cd3d 100644 --- a/src/app/(mentor)/mentor/workspace/[projectId]/page.tsx +++ b/src/app/(mentor)/mentor/workspace/[projectId]/page.tsx @@ -1,21 +1,29 @@ 'use client' import { useParams, useRouter } from 'next/navigation' +import { useSession } from 'next-auth/react' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' import { WorkspaceChat } from '@/components/mentor/workspace-chat' import { FilePromotionPanel } from '@/components/mentor/file-promotion-panel' import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-panel' -import { ArrowLeft, MessageSquare, FileText, Upload } from 'lucide-react' +import { ArrowLeft, MessageSquare, FileText, Upload, Users } from 'lucide-react' import { toast } from 'sonner' export default function MentorWorkspaceDetailPage() { const params = useParams() const router = useRouter() + const { data: session } = useSession() const projectId = params.projectId as string // Get mentor assignment for this project @@ -27,6 +35,22 @@ export default function MentorWorkspaceDetailPage() { { enabled: !!projectId } ) + // Co-mentor visibility (PR8 multi-mentor): show who else is on the team. + // Gracefully tolerates stale tabs where the caller no longer has access + // (assignment dropped) — query just returns nothing in that case. + const { data: projectMentors } = trpc.mentor.getProjectMentors.useQuery( + { projectId }, + { enabled: !!projectId, retry: false } + ) + + const currentUserId = session?.user?.id + const coMentors = (projectMentors ?? []).filter( + a => a.mentor.id !== currentUserId + ) + const coMentorNames = coMentors.map(a => a.mentor.name ?? 'Unnamed mentor') + const visibleCoMentors = coMentorNames.slice(0, 3) + const hiddenCoMentors = coMentorNames.slice(3) + if (isLoading) { return (
@@ -70,6 +94,37 @@ export default function MentorWorkspaceDetailPage() { {project.teamName && (

{project.teamName}

)} + {coMentors.length > 0 && ( +
+ + + You + {coMentors.length} co-mentor + {coMentors.length === 1 ? '' : 's'}:{' '} + + {visibleCoMentors.join(', ')} + + {hiddenCoMentors.length > 0 && ( + <> + {' '} + + + + + +{hiddenCoMentors.length} more + + + +
+ {hiddenCoMentors.join(', ')} +
+
+
+
+ + )} +
+
+ )}
diff --git a/src/server/routers/mentor.ts b/src/server/routers/mentor.ts index 1677703..adbfd6a 100644 --- a/src/server/routers/mentor.ts +++ b/src/server/routers/mentor.ts @@ -1326,6 +1326,50 @@ export const mentorRouter = router({ return assignments }), + /** + * List all active mentors assigned to a project (PR8 multi-mentor). + * + * Returns one row per active MentorAssignment (droppedAt = null) with the + * mentor's id + name. Used by the mentor workspace page to display the + * co-mentor team so each mentor knows who else they're working with. + * + * Authorization: caller must be an active mentor on the project (or an + * admin via mentorProcedure). Non-assigned mentors get FORBIDDEN. + */ + getProjectMentors: mentorProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { + const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role) + + if (!isAdmin) { + const ownAssignment = await ctx.prisma.mentorAssignment.findFirst({ + where: { + projectId: input.projectId, + mentorId: ctx.user.id, + droppedAt: null, + }, + select: { id: true }, + }) + if (!ownAssignment) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'You are not assigned to mentor this project', + }) + } + } + + const assignments = await ctx.prisma.mentorAssignment.findMany({ + where: { projectId: input.projectId, droppedAt: null }, + select: { + id: true, + mentor: { select: { id: true, name: true } }, + }, + orderBy: { assignedAt: 'asc' }, + }) + + return assignments + }), + /** * Get detailed project info for a mentor's assigned project */