'use client' import { useEffect, useState } from 'react' import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { directSessionUpdate } from '@/lib/session-update' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Skeleton } from '@/components/ui/skeleton' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { toast } from 'sonner' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { TagInput } from '@/components/shared/tag-input' import { UserActivityLog } from '@/components/shared/user-activity-log' import { EvaluationEditSheet } from '@/components/admin/evaluation-edit-sheet' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { UserAvatar } from '@/components/shared/user-avatar' import { Checkbox } from '@/components/ui/checkbox' import { ArrowLeft, Save, Mail, User, Shield, Loader2, AlertCircle, ClipboardList, Eye, ThumbsUp, ThumbsDown, Globe, Building2, FileText, FolderOpen, LogIn, Calendar, Clock, Link as LinkIcon, Copy, Check, } from 'lucide-react' import { getCountryName, getCountryFlag } from '@/lib/countries' import { formatRelativeTime } from '@/lib/utils' function getRoleHomePath(role: string): string { switch (role) { case 'JURY_MEMBER': return '/jury' case 'APPLICANT': return '/applicant' case 'MENTOR': return '/mentor' case 'OBSERVER': return '/observer' default: return '/admin' } } const statusVariant: Record = { ACTIVE: 'success', SUSPENDED: 'destructive', INVITED: 'secondary', NONE: 'secondary', } const roleColors: Record = { JURY_MEMBER: 'default', MENTOR: 'secondary', OBSERVER: 'outline', PROGRAM_ADMIN: 'default', SUPER_ADMIN: 'default', APPLICANT: 'secondary', AUDIENCE: 'outline', } export default function MemberDetailPage() { const params = useParams() const router = useRouter() const utils = trpc.useUtils() const userId = params.id as string const { data: user, isLoading, error, refetch } = trpc.user.get.useQuery({ id: userId }) const { data: currentUser } = trpc.user.me.useQuery() const isSuperAdmin = currentUser?.role === 'SUPER_ADMIN' const updateUser = trpc.user.update.useMutation() const sendInvitation = trpc.user.sendInvitation.useMutation() const generateAccessLink = trpc.user.generateAccessLink.useMutation() const startImpersonation = trpc.user.startImpersonation.useMutation() const [accessLinkOpen, setAccessLinkOpen] = useState(false) const [accessLink, setAccessLink] = useState<{ url: string kind: 'setup' | 'magic_login' expiresAt: Date } | null>(null) const [linkCopied, setLinkCopied] = useState(false) const handleGenerateAccessLink = async () => { try { const result = await generateAccessLink.mutateAsync({ userId }) setAccessLink({ url: result.url, kind: result.kind, expiresAt: new Date(result.expiresAt), }) setLinkCopied(false) setAccessLinkOpen(true) } catch (error) { toast.error( error instanceof Error ? error.message : 'Failed to generate access link' ) } } const handleCopyAccessLink = async () => { if (!accessLink) return try { await navigator.clipboard.writeText(accessLink.url) setLinkCopied(true) toast.success('Link copied to clipboard') } catch { toast.error('Could not copy — please select and copy the link manually') } } // Mentor assignments (only fetched for mentors) const { data: mentorAssignments } = trpc.mentor.listAssignments.useQuery( { mentorId: userId, page: 1, perPage: 50 }, { enabled: user?.role === 'MENTOR' } ) // Juror evaluations (only fetched for jury members) const isJuror = user?.role === 'JURY_MEMBER' || user?.roles?.includes('JURY_MEMBER') const { data: jurorEvaluations } = trpc.evaluation.getJurorEvaluations.useQuery( { userId }, { enabled: !!user && !!isJuror } ) // eslint-disable-next-line @typescript-eslint/no-explicit-any const [selectedEvaluation, setSelectedEvaluation] = useState(null) const [name, setName] = useState('') const [email, setEmail] = useState('') const [role, setRole] = useState('JURY_MEMBER') const [status, setStatus] = useState('NONE') const [expertiseTags, setExpertiseTags] = useState([]) const [maxAssignments, setMaxAssignments] = useState('') const [showSuperAdminConfirm, setShowSuperAdminConfirm] = useState(false) const [pendingSuperAdminRole, setPendingSuperAdminRole] = useState(false) const [additionalRoles, setAdditionalRoles] = useState([]) useEffect(() => { if (user) { setName(user.name || '') setEmail(user.email || '') setRole(user.role) setStatus(user.status) setExpertiseTags(user.expertiseTags || []) setMaxAssignments(user.maxAssignments?.toString() || '') setAdditionalRoles(user.roles?.filter((r: string) => r !== user.role) || []) } }, [user]) const handleSave = async () => { try { const allRoles = [role, ...additionalRoles.filter((r) => r !== role)] as Array<'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER' | 'APPLICANT' | 'AUDIENCE'> await updateUser.mutateAsync({ id: userId, email: email || undefined, name: name || null, role: role as typeof allRoles[number], roles: allRoles, status: status as 'NONE' | 'INVITED' | 'ACTIVE' | 'SUSPENDED', expertiseTags, maxAssignments: maxAssignments ? parseInt(maxAssignments) : null, }) utils.user.get.invalidate({ id: userId }) utils.user.list.invalidate() toast.success('Member updated successfully') } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to update member') } } const handleSendInvitation = async () => { try { await sendInvitation.mutateAsync({ userId }) toast.success('Invitation email sent successfully') refetch() utils.user.list.invalidate() } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to send invitation') } } const handleImpersonate = async () => { try { const result = await startImpersonation.mutateAsync({ targetUserId: userId }) const ok = await directSessionUpdate({ impersonate: userId }) if (!ok) { toast.error('Failed to update session for impersonation') return } window.location.href = getRoleHomePath(result.targetRole) } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to start impersonation') } } if (isLoading) { return (
) } if (error || !user) { return (
Error Loading Member {error?.message || 'The member you\'re looking for does not exist.'}
) } const displayRoles = user.roles?.length ? user.roles : [user.role] return (
{/* Back nav */} {/* Header Hero */}

{user.name || 'Unnamed Member'}

{user.email}

{user.status === 'NONE' ? 'Not Invited' : user.status} {displayRoles.map((r) => ( {r.replace(/_/g, ' ')} ))}
{(user.status === 'NONE' || user.status === 'INVITED') && ( )} {user.status !== 'SUSPENDED' && ( )}
Profile {isJuror && ( Evaluations {jurorEvaluations && jurorEvaluations.length > 0 && ( {jurorEvaluations.length} )} )}
{/* Left column: Profile info + Projects */}
{/* Profile Details (read-only) */} {(user.nationality || user.country || user.institution || user.bio) && (
Profile Details
Information provided during onboarding
{user.nationality && (
{getCountryFlag(user.nationality)}

Nationality

{getCountryName(user.nationality)}

)} {user.country && (
{getCountryFlag(user.country)}

Country of Residence

{getCountryName(user.country)}

)} {user.institution && (

Institution / Organization

{user.institution}

)} {user.bio && (

Bio

{user.bio}

)}
)} {/* Projects */} {user.teamMemberships && user.teamMemberships.length > 0 && (
Projects ({user.teamMemberships.length})
{user.teamMemberships.map((tm) => (

{tm.project.title}

{tm.project.teamName && (

{tm.project.teamName}

)}
{tm.project.competitionCategory && ( {tm.project.competitionCategory.replace('_', ' ')} )} {tm.role === 'LEAD' ? 'Lead' : tm.role === 'ADVISOR' ? 'Advisor' : 'Member'}
))}
)} {/* Jury Groups */} {user.juryGroupMemberships && user.juryGroupMemberships.length > 0 && (
Jury Groups ({user.juryGroupMemberships.length})
{user.juryGroupMemberships.map((m: { id: string; role: string; juryGroup: { id: string; name: string } }) => ( {m.juryGroup.name} ({m.role === 'CHAIR' ? 'Chair' : m.role === 'OBSERVER' ? 'Observer' : 'Member'}) ))}
)} {/* Mentor Assignments */} {user.role === 'MENTOR' && mentorAssignments && mentorAssignments.assignments.length > 0 && (
Mentored Projects
{mentorAssignments.assignments.length} project{mentorAssignments.assignments.length !== 1 ? 's' : ''} assigned
Project Category Status Assigned {mentorAssignments.assignments.map((assignment) => ( {assignment.project.title} {assignment.project.teamName && (

{assignment.project.teamName}

)}
{assignment.project.competitionCategory ? ( {assignment.project.competitionCategory.replace('_', ' ')} ) : '-'} {assignment.project.status ?? 'SUBMITTED'} {new Date(assignment.assignedAt).toLocaleDateString()}
))}
)} {/* Activity Log */}
{/* Right sidebar: Edit form + Quick info */}
{/* Quick Info Card */}
Quick Info
Created {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
Last Login {user.lastLoginAt ? ( {formatRelativeTime(user.lastLoginAt)} ) : 'Never'}
{user._count && !['APPLICANT', 'AUDIENCE'].includes(user.role) && ( <>
Jury Assignments {user._count.assignments}
Mentor Assignments {user._count.mentorAssignments}
)}
{/* Status Alerts */} {user.status === 'NONE' && ( Not Yet Invited This member was added via import but hasn't been invited yet. )} {user.status === 'INVITED' && ( Invitation Pending This member hasn't accepted their invitation yet. )} {/* Basic Info Edit */}
Edit Details
setEmail(e.target.value)} />
setName(e.target.value)} placeholder="Enter name" />

Grant additional dashboard access beyond the primary role

{(['JURY_MEMBER', 'OBSERVER', 'MENTOR'] as const) .filter((r) => r !== role) .map((r) => ( ))}
{/* Expertise & Capacity for non-applicant */} {!['APPLICANT', 'AUDIENCE'].includes(user.role) && ( <>
setMaxAssignments(e.target.value)} placeholder="Unlimited" />
)}
{/* Evaluations Tab */} {isJuror && ( {!jurorEvaluations || jurorEvaluations.length === 0 ? (

No evaluations submitted yet

) : ( (() => { const byRound = new Map() for (const ev of jurorEvaluations) { const key = ev.roundName if (!byRound.has(key)) byRound.set(key, []) byRound.get(key)!.push(ev) } return Array.from(byRound.entries()).map(([roundName, evals]) => ( {roundName} {evals.length} evaluation{evals.length !== 1 ? 's' : ''} Project Score Decision Status Submitted {evals.map((ev) => ( {ev.projectTitle} {ev.evaluation.globalScore !== null && ev.evaluation.globalScore !== undefined ? {ev.evaluation.globalScore}/10 : -} {ev.evaluation.binaryDecision !== null && ev.evaluation.binaryDecision !== undefined ? ( ev.evaluation.binaryDecision ? (
Yes
) : (
No
) ) : ( - )}
{ev.evaluation.status.replace('_', ' ')} {ev.evaluation.submittedAt ? new Date(ev.evaluation.submittedAt).toLocaleDateString() : '-'}
))}
)) })() )} { if (!open) setSelectedEvaluation(null) }} onSaved={() => utils.evaluation.getJurorEvaluations.invalidate({ userId })} />
)}
{/* Super Admin Confirmation Dialog */} Grant Super Admin Access? This will grant {name || user?.name || 'this user'} full Super Admin access, including user management, system settings, and all administrative capabilities. This action should only be performed for trusted administrators. setPendingSuperAdminRole(false)}> Cancel { setRole('SUPER_ADMIN') setPendingSuperAdminRole(false) setShowSuperAdminConfirm(false) }} className="bg-red-600 hover:bg-red-700" > Confirm Super Admin Access link ready {accessLink?.kind === 'magic_login' ? `Share this with ${user.name || user.email} via Slack, WhatsApp, or any channel that reaches them. Clicking it will sign them in directly (their existing password is preserved) and take them to their dashboard.` : `Share this with ${user.name || user.email} via Slack, WhatsApp, or any channel that reaches them. They'll be walked through setting a password and onboarding.`}
e.currentTarget.select()} className="font-mono text-xs bg-background" />
Expires {accessLink?.expiresAt.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })} {' · '}consumed on first successful login

Don't paste this in a public channel. Anyone with the link can sign in as this user until it's consumed.

) }