Multi-role members, round detail UI overhaul, dashboard jury progress, and submit bug fix
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Add roles UserRole[] to User model with migration + backfill from existing role column - Update auth JWT/session to propagate roles array with [role] fallback for stale tokens - Update tRPC hasRole() middleware and add userHasRole() helper for inline role checks - Update ~15 router inline checks and ~13 DB queries to use roles array - Add updateRoles admin mutation with SUPER_ADMIN guard and priority-based primary role - Add role switcher UI in admin sidebar and role-nav for multi-role users - Remove redundant stats cards from round detail, add window dates to header banner - Merge Members section into JuryProgressTable with inline cap editor and remove buttons - Reorder round detail assignments tab: Progress > Score Dist > Assignments > Coverage > Jury Group - Make score distribution fill full vertical height, reassignment history always open - Add per-juror progress bars to admin dashboard ActiveRoundPanel for EVALUATION rounds - Fix evaluation submit bug: use isSubmitting state instead of startMutation.isPending Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { motion } from 'motion/react'
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
import { StatusBadge } from '@/components/shared/status-badge'
|
||||
import { cn, formatEnumLabel, daysUntil } from '@/lib/utils'
|
||||
import { roundTypeConfig, projectStateConfig } from '@/lib/round-config'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
|
||||
export type PipelineRound = {
|
||||
id: string
|
||||
@@ -138,6 +141,80 @@ function ProjectStateBar({
|
||||
)
|
||||
}
|
||||
|
||||
function EvaluationRoundContent({ round }: { round: PipelineRound }) {
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
|
||||
const { data: workload, isLoading: isLoadingWorkload } = trpc.analytics.getJurorWorkload.useQuery(
|
||||
{ roundId: round.id },
|
||||
{ enabled: round.roundType === 'EVALUATION' }
|
||||
)
|
||||
|
||||
const pct =
|
||||
round.evalTotal > 0
|
||||
? Math.round((round.evalSubmitted / round.evalTotal) * 100)
|
||||
: 0
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Evaluation progress</span>
|
||||
<span className="font-medium">
|
||||
{round.evalSubmitted} / {round.evalTotal} ({pct}%)
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={pct} gradient />
|
||||
{round.evalDraft > 0 && (
|
||||
<p className="text-xs text-amber-600">
|
||||
{round.evalDraft} draft{round.evalDraft !== 1 ? 's' : ''} in progress
|
||||
</p>
|
||||
)}
|
||||
{/* Per-juror progress */}
|
||||
<div className="mt-3 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">Jury Progress</span>
|
||||
{workload && workload.length > 8 && (
|
||||
<button
|
||||
onClick={() => setShowAll(!showAll)}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{showAll ? 'Show less' : `Show all (${workload.length})`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isLoadingWorkload ? (
|
||||
<div className="space-y-1">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-4 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : workload && workload.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{(showAll ? workload : workload.slice(0, 8)).map((juror) => {
|
||||
const pct = juror.assigned > 0 ? (juror.completed / juror.assigned) * 100 : 0
|
||||
return (
|
||||
<div key={juror.id} className="flex items-center gap-2">
|
||||
<span className="max-w-[140px] truncate text-xs">{juror.name}</span>
|
||||
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="whitespace-nowrap text-xs text-muted-foreground">
|
||||
{juror.completed}/{juror.assigned}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">No jurors assigned yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RoundTypeContent({ round }: { round: PipelineRound }) {
|
||||
const { projectStates } = round
|
||||
|
||||
@@ -171,29 +248,8 @@ function RoundTypeContent({ round }: { round: PipelineRound }) {
|
||||
)
|
||||
}
|
||||
|
||||
case 'EVALUATION': {
|
||||
const pct =
|
||||
round.evalTotal > 0
|
||||
? Math.round((round.evalSubmitted / round.evalTotal) * 100)
|
||||
: 0
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Evaluation progress</span>
|
||||
<span className="font-medium">
|
||||
{round.evalSubmitted} / {round.evalTotal} ({pct}%)
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={pct} gradient />
|
||||
{round.evalDraft > 0 && (
|
||||
<p className="text-xs text-amber-600">
|
||||
{round.evalDraft} draft{round.evalDraft !== 1 ? 's' : ''} in progress
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
case 'EVALUATION':
|
||||
return <EvaluationRoundContent round={round} />
|
||||
|
||||
case 'SUBMISSION':
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user