feat(mentor): round-level auto-fill toolbar on Projects tab (§C)
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
This commit is contained in:
@@ -145,6 +145,73 @@ const stateColors: Record<string, string> = Object.fromEntries(
|
|||||||
Object.entries(projectStateConfig).map(([k, v]) => [k, v.bg])
|
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<string, unknown>
|
||||||
|
}) {
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center justify-between rounded-md border bg-muted/30 px-4 py-2.5">
|
||||||
|
<div className="text-sm">
|
||||||
|
{isAdminSelected ? (
|
||||||
|
<>
|
||||||
|
<span className="font-medium">Eligibility: admin-selected</span>
|
||||||
|
<span className="text-muted-foreground ml-2">
|
||||||
|
— auto-fill is disabled. Assign each project manually.
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : count > 0 ? (
|
||||||
|
<>
|
||||||
|
<span className="font-medium">{count}</span>{' '}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
project{count === 1 ? '' : 's'} eligible for auto-fill ({eligibilityLabel})
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
All eligible projects have a mentor.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => bulk.mutate({ roundId })}
|
||||||
|
disabled={isAdminSelected || count === 0 || bulk.isPending}
|
||||||
|
>
|
||||||
|
{bulk.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||||
|
Auto-fill remaining
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// Main Page Component
|
// Main Page Component
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -1477,6 +1544,9 @@ export default function RoundDetailPage() {
|
|||||||
|
|
||||||
{/* ═══════════ PROJECTS TAB ═══════════ */}
|
{/* ═══════════ PROJECTS TAB ═══════════ */}
|
||||||
<TabsContent value="projects" className="space-y-4">
|
<TabsContent value="projects" className="space-y-4">
|
||||||
|
{isMentoring && (
|
||||||
|
<MentoringBulkAssignToolbar roundId={roundId} configJson={config} />
|
||||||
|
)}
|
||||||
<ProjectStatesTable
|
<ProjectStatesTable
|
||||||
competitionId={competitionId}
|
competitionId={competitionId}
|
||||||
roundId={roundId}
|
roundId={roundId}
|
||||||
|
|||||||
@@ -215,6 +215,35 @@ export const roundRouter = router({
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count projects in a MENTORING round eligible for mentor auto-fill
|
||||||
|
* (i.e., no mentorAssignment, scoped by configJson.eligibility).
|
||||||
|
* Used by the Auto-fill remaining toolbar on the round Projects tab.
|
||||||
|
*/
|
||||||
|
getProjectsNeedingMentor: adminProcedure
|
||||||
|
.input(z.object({ roundId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
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<string, unknown>
|
||||||
|
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
|
* Delete a round
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user