feat: add juror progress dashboard with evaluation.getMyProgress query

- Add getMyProgress juryProcedure query to evaluationRouter: fetches all
  assignments for the current juror in a round, tallies completed/total,
  advance yes/no counts, per-project numeric scores and averages
- Create JurorProgressDashboard client component with progress bar, advance
  badge summary, and collapsible per-submission score table
- Wire dashboard into jury round page, gated by configJson.showJurorProgressDashboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 15:15:08 +01:00
parent 2a61aa8e08
commit 79bd4dbae7
3 changed files with 221 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
'use client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { ChevronDown, ChevronUp, ThumbsUp, ThumbsDown } from 'lucide-react'
export function JurorProgressDashboard({ roundId }: { roundId: string }) {
const [expanded, setExpanded] = useState(true)
const { data, isLoading } = trpc.evaluation.getMyProgress.useQuery(
{ roundId },
{ refetchInterval: 30_000 },
)
if (isLoading) {
return <Skeleton className="h-32 w-full" />
}
if (!data || data.total === 0) return null
const pct = Math.round((data.completed / data.total) * 100)
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Your Progress</CardTitle>
<Button variant="ghost" size="sm" onClick={() => setExpanded(!expanded)}>
{expanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Progress bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{data.completed} / {data.total} evaluated
</span>
<span className="font-medium">{pct}%</span>
</div>
<Progress value={pct} className="h-2" />
</div>
{/* Advance summary */}
{(data.advanceCounts.yes > 0 || data.advanceCounts.no > 0) && (
<div className="flex items-center gap-3">
<span className="text-sm text-muted-foreground">Advance:</span>
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200">
<ThumbsUp className="mr-1 h-3 w-3" />
{data.advanceCounts.yes} Yes
</Badge>
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
<ThumbsDown className="mr-1 h-3 w-3" />
{data.advanceCounts.no} No
</Badge>
</div>
)}
{/* Submissions table */}
{expanded && data.submissions.length > 0 && (
<div className="border rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/30">
<th className="text-left px-3 py-2 font-medium">Project</th>
<th className="text-center px-3 py-2 font-medium">Avg Score</th>
{data.submissions[0]?.criterionScores.map((cs, i) => (
<th key={i} className="text-center px-2 py-2 font-medium text-xs max-w-[80px] truncate" title={cs.label}>
{cs.label}
</th>
))}
<th className="text-center px-3 py-2 font-medium">Advance</th>
<th className="text-right px-3 py-2 font-medium">Date</th>
</tr>
</thead>
<tbody>
{data.submissions.map((s) => (
<tr key={s.projectId} className="border-b last:border-0 hover:bg-muted/20">
<td className="px-3 py-2 font-medium truncate max-w-[200px]">{s.projectName}</td>
<td className="text-center px-3 py-2">
{s.numericAverage != null ? (
<span className="font-semibold">{s.numericAverage}</span>
) : '—'}
</td>
{s.criterionScores.map((cs, i) => (
<td key={i} className="text-center px-2 py-2 text-muted-foreground">{cs.value}</td>
))}
<td className="text-center px-3 py-2">
{s.advanceDecision === true ? (
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200 text-xs">YES</Badge>
) : s.advanceDecision === false ? (
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200 text-xs">NO</Badge>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
<td className="text-right px-3 py-2 text-muted-foreground text-xs whitespace-nowrap">
{s.submittedAt ? new Date(s.submittedAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</CardContent>
</Card>
)
}