feat(mentor): show co-mentors on workspace page (PR8 Task 9)

- 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) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-05-22 17:07:11 +02:00
parent ee47c0305f
commit d440b5f274
2 changed files with 100 additions and 1 deletions

View File

@@ -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 (
<div className="space-y-6">
@@ -70,6 +94,37 @@ export default function MentorWorkspaceDetailPage() {
{project.teamName && (
<p className="text-muted-foreground mt-1">{project.teamName}</p>
)}
{coMentors.length > 0 && (
<div className="mt-2 flex items-center gap-1.5 text-sm text-muted-foreground">
<Users className="h-3.5 w-3.5 shrink-0" />
<span>
You + {coMentors.length} co-mentor
{coMentors.length === 1 ? '' : 's'}:{' '}
<span className="text-foreground">
{visibleCoMentors.join(', ')}
</span>
{hiddenCoMentors.length > 0 && (
<>
{' '}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="cursor-help underline decoration-dotted underline-offset-2">
+{hiddenCoMentors.length} more
</span>
</TooltipTrigger>
<TooltipContent>
<div className="text-xs">
{hiddenCoMentors.join(', ')}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</span>
</div>
)}
</div>
</div>

View File

@@ -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
*/