feat(mentor): mentoring-specific Round Overview card grid (§B)

Renders above Round Details when round.roundType === 'MENTORING':
  - Top-line counts: requested + assigned (with awaiting badge)
  - Request window: countdown pill (amber <48h, red <12h)
  - Mentor pool: size + avg load + 'View all' link to /admin/mentors
  - Workspace activity: msgs / files / milestones / last activity

Round Details panel now shows 'Mentor Pool: N members' (linked) instead
of an always-empty 'Jury Group' row on MENTORING rounds.

Plan: docs/superpowers/plans/2026-04-28-pr5-mentor-round-overview.md
This commit is contained in:
Matt
2026-04-28 15:26:31 +02:00
parent f9bffabf05
commit a0a2c5f06a
2 changed files with 232 additions and 1 deletions

View File

@@ -0,0 +1,217 @@
'use client'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import {
ArrowRight,
Clock,
FileText,
MessageCircle,
Target,
UserCheck,
Users,
} from 'lucide-react'
interface Props {
roundId: string
}
function formatRelativeFuture(date: Date | null): { label: string; tone: 'normal' | 'amber' | 'red' } {
if (!date) return { label: '—', tone: 'normal' }
const ms = date.getTime() - Date.now()
if (ms <= 0) return { label: 'Closed', tone: 'red' }
const hours = Math.floor(ms / 3_600_000)
const days = Math.floor(hours / 24)
const tone: 'normal' | 'amber' | 'red' =
hours <= 12 ? 'red' : hours <= 48 ? 'amber' : 'normal'
const label = days > 0 ? `Closes in ${days}d` : `Closes in ${hours}h`
return { label, tone }
}
function formatRelativePast(date: Date | null): string {
if (!date) return '—'
const ms = Date.now() - date.getTime()
const minutes = Math.floor(ms / 60_000)
const hours = Math.floor(ms / 3_600_000)
const days = Math.floor(ms / 86_400_000)
if (days > 0) return `${days}d ago`
if (hours > 0) return `${hours}h ago`
return `${Math.max(0, minutes)}m ago`
}
export function MentoringRoundOverview({ roundId }: Props) {
const { data: stats, isLoading: statsLoading } = trpc.mentor.getRoundStats.useQuery(
{ roundId },
{ refetchInterval: 30_000 },
)
const { data: pool, isLoading: poolLoading } = trpc.mentor.getMentorPool.useQuery({})
if (statsLoading || poolLoading) {
return (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-32 w-full rounded-md" />
))}
</div>
)
}
if (!stats || !pool) return null
const requestedPct = stats.totalProjects
? Math.round((stats.requestedCount / stats.totalProjects) * 100)
: 0
const assignedPct = stats.totalProjects
? Math.round((stats.assignedCount / stats.totalProjects) * 100)
: 0
const window = formatRelativeFuture(
stats.requestWindow.deadline ? new Date(stats.requestWindow.deadline) : null,
)
const avgLoad =
pool.poolSize > 0 ? (pool.totalCurrentAssignments / pool.poolSize).toFixed(1) : '—'
const lastActivity = stats.workspaceActivity.lastActivityAt
? new Date(stats.workspaceActivity.lastActivityAt)
: null
return (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
Requested mentoring
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold tabular-nums">
{stats.requestedCount}
<span className="text-muted-foreground ml-2 text-sm font-normal">
/ {stats.totalProjects}
</span>
</div>
<p className="text-muted-foreground mt-1 text-xs">{requestedPct}% of round</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
Mentor assigned
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-2">
<div className="text-3xl font-bold tabular-nums">{stats.assignedCount}</div>
<UserCheck className="text-muted-foreground h-5 w-5" />
</div>
<p className="text-muted-foreground mt-1 text-xs">
{assignedPct}% of round{' '}
{stats.awaitingAssignment > 0 && (
<span className="text-amber-700 dark:text-amber-400">
· {stats.awaitingAssignment} awaiting
</span>
)}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
Request window
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-2">
<Clock className="text-muted-foreground h-5 w-5" />
<Badge
variant={
window.tone === 'red'
? 'destructive'
: window.tone === 'amber'
? 'secondary'
: 'outline'
}
className="text-sm"
>
{window.label}
</Badge>
</div>
<p className="text-muted-foreground mt-2 text-xs">
{stats.requestWindow.deadline
? `Closes ${new Date(stats.requestWindow.deadline).toLocaleDateString()} · ${stats.requestWindow.deadlineDays}-day window`
: 'No window deadline set'}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
Mentor pool
</CardTitle>
<Link
href="/admin/mentors"
className="text-muted-foreground hover:text-foreground inline-flex items-center text-xs"
>
View all
<ArrowRight className="ml-0.5 h-3 w-3" />
</Link>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-2">
<div className="text-3xl font-bold tabular-nums">{pool.poolSize}</div>
<Users className="text-muted-foreground h-5 w-5" />
</div>
<p className="text-muted-foreground mt-1 text-xs">
Avg load <span className="text-foreground font-medium">{avgLoad}</span> ·{' '}
{pool.totalCurrentAssignments} active
</p>
</CardContent>
</Card>
<Card className="md:col-span-2 xl:col-span-4">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Workspace activity</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm md:grid-cols-4">
<div className="flex items-center gap-2">
<MessageCircle className="text-muted-foreground h-4 w-4" />
<div>
<div className="font-bold tabular-nums">{stats.workspaceActivity.messageCount}</div>
<div className="text-muted-foreground text-xs">messages</div>
</div>
</div>
<div className="flex items-center gap-2">
<FileText className="text-muted-foreground h-4 w-4" />
<div>
<div className="font-bold tabular-nums">{stats.workspaceActivity.fileCount}</div>
<div className="text-muted-foreground text-xs">files</div>
</div>
</div>
<div className="flex items-center gap-2">
<Target className="text-muted-foreground h-4 w-4" />
<div>
<div className="font-bold tabular-nums">
{stats.workspaceActivity.milestoneCount}
</div>
<div className="text-muted-foreground text-xs">milestones</div>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="text-muted-foreground h-4 w-4" />
<div>
<div className="font-medium">{formatRelativePast(lastActivity)}</div>
<div className="text-muted-foreground text-xs">last activity</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}