diff --git a/src/app/(admin)/admin/mentors/page.tsx b/src/app/(admin)/admin/mentors/page.tsx index 4a655f8..7f2e7ce 100644 --- a/src/app/(admin)/admin/mentors/page.tsx +++ b/src/app/(admin)/admin/mentors/page.tsx @@ -1,5 +1,281 @@ -import { redirect } from 'next/navigation' +'use client' -export default function MentorsPage() { - redirect('/admin/members') +import { useMemo, useState } from 'react' +import Link from 'next/link' +import { trpc } from '@/lib/trpc/client' +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Skeleton } from '@/components/ui/skeleton' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { ArrowUpDown, Search, Users } from 'lucide-react' + +type SortKey = 'name' | 'load' | 'capacity' | 'lastActivity' + +function formatRelativePast(date: Date | string | null): string { + if (!date) return '—' + const d = typeof date === 'string' ? new Date(date) : date + const ms = Date.now() - d.getTime() + const days = Math.floor(ms / 86_400_000) + const hours = Math.floor(ms / 3_600_000) + if (days >= 1) return `${days}d ago` + if (hours >= 1) return `${hours}h ago` + const minutes = Math.floor(ms / 60_000) + return `${Math.max(0, minutes)}m ago` +} + +type Mentor = { + id: string + name: string | null + email: string + country: string | null + expertiseTags: string[] + currentAssignments: number + completedAssignments: number + maxAssignments: number | null + capacityRemaining: number | null + lastActivityAt: Date | string | null +} + +export default function MentorsListPage() { + const [search, setSearch] = useState('') + const [sortKey, setSortKey] = useState('load') + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc') + + const { data, isLoading } = trpc.mentor.getMentorPool.useQuery({}) + + const filtered = useMemo(() => { + if (!data) return [] + const q = search.trim().toLowerCase() + let rows: Mentor[] = data.mentors + if (q) { + rows = rows.filter((m) => + [m.name ?? '', m.email, m.country ?? '', ...m.expertiseTags] + .join(' ') + .toLowerCase() + .includes(q), + ) + } + rows = [...rows].sort((a, b) => { + let av: string | number = 0 + let bv: string | number = 0 + switch (sortKey) { + case 'name': + av = (a.name ?? '').toLowerCase() + bv = (b.name ?? '').toLowerCase() + break + case 'load': + av = a.currentAssignments + bv = b.currentAssignments + break + case 'capacity': + av = a.capacityRemaining ?? Number.MAX_SAFE_INTEGER + bv = b.capacityRemaining ?? Number.MAX_SAFE_INTEGER + break + case 'lastActivity': + av = a.lastActivityAt ? new Date(a.lastActivityAt).getTime() : 0 + bv = b.lastActivityAt ? new Date(b.lastActivityAt).getTime() : 0 + break + } + if (av < bv) return sortDir === 'asc' ? -1 : 1 + if (av > bv) return sortDir === 'asc' ? 1 : -1 + return 0 + }) + return rows + }, [data, search, sortKey, sortDir]) + + const toggleSort = (key: SortKey) => { + if (sortKey === key) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')) + else { + setSortKey(key) + setSortDir(key === 'name' ? 'asc' : 'desc') + } + } + + const SortHeader = ({ + k, + children, + align = 'left', + }: { + k: SortKey + children: React.ReactNode + align?: 'left' | 'right' + }) => ( + + ) + + return ( +
+
+
+

Mentors

+

+ All users with the MENTOR role and their current workload. +

+
+ +
+ +
+ + + + Pool size + + + +
{data?.poolSize ?? '—'}
+
+
+ + + + Active assignments + + + +
+ {data?.totalCurrentAssignments ?? '—'} +
+
+
+ + + + Average load + + + +
+ {data && data.poolSize > 0 + ? (data.totalCurrentAssignments / data.poolSize).toFixed(1) + : '—'} +
+
+
+
+ + + + Mentor list +
+ + setSearch(e.target.value)} + placeholder="Search by name, email, country, or expertise tag…" + className="pl-9" + /> +
+
+ + {isLoading ? ( +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ ) : filtered.length === 0 ? ( +
+ {search ? 'No matching mentors.' : 'No mentors yet.'} +
+ ) : ( +
+ + + + + Mentor + + Expertise + Country + + + Active + + + Completed + + + Capacity + + + + Last activity + + + + + {filtered.map((m) => ( + + + +
{m.name ?? 'Unnamed'}
+
{m.email}
+ +
+ +
+ {m.expertiseTags.slice(0, 4).map((tag) => ( + + {tag} + + ))} + {m.expertiseTags.length > 4 && ( + + +{m.expertiseTags.length - 4} + + )} +
+
+ {m.country ?? '—'} + + {m.currentAssignments} + + + {m.completedAssignments} + + + {m.capacityRemaining != null ? m.capacityRemaining : '∞'} + + + {formatRelativePast(m.lastActivityAt)} + +
+ ))} +
+
+
+ )} +
+
+
+ ) }