From 2b07c12c1889b147599b8a1e7103571a80948563 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Apr 2026 14:58:32 +0200 Subject: [PATCH] =?UTF-8?q?feat(mentor):=20round-level=20auto-fill=20toolb?= =?UTF-8?q?ar=20on=20Projects=20tab=20(=C2=A7C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an 'Auto-fill remaining' button above ProjectStatesTable on the MENTORING round Projects tab. Calls mentor.autoAssignBulkForRound, respecting the round's configJson.eligibility: - requested_only / all_advancing: enabled, count from new round.getProjectsNeedingMentor query - admin_selected: disabled with explanatory copy Plan: docs/superpowers/plans/2026-04-28-pr4-mentor-assignment-ux.md --- .../(admin)/admin/rounds/[roundId]/page.tsx | 70 +++++++++++++++++++ src/server/routers/round.ts | 29 ++++++++ 2 files changed, 99 insertions(+) diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 78b4632..c66d2ec 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -145,6 +145,73 @@ const stateColors: Record = Object.fromEntries( Object.entries(projectStateConfig).map(([k, v]) => [k, v.bg]) ) +// ═══════════════════════════════════════════════════════════════════════════ +// Mentoring round: Auto-fill remaining toolbar (Projects tab) +// ═══════════════════════════════════════════════════════════════════════════ + +function MentoringBulkAssignToolbar({ + roundId, + configJson, +}: { + roundId: string + configJson: Record +}) { + const utils = trpc.useUtils() + const eligibility = (configJson.eligibility as string) ?? 'requested_only' + const isAdminSelected = eligibility === 'admin_selected' + + const { data: pending } = trpc.round.getProjectsNeedingMentor.useQuery( + { roundId }, + { enabled: !isAdminSelected, refetchInterval: 30_000 }, + ) + const count = pending?.count ?? 0 + + const bulk = trpc.mentor.autoAssignBulkForRound.useMutation({ + onSuccess: (result) => { + toast.success(result.message) + utils.round.getProjectsNeedingMentor.invalidate({ roundId }) + utils.project.list.invalidate() + }, + onError: (err) => toast.error(err.message), + }) + + const eligibilityLabel = eligibility.replace('_', ' ') + + return ( +
+
+ {isAdminSelected ? ( + <> + Eligibility: admin-selected + + — auto-fill is disabled. Assign each project manually. + + + ) : count > 0 ? ( + <> + {count}{' '} + + project{count === 1 ? '' : 's'} eligible for auto-fill ({eligibilityLabel}) + + + ) : ( + + All eligible projects have a mentor. + + )} +
+ +
+ ) +} + // ═══════════════════════════════════════════════════════════════════════════ // Main Page Component // ═══════════════════════════════════════════════════════════════════════════ @@ -1477,6 +1544,9 @@ export default function RoundDetailPage() { {/* ═══════════ PROJECTS TAB ═══════════ */} + {isMentoring && ( + + )} { + const round = await ctx.prisma.round.findUniqueOrThrow({ + where: { id: input.roundId }, + select: { roundType: true, configJson: true }, + }) + if (round.roundType !== 'MENTORING') return { count: 0 } + const config = (round.configJson ?? {}) as Record + const eligibility = (config.eligibility as string) ?? 'requested_only' + if (eligibility === 'admin_selected') return { count: 0 } + + const count = await ctx.prisma.projectRoundState.count({ + where: { + roundId: input.roundId, + project: { + mentorAssignment: null, + ...(eligibility === 'requested_only' ? { wantsMentorship: true } : {}), + }, + }, + }) + return { count } + }), + /** * Delete a round */