Files
MOPC-Portal/src/components/admin/round/mentoring-projects-table.tsx

598 lines
21 KiB
TypeScript
Raw Normal View History

fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
'use client'
import { useMemo, useState } from 'react'
import Link from 'next/link'
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
import { toast } from 'sonner'
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
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'
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
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'
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
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<Filter>('all')
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
const [selected, setSelected] = useState<Set<string>>(new Set())
const [bulkOpen, setBulkOpen] = useState(false)
const [chosenMentorId, setChosenMentorId] = useState<string>('')
const [mentorSearch, setMentorSearch] = useState('')
const utils = trpc.useUtils()
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
const { data, isLoading } = trpc.round.listMentoringProjects.useQuery(
{ roundId },
{ refetchInterval: 30_000 },
)
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
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.assignedCount === 0 && result.skippedCount > 0) {
toast.info(
`No new assignments — the selected mentor is already on all ${result.skippedCount} project${result.skippedCount === 1 ? '' : 's'}.`,
)
} else {
toast.success(
`Assigned mentor to ${result.assignedCount} project${
result.assignedCount === 1 ? '' : 's'
}${result.skippedCount > 0 ? ` (${result.skippedCount} already had this mentor)` : ''}${
result.emailSent ? ' · email sent' : ''
}`,
)
}
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())
setChosenMentorId('')
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 && (
<div className="flex flex-col gap-2 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<div className="text-amber-900">
<span className="font-medium">
{importCandidates.pendingCount} PASSED project
{importCandidates.pendingCount === 1 ? '' : 's'}
</span>{' '}
from{' '}
<span className="font-medium">
{importCandidates.priorRound.name}
</span>{' '}
{importCandidates.pendingCount === 1 ? "isn't" : "aren't"} in this
mentoring round yet.
</div>
<Button
size="sm"
onClick={() =>
advanceMutation.mutate({
roundId: importCandidates.priorRound!.id,
targetRoundId: roundId,
})
}
disabled={advanceMutation.isPending}
>
{advanceMutation.isPending ? (
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
) : (
<Download className="mr-1.5 h-4 w-4" />
)}
Import {importCandidates.pendingCount}
</Button>
</div>
)
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
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 (
<div className="space-y-2">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-14 w-full" />
))}
</div>
)
}
if (!data || data.projects.length === 0) {
return (
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
<div className="space-y-3">
{importBanner}
<div className="rounded-md border bg-muted/30 px-4 py-12 text-center text-sm text-muted-foreground">
No projects in this mentoring round yet.
{!importBanner && (
<>
{' '}Use{' '}
<span className="font-medium text-foreground">
Add Project to Round
</span>{' '}
to populate it.
</>
)}
</div>
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
</div>
)
}
const Pill = ({
value,
label,
count,
}: {
value: Filter
label: string
count: number
}) => (
<button
type="button"
onClick={() => setFilter(value)}
className={`rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
filter === value
? 'border-primary bg-primary text-primary-foreground'
: 'bg-background hover:bg-muted'
}`}
>
{label}{' '}
<span className="tabular-nums opacity-80">({count})</span>
</button>
)
return (
<div className="space-y-3">
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
{importBanner}
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap gap-1.5">
<Pill value="all" label="All" count={totals.total} />
<Pill value="unassigned" label="No mentor" count={totals.unassigned} />
<Pill value="assigned" label="Has mentor" count={totals.assigned} />
<Pill value="wants_only" label="Wants mentorship" count={totals.wants} />
</div>
<div className="relative w-full sm:max-w-xs">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search projects, teams, or mentors…"
className="pl-8"
/>
</div>
</div>
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
{selected.size > 0 ? (
<div className="flex flex-col gap-2 rounded-md border border-primary/30 bg-primary/5 px-4 py-2.5 text-sm sm:flex-row sm:items-center sm:justify-between">
<div>
<span className="font-medium">{selected.size}</span>{' '}
<span className="text-muted-foreground">
project{selected.size === 1 ? '' : 's'} selected
</span>
</div>
<div className="flex flex-wrap gap-2">
<Button size="sm" onClick={() => setBulkOpen(true)}>
<UserPlus className="mr-1.5 h-4 w-4" />
Assign mentor
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setSelected(new Set())}
>
<X className="mr-1 h-4 w-4" />
Clear
</Button>
</div>
</div>
) : (
<div className="flex items-center justify-between rounded-md border border-dashed bg-muted/20 px-4 py-2 text-xs text-muted-foreground">
<span>
Tip: tick checkboxes to bulk-assign one mentor to multiple
projects in a single click (mentor gets one combined email).
</span>
{totals.unassigned > 0 && (
<button
type="button"
className="text-xs font-medium text-foreground hover:underline"
onClick={() => {
setFilter('unassigned')
setSelected(
new Set(
data.projects
.filter((p) => p.mentors.length === 0)
.map((p) => p.id),
),
)
}}
>
Select all {totals.unassigned} without a mentor
</button>
)}
</div>
)}
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
<TableRow>
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
<TableHead className="w-10">
<Checkbox
checked={
filtered.length > 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"
/>
</TableHead>
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
<TableHead>Project</TableHead>
<TableHead>Wants?</TableHead>
<TableHead>Mentors</TableHead>
<TableHead className="w-32 text-right">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.length === 0 ? (
<TableRow>
<TableCell
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
colSpan={5}
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
className="py-8 text-center text-sm text-muted-foreground"
>
No projects match the current filter.
</TableCell>
</TableRow>
) : (
filtered.map((p) => (
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
<TableRow
key={p.id}
data-state={selected.has(p.id) ? 'selected' : undefined}
>
<TableCell>
<Checkbox
checked={selected.has(p.id)}
onCheckedChange={(checked) =>
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}`}
/>
</TableCell>
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
<TableCell>
<div className="font-medium">{p.title}</div>
<div className="text-xs text-muted-foreground">
{p.teamName ?? '—'}
{p.country && (
<>
{' · '}
<CountryDisplay country={p.country} />
</>
)}
</div>
</TableCell>
<TableCell>
<div className="flex flex-col gap-1">
{p.wantsMentorship ? (
<Badge variant="secondary" className="w-fit text-xs">
Requested
</Badge>
) : (
<span className="text-xs text-muted-foreground">No</span>
)}
{p.finalistConfirmationStatus !== 'CONFIRMED' && (
<span
className="text-[10px] uppercase tracking-wide text-amber-700"
title="Auto-fill skips projects whose team has not confirmed attendance."
>
{p.finalistConfirmationStatus
? p.finalistConfirmationStatus.toLowerCase()
: 'no confirmation'}
</span>
)}
</div>
</TableCell>
<TableCell>
{p.mentors.length === 0 ? (
<span className="text-xs italic text-muted-foreground">
Unassigned
</span>
) : (
<div className="flex flex-wrap gap-1">
{p.mentors.map((m) => (
<Badge
key={m.assignmentId}
variant="outline"
className="gap-1 text-xs"
title={m.email}
>
{(m.method === 'AI_AUTO' ||
m.method === 'AI_SUGGESTED') && (
<Sparkles className="h-3 w-3 text-amber-500" />
)}
{m.name ?? m.email}
</Badge>
))}
</div>
)}
</TableCell>
<TableCell className="text-right">
<Button asChild size="sm" variant="outline">
<Link href={`/admin/projects/${p.id}/mentor`}>
{p.mentors.length === 0 ? (
<>
<UserPlus className="mr-1 h-3.5 w-3.5" />
Assign
</>
) : (
<>
Open
<ArrowRight className="ml-1 h-3.5 w-3.5" />
</>
)}
</Link>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
feat(mentor): bulk assignment + coalesced emails + team intros on round open Round-page bulk-assign UI - Checkboxes on every project row, header select-all, primary-tinted action toolbar that appears when 1+ rows are selected with an "Assign mentor…" CTA and Clear. Dialog lists the mentor pool with search (name/email/ country/expertise), load indicator, and a radio picker. - Always-visible tip strip when nothing is selected explains the bulk flow and offers a one-click "Select all N without a mentor" shortcut. - New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns one mentor to many projects in a transaction; idempotent on the per-pair `(projectId, mentorId)` unique; per-project in-app notifications still fire for each team. - Mutation invalidates listMentoringProjects, getProjectsNeedingMentor, getMentoringImportCandidates, getMentorPool, getRoundStats, project.list so the page reflects the new state without a refresh. Coalesced mentor emails - New `sendMentorBulkAssignmentEmail` (single email listing every newly- assigned project + workspace links) used by `mentor.bulkAssign` and `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow now emails mentors at the end of the batch, one combined email per mentor regardless of how many projects they received. Team introduction emails when the round opens - New `sendTeamMentorIntroductionEmail` lists every assigned mentor with name + email and a link to the workspace, so teams can reach out directly. - `activateRound` (round-engine) fires the introduction for every project in a MENTORING round that has active mentors when the round opens. - `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also fire the introduction immediately when the project's MENTORING round is already ROUND_ACTIVE — so mentors added mid-round still reach the team. - Idempotency via the new `MentorAssignment.teamIntroducedAt` column (migration 20260526114936) — independent from `notificationSentAt` so pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
<Dialog
open={bulkOpen}
onOpenChange={(next) => {
if (!next) {
setBulkOpen(false)
setChosenMentorId('')
setMentorSearch('')
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
Assign mentor to {selected.size} project
{selected.size === 1 ? '' : 's'}
</DialogTitle>
<DialogDescription>
Choose one mentor they'll receive a single email listing every
new assignment. Projects where they're already an active mentor
will be skipped.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={mentorSearch}
onChange={(e) => setMentorSearch(e.target.value)}
placeholder="Search mentor by name, email, country, or expertise…"
className="pl-8"
/>
</div>
<div className="max-h-72 overflow-y-auto rounded-md border">
{(() => {
const mentors = mentorPool?.mentors ?? []
const q = mentorSearch.trim().toLowerCase()
const filteredMentors = q
? mentors.filter((m) =>
[
m.name ?? '',
m.email,
m.country ?? '',
...(m.expertiseTags ?? []),
]
.join(' ')
.toLowerCase()
.includes(q),
)
: mentors
if (mentors.length === 0) {
return (
<p className="p-4 text-center text-sm text-muted-foreground">
No mentors in the pool yet.{' '}
<Link
href="/admin/members?tab=mentors"
className="underline-offset-2 hover:underline"
>
Add mentors
</Link>
.
</p>
)
}
if (filteredMentors.length === 0) {
return (
<p className="p-4 text-center text-sm text-muted-foreground">
No mentors match &ldquo;{mentorSearch}&rdquo;.
</p>
)
}
return filteredMentors.map((m) => {
const isChosen = chosenMentorId === m.id
return (
<label
key={m.id}
className={`flex cursor-pointer items-start gap-3 border-b px-3 py-2 text-sm last:border-b-0 ${
isChosen ? 'bg-accent' : 'hover:bg-muted/50'
}`}
>
<input
type="radio"
name="bulk-mentor"
className="mt-1"
checked={isChosen}
onChange={() => setChosenMentorId(m.id)}
/>
<div className="min-w-0 flex-1">
<div className="font-medium">
{m.name ?? 'Unnamed'}
</div>
<div className="truncate text-xs text-muted-foreground">
{m.email}
{m.country && <> · {m.country}</>}
</div>
{m.expertiseTags && m.expertiseTags.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{m.expertiseTags.slice(0, 4).map((t) => (
<Badge
key={t}
variant="secondary"
className="text-[10px]"
>
{t}
</Badge>
))}
{m.expertiseTags.length > 4 && (
<Badge variant="outline" className="text-[10px]">
+{m.expertiseTags.length - 4}
</Badge>
)}
</div>
)}
</div>
<div className="shrink-0 text-right text-xs tabular-nums text-muted-foreground">
{m.currentAssignments}
{m.maxAssignments != null && `/${m.maxAssignments}`}{' '}
load
</div>
</label>
)
})
})()}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setBulkOpen(false)
setChosenMentorId('')
setMentorSearch('')
}}
>
Cancel
</Button>
<Button
onClick={() =>
bulkAssignMutation.mutate({
mentorId: chosenMentorId,
projectIds: Array.from(selected),
})
}
disabled={!chosenMentorId || bulkAssignMutation.isPending}
>
{bulkAssignMutation.isPending && (
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
)}
Assign to {selected.size} project
{selected.size === 1 ? '' : 's'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
fix(mentor): unbreak the mentor pipeline end-to-end Adding the MENTOR role from /admin/members/[id] only updated React state — the AlertDialog "Add role" confirmation never called the server, so prod ended up with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet". The dialog now awaits updateUser.mutateAsync({ roles }) before closing. Other corrections in the same area: - DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall modals (e.g. Add Project to Round) scroll internally instead of overflowing past their own rounded background. - getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both filter mentorAssignments by droppedAt: null and require finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what auto-fill actually processes. The toolbar surfaces hasNoMentors / hasNoEligible / count / all-assigned as distinct states instead of one misleading "All eligible projects have a mentor" line. - New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on the Projects tab of MENTORING rounds. Lists every project with its active mentors (multi-mentor aware), filter pills, search, finalist-confirmation badge, and a per-row link to /admin/projects/[id]/mentor for assigning. - Applicant team page now lists ALL active mentors (PR8 Task 7) instead of just mentorAssignments[0]. - Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test or VITEST=true so test runs can never emit real notifications again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
</div>
)
}