feat: mentor detail side sheet + Teams column
The mentor list now ends with a Teams column showing chips of each mentor's active assignments (truncated at 2 + overflow badge). Clicking any row opens a right-side Sheet with the mentor's profile (expertise, country, joined date, max assignments) and a per-team activity feed — project, status (active / completed / dropped), assignment date, and counts of messages / files / milestones with their last timestamp. Stat cards on both the Mentor and Mentee panels were stale and not particularly informative, so they're gone — the table itself is now the focal element on each panel. getMentorPool gained an activeTeams[] field; new getMentorDetail query backs the side sheet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
} from '@/components/ui/table'
|
||||
import { ArrowUpDown, GraduationCap, Search, Users } from 'lucide-react'
|
||||
import { formatEnumLabel } from '@/lib/utils'
|
||||
import { MentorDetailSheet } from '@/components/admin/mentor/mentor-detail-sheet'
|
||||
|
||||
type SortKey = 'name' | 'load' | 'capacity' | 'lastActivity'
|
||||
|
||||
@@ -60,12 +61,14 @@ type Mentor = {
|
||||
maxAssignments: number | null
|
||||
capacityRemaining: number | null
|
||||
lastActivityAt: Date | string | null
|
||||
activeTeams: { id: string; title: string }[]
|
||||
}
|
||||
|
||||
function MentorListPanel() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortKey, setSortKey] = useState<SortKey>('load')
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
const [detailMentorId, setDetailMentorId] = useState<string | null>(null)
|
||||
|
||||
const { data, isLoading } = trpc.mentor.getMentorPool.useQuery({})
|
||||
|
||||
@@ -140,45 +143,6 @@ function MentorListPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Pool size
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">{data?.poolSize ?? '—'}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Active assignments
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">
|
||||
{data?.totalCurrentAssignments ?? '—'}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Average load
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">
|
||||
{data && data.poolSize > 0
|
||||
? (data.totalCurrentAssignments / data.poolSize).toFixed(1)
|
||||
: '—'}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="space-y-4">
|
||||
<CardTitle className="text-base">Mentor list</CardTitle>
|
||||
@@ -213,12 +177,6 @@ function MentorListPanel() {
|
||||
</TableHead>
|
||||
<TableHead>Expertise</TableHead>
|
||||
<TableHead>Country</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<SortHeader k="load" align="right">
|
||||
Active
|
||||
</SortHeader>
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Completed</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<SortHeader k="capacity" align="right">
|
||||
Capacity
|
||||
@@ -227,16 +185,21 @@ function MentorListPanel() {
|
||||
<TableHead>
|
||||
<SortHeader k="lastActivity">Last activity</SortHeader>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<SortHeader k="load">Teams</SortHeader>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((m) => (
|
||||
<TableRow key={m.id}>
|
||||
<TableRow
|
||||
key={m.id}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setDetailMentorId(m.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Link href={`/admin/members/${m.id}`} className="hover:underline">
|
||||
<div className="font-medium">{m.name ?? 'Unnamed'}</div>
|
||||
<div className="text-muted-foreground text-xs">{m.email}</div>
|
||||
</Link>
|
||||
<div className="font-medium">{m.name ?? 'Unnamed'}</div>
|
||||
<div className="text-muted-foreground text-xs">{m.email}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -253,18 +216,35 @@ function MentorListPanel() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{m.country ?? '—'}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{m.currentAssignments}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{m.completedAssignments}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-sm tabular-nums">
|
||||
{m.capacityRemaining != null ? m.capacityRemaining : '∞'}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{formatRelativePast(m.lastActivityAt)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{m.activeTeams.length === 0 ? (
|
||||
<span className="text-muted-foreground text-xs">—</span>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{m.activeTeams.slice(0, 2).map((t) => (
|
||||
<Badge
|
||||
key={t.id}
|
||||
variant="outline"
|
||||
className="max-w-[12rem] truncate text-xs"
|
||||
title={t.title}
|
||||
>
|
||||
{t.title}
|
||||
</Badge>
|
||||
))}
|
||||
{m.activeTeams.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{m.activeTeams.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -273,6 +253,14 @@ function MentorListPanel() {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<MentorDetailSheet
|
||||
mentorId={detailMentorId}
|
||||
open={!!detailMentorId}
|
||||
onOpenChange={(next) => {
|
||||
if (!next) setDetailMentorId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -323,51 +311,6 @@ function MenteeActivityPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Unassigned
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">{totals.unassigned}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Assigned
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">{totals.assigned}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Active
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums">{totals.active}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||
Stalled
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold tabular-nums text-destructive">
|
||||
{totals.stalled}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
|
||||
Reference in New Issue
Block a user