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:
@@ -1,21 +1,29 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import { useSession } from 'next-auth/react'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
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 { WorkspaceChat } from '@/components/mentor/workspace-chat'
|
||||||
import { FilePromotionPanel } from '@/components/mentor/file-promotion-panel'
|
import { FilePromotionPanel } from '@/components/mentor/file-promotion-panel'
|
||||||
import { WorkspaceFilesPanel } from '@/components/mentor/workspace-files-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'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
export default function MentorWorkspaceDetailPage() {
|
export default function MentorWorkspaceDetailPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { data: session } = useSession()
|
||||||
const projectId = params.projectId as string
|
const projectId = params.projectId as string
|
||||||
|
|
||||||
// Get mentor assignment for this project
|
// Get mentor assignment for this project
|
||||||
@@ -27,6 +35,22 @@ export default function MentorWorkspaceDetailPage() {
|
|||||||
{ enabled: !!projectId }
|
{ 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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -70,6 +94,37 @@ export default function MentorWorkspaceDetailPage() {
|
|||||||
{project.teamName && (
|
{project.teamName && (
|
||||||
<p className="text-muted-foreground mt-1">{project.teamName}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1326,6 +1326,50 @@ export const mentorRouter = router({
|
|||||||
return assignments
|
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
|
* Get detailed project info for a mentor's assigned project
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user