'use client' import { useMemo, useState } from 'react' import Link from 'next/link' import { toast } from 'sonner' import { trpc } from '@/lib/trpc/client' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Search, UserPlus, ArrowRight, Sparkles, Loader2, Download, X, } from 'lucide-react' import { CountryDisplay } from '@/components/shared/country-display' type Filter = 'all' | 'unassigned' | 'assigned' | 'wants_only' export function MentoringProjectsTable({ roundId }: { roundId: string }) { const [search, setSearch] = useState('') const [filter, setFilter] = useState('all') const [selected, setSelected] = useState>(new Set()) const [bulkOpen, setBulkOpen] = useState(false) const [chosenMentorIds, setChosenMentorIds] = useState>(new Set()) const [mentorSearch, setMentorSearch] = useState('') const utils = trpc.useUtils() const { data, isLoading } = trpc.round.listMentoringProjects.useQuery( { roundId }, { refetchInterval: 30_000 }, ) const { data: importCandidates } = trpc.round.getMentoringImportCandidates.useQuery({ roundId }) const { data: mentorPool } = trpc.mentor.getMentorPool.useQuery( {}, { enabled: bulkOpen }, ) const bulkAssignMutation = trpc.mentor.bulkAssign.useMutation({ onSuccess: (result) => { if (result.totalAssigned === 0 && result.totalSkipped > 0) { toast.info( `No new assignments — every selected mentor is already on every selected project (${result.totalSkipped} pair${result.totalSkipped === 1 ? '' : 's'} skipped).`, ) } else { const mentorCount = result.perMentor.filter((m) => m.assigned > 0).length toast.success( `Created ${result.totalAssigned} assignment${ result.totalAssigned === 1 ? '' : 's' } across ${result.touchedProjectCount} project${ result.touchedProjectCount === 1 ? '' : 's' }${result.totalSkipped > 0 ? ` (${result.totalSkipped} pair${result.totalSkipped === 1 ? '' : 's'} already existed)` : ''}${ result.emailsSent > 0 ? ` · ${result.emailsSent} mentor email${result.emailsSent === 1 ? '' : 's'} sent` : '' }`, { description: mentorCount > 1 ? `Each of ${mentorCount} mentors gets a single combined email listing only their new projects.` : undefined, }, ) } utils.round.listMentoringProjects.invalidate({ roundId }) utils.round.getProjectsNeedingMentor.invalidate({ roundId }) utils.round.getMentoringImportCandidates.invalidate({ roundId }) utils.mentor.getMentorPool.invalidate() utils.mentor.getRoundStats.invalidate({ roundId }) utils.project.list.invalidate() setSelected(new Set()) setChosenMentorIds(new Set()) setMentorSearch('') setBulkOpen(false) }, onError: (err) => toast.error(err.message), }) const advanceMutation = trpc.round.advanceProjects.useMutation({ onSuccess: (result) => { toast.success( `Imported ${result.advancedCount} project${ result.advancedCount === 1 ? '' : 's' } from ${result.targetRoundName ? '' : ''}${ importCandidates?.priorRound?.name ?? 'the prior round' }`, ) utils.round.listMentoringProjects.invalidate({ roundId }) utils.round.getMentoringImportCandidates.invalidate({ roundId }) utils.round.getProjectsNeedingMentor.invalidate({ roundId }) }, onError: (err) => toast.error(err.message), }) const importBanner = importCandidates?.priorRound && importCandidates.pendingCount > 0 && (
{importCandidates.pendingCount} PASSED project {importCandidates.pendingCount === 1 ? '' : 's'} {' '} from{' '} {importCandidates.priorRound.name} {' '} {importCandidates.pendingCount === 1 ? "isn't" : "aren't"} in this mentoring round yet.
) const filtered = useMemo(() => { if (!data) return [] const q = search.trim().toLowerCase() return data.projects.filter((p) => { if (filter === 'unassigned' && p.mentors.length > 0) return false if (filter === 'assigned' && p.mentors.length === 0) return false if (filter === 'wants_only' && !p.wantsMentorship) return false if (!q) return true const hay = [ p.title, p.teamName ?? '', p.country ?? '', ...p.mentors.map((m) => m.name ?? m.email), ] .join(' ') .toLowerCase() return hay.includes(q) }) }, [data, search, filter]) const totals = useMemo(() => { if (!data) return { total: 0, unassigned: 0, assigned: 0, wants: 0 } return { total: data.projects.length, unassigned: data.projects.filter((p) => p.mentors.length === 0).length, assigned: data.projects.filter((p) => p.mentors.length > 0).length, wants: data.projects.filter((p) => p.wantsMentorship).length, } }, [data]) if (isLoading) { return (
{[1, 2, 3, 4, 5].map((i) => ( ))}
) } if (!data || data.projects.length === 0) { return (
{importBanner}
No projects in this mentoring round yet. {!importBanner && ( <> {' '}Use{' '} Add Project to Round {' '} to populate it. )}
) } const Pill = ({ value, label, count, }: { value: Filter label: string count: number }) => ( ) return (
{importBanner}
setSearch(e.target.value)} placeholder="Search projects, teams, or mentors…" className="pl-8" />
{selected.size > 0 ? (
{selected.size}{' '} project{selected.size === 1 ? '' : 's'} selected
) : (
Tip: tick checkboxes to bulk-assign one mentor to multiple projects in a single click (mentor gets one combined email). {totals.unassigned > 0 && ( )}
)}
0 && filtered.every((p) => selected.has(p.id)) } onCheckedChange={(checked) => { setSelected((prev) => { const next = new Set(prev) if (checked) { filtered.forEach((p) => next.add(p.id)) } else { filtered.forEach((p) => next.delete(p.id)) } return next }) }} aria-label="Select all visible" /> Project Wants? Mentors Action {filtered.length === 0 ? ( No projects match the current filter. ) : ( filtered.map((p) => ( setSelected((prev) => { const next = new Set(prev) if (checked) next.add(p.id) else next.delete(p.id) return next }) } aria-label={`Select ${p.title}`} />
{p.title}
{p.teamName ?? '—'} {p.country && ( <> {' · '} )}
{p.wantsMentorship ? ( Requested ) : ( No )} {p.finalistConfirmationStatus !== 'CONFIRMED' && ( {p.finalistConfirmationStatus ? p.finalistConfirmationStatus.toLowerCase() : 'no confirmation'} )}
{p.mentors.length === 0 ? ( Unassigned ) : (
{p.mentors.map((m) => ( {(m.method === 'AI_AUTO' || m.method === 'AI_SUGGESTED') && ( )} {m.name ?? m.email} ))}
)}
)) )}
{ if (!next) { setBulkOpen(false) setChosenMentorIds(new Set()) setMentorSearch('') } }} > Assign mentors to {selected.size} project {selected.size === 1 ? '' : 's'} Tick any number of mentors. Each chosen mentor will be added to every selected project they aren't already on. Each mentor receives one combined email; each team receives one intro email listing all of their mentors.
{(() => { const allMentors = mentorPool?.mentors ?? [] const chosenMentors = allMentors.filter((m) => chosenMentorIds.has(m.id), ) const upperBound = chosenMentorIds.size * selected.size return ( <> {chosenMentors.length > 0 && (
{chosenMentors.map((m) => ( {m.name ?? m.email} ))}
)}
setMentorSearch(e.target.value)} placeholder="Search mentor by name, email, country, or expertise…" className="pl-8" />
{(() => { const q = mentorSearch.trim().toLowerCase() const filteredMentors = q ? allMentors.filter((m) => [ m.name ?? '', m.email, m.country ?? '', ...(m.expertiseTags ?? []), ] .join(' ') .toLowerCase() .includes(q), ) : allMentors if (allMentors.length === 0) { return (

No mentors in the pool yet.{' '} Add mentors .

) } if (filteredMentors.length === 0) { return (

No mentors match “{mentorSearch}”.

) } return filteredMentors.map((m) => { const isChosen = chosenMentorIds.has(m.id) return ( ) }) })()}
{chosenMentorIds.size > 0 && (

Will create up to{' '} {upperBound} {' '} assignment{upperBound === 1 ? '' : 's'} ( {chosenMentorIds.size} mentor {chosenMentorIds.size === 1 ? '' : 's'} × {selected.size}{' '} project{selected.size === 1 ? '' : 's'}). Pairs that already exist are skipped.

)} ) })()}
) }