merge: PR8 Task 8 — admin multi-mentor UI + change-request inbox
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Clock,
|
Clock,
|
||||||
FileText,
|
FileText,
|
||||||
|
Inbox,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
Target,
|
Target,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
@@ -48,6 +49,10 @@ export function MentoringRoundOverview({ roundId }: Props) {
|
|||||||
{ refetchInterval: 30_000 },
|
{ refetchInterval: 30_000 },
|
||||||
)
|
)
|
||||||
const { data: pool, isLoading: poolLoading } = trpc.mentor.getMentorPool.useQuery({})
|
const { data: pool, isLoading: poolLoading } = trpc.mentor.getMentorPool.useQuery({})
|
||||||
|
const { data: pendingChangeRequests } = trpc.mentor.listChangeRequests.useQuery(
|
||||||
|
{ status: 'PENDING' },
|
||||||
|
{ refetchInterval: 30_000 },
|
||||||
|
)
|
||||||
|
|
||||||
if (statsLoading || poolLoading) {
|
if (statsLoading || poolLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -60,6 +65,15 @@ export function MentoringRoundOverview({ roundId }: Props) {
|
|||||||
}
|
}
|
||||||
if (!stats || !pool) return null
|
if (!stats || !pool) return null
|
||||||
|
|
||||||
|
const pendingCount = pendingChangeRequests?.length ?? 0
|
||||||
|
// If there's at least one pending request, deep-link directly into the
|
||||||
|
// first one's project (admins can resolve / view siblings from there).
|
||||||
|
// Otherwise the card stays static.
|
||||||
|
const firstPendingProjectId = pendingChangeRequests?.[0]?.project.id ?? null
|
||||||
|
const changeRequestsHref = firstPendingProjectId
|
||||||
|
? `/admin/projects/${firstPendingProjectId}/mentor`
|
||||||
|
: null
|
||||||
|
|
||||||
const requestedPct = stats.totalProjects
|
const requestedPct = stats.totalProjects
|
||||||
? Math.round((stats.requestedCount / stats.totalProjects) * 100)
|
? Math.round((stats.requestedCount / stats.totalProjects) * 100)
|
||||||
: 0
|
: 0
|
||||||
@@ -173,6 +187,42 @@ export function MentoringRoundOverview({ roundId }: Props) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={`md:col-span-2 xl:col-span-4 ${
|
||||||
|
pendingCount > 0 ? 'border-amber-300 dark:border-amber-700' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardContent className="flex items-center justify-between py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Inbox
|
||||||
|
className={`h-5 w-5 ${
|
||||||
|
pendingCount > 0 ? 'text-amber-600' : 'text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">Pending change requests</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
Team members asking admin to swap a mentor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-2xl font-bold tabular-nums">{pendingCount}</div>
|
||||||
|
{changeRequestsHref ? (
|
||||||
|
<Link
|
||||||
|
href={changeRequestsHref}
|
||||||
|
className="text-muted-foreground hover:text-foreground inline-flex items-center text-xs"
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
<ArrowRight className="ml-0.5 h-3 w-3" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-xs">All clear</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card className="md:col-span-2 xl:col-span-4">
|
<Card className="md:col-span-2 xl:col-span-4">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm">Workspace activity</CardTitle>
|
<CardTitle className="text-sm">Workspace activity</CardTitle>
|
||||||
|
|||||||
@@ -565,27 +565,45 @@ export const mentorRouter = router({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove mentor assignment
|
* Remove mentor assignment.
|
||||||
|
*
|
||||||
|
* Multi-mentor (PR8): callers should pass `assignmentId` to target a
|
||||||
|
* specific co-mentor. Legacy callers passing only `projectId` get the
|
||||||
|
* most-recent assignment removed (kept for backward compatibility).
|
||||||
*/
|
*/
|
||||||
unassign: adminProcedure
|
unassign: adminProcedure
|
||||||
.input(z.object({ projectId: z.string() }))
|
.input(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
assignmentId: z.string().optional(),
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine((v) => !!v.assignmentId || !!v.projectId, {
|
||||||
|
message: 'Either assignmentId or projectId is required',
|
||||||
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
// TODO(PR8 Task 8): admin UI should specify which mentor to drop when
|
const assignment = input.assignmentId
|
||||||
// multiple are assigned. Legacy callers pass only projectId — we resolve
|
? await ctx.prisma.mentorAssignment.findUnique({
|
||||||
// to the most-recent assignment for backward compatibility.
|
where: { id: input.assignmentId },
|
||||||
const assignment = await ctx.prisma.mentorAssignment.findFirst({
|
include: {
|
||||||
where: { projectId: input.projectId },
|
mentor: { select: { id: true, name: true } },
|
||||||
orderBy: { assignedAt: 'desc' },
|
project: { select: { id: true, title: true } },
|
||||||
include: {
|
},
|
||||||
mentor: { select: { id: true, name: true } },
|
})
|
||||||
project: { select: { id: true, title: true } },
|
: await ctx.prisma.mentorAssignment.findFirst({
|
||||||
},
|
where: { projectId: input.projectId! },
|
||||||
})
|
orderBy: { assignedAt: 'desc' },
|
||||||
|
include: {
|
||||||
|
mentor: { select: { id: true, name: true } },
|
||||||
|
project: { select: { id: true, title: true } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (!assignment) {
|
if (!assignment) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
message: 'No mentor assignment found for this project',
|
message: 'No mentor assignment found',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +620,7 @@ export const mentorRouter = router({
|
|||||||
entityType: 'MentorAssignment',
|
entityType: 'MentorAssignment',
|
||||||
entityId: assignment.id,
|
entityId: assignment.id,
|
||||||
detailsJson: {
|
detailsJson: {
|
||||||
projectId: input.projectId,
|
projectId: assignment.project.id,
|
||||||
projectTitle: assignment.project.title,
|
projectTitle: assignment.project.title,
|
||||||
mentorId: assignment.mentor.id,
|
mentorId: assignment.mentor.id,
|
||||||
mentorName: assignment.mentor.name,
|
mentorName: assignment.mentor.name,
|
||||||
|
|||||||
Reference in New Issue
Block a user