Fix evaluation criteria, jury preferences, assignment config, and dashboard stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
- Fix criteria not showing for jurors: fetch active form independently via
getStageForm query instead of relying on existing evaluation record
- Fix scoringMode default from 'global' to 'criteria' (matching schema)
- Parse scale string format ("1-10") into minScore/maxScore for criteria display
- Fix COI dialog dismissal: prevent outside click on evaluate page Dialog
- Fix requiredReviews hardcoded to 3: read from round configJson in 4 locations
- Add jury preferences banner for unconfirmed caps on jury dashboard
- Add updateJuryPreferences tRPC procedure for self-service cap/ratio
- Simplify onboarding: always show jury step, allow cap up to 50
- Add role/ratio/availability fields to jury member invite dialog
- Simplify jury group settings (keep only defaultMaxAssignments)
- Enforce deliberation showCollectiveRankings flag for non-admin users
- Redesign dashboard stat cards: editorial data strip on mobile,
clean grid layout on desktop (no more generic card pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
import { useState, useMemo, useCallback, useRef } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { toast } from 'sonner'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
@@ -165,7 +166,8 @@ export default function RoundDetailPage() {
|
||||
const roundId = params.roundId as string
|
||||
|
||||
const [config, setConfig] = useState<Record<string, unknown>>({})
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [autosaveStatus, setAutosaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
||||
const pendingSaveRef = useRef(false)
|
||||
const [activeTab, setActiveTab] = useState('overview')
|
||||
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
@@ -215,8 +217,8 @@ export default function RoundDetailPage() {
|
||||
)
|
||||
const roundAwards = awards?.filter((a) => a.evaluationRoundId === roundId) ?? []
|
||||
|
||||
// Sync config from server when not dirty
|
||||
if (round && !hasChanges) {
|
||||
// Sync config from server when no pending save
|
||||
if (round && !pendingSaveRef.current) {
|
||||
const roundConfig = (round.configJson as Record<string, unknown>) ?? {}
|
||||
if (JSON.stringify(roundConfig) !== JSON.stringify(config)) {
|
||||
setConfig(roundConfig)
|
||||
@@ -227,10 +229,15 @@ export default function RoundDetailPage() {
|
||||
const updateMutation = trpc.round.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
toast.success('Round configuration saved')
|
||||
setHasChanges(false)
|
||||
pendingSaveRef.current = false
|
||||
setAutosaveStatus('saved')
|
||||
setTimeout(() => setAutosaveStatus('idle'), 2000)
|
||||
},
|
||||
onError: (err) => {
|
||||
pendingSaveRef.current = false
|
||||
setAutosaveStatus('error')
|
||||
toast.error(err.message)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const activateMutation = trpc.roundEngine.activate.useMutation({
|
||||
@@ -356,14 +363,17 @@ export default function RoundDetailPage() {
|
||||
|
||||
const isTransitioning = activateMutation.isPending || closeMutation.isPending || reopenMutation.isPending || archiveMutation.isPending
|
||||
|
||||
const debouncedSave = useDebouncedCallback((newConfig: Record<string, unknown>) => {
|
||||
setAutosaveStatus('saving')
|
||||
updateMutation.mutate({ id: roundId, configJson: newConfig })
|
||||
}, 1500)
|
||||
|
||||
const handleConfigChange = useCallback((newConfig: Record<string, unknown>) => {
|
||||
setConfig(newConfig)
|
||||
setHasChanges(true)
|
||||
}, [])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
updateMutation.mutate({ id: roundId, configJson: config })
|
||||
}, [roundId, config, updateMutation])
|
||||
pendingSaveRef.current = true
|
||||
setAutosaveStatus('saving')
|
||||
debouncedSave(newConfig)
|
||||
}, [debouncedSave])
|
||||
|
||||
// ── Computed values ────────────────────────────────────────────────────
|
||||
const projectCount = round?._count?.projectRoundStates ?? 0
|
||||
@@ -565,15 +575,23 @@ export default function RoundDetailPage() {
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
||||
{hasChanges && (
|
||||
<Button size="sm" onClick={handleSave} disabled={updateMutation.isPending} className="bg-white text-[#053d57] hover:bg-white/90">
|
||||
{updateMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4 mr-1.5" />
|
||||
)}
|
||||
Save Config
|
||||
</Button>
|
||||
{autosaveStatus === 'saving' && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
Saving...
|
||||
</span>
|
||||
)}
|
||||
{autosaveStatus === 'saved' && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-emerald-300">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" />
|
||||
Saved
|
||||
</span>
|
||||
)}
|
||||
{autosaveStatus === 'error' && (
|
||||
<span className="flex items-center gap-1.5 text-xs text-red-300">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
Save failed
|
||||
</span>
|
||||
)}
|
||||
<Link href={poolLink}>
|
||||
<Button variant="outline" size="sm" className="border-white/40 bg-white/15 text-white hover:bg-white/30 hover:text-white">
|
||||
@@ -1491,7 +1509,7 @@ export default function RoundDetailPage() {
|
||||
{isEvaluation && (
|
||||
<TabsContent value="assignments" className="space-y-6">
|
||||
{/* Coverage Report */}
|
||||
<CoverageReport roundId={roundId} />
|
||||
<CoverageReport roundId={roundId} requiredReviews={(config.requiredReviewsPerProject as number) || 3} />
|
||||
|
||||
{/* Generate Assignments */}
|
||||
<Card>
|
||||
@@ -1554,13 +1572,14 @@ export default function RoundDetailPage() {
|
||||
<IndividualAssignmentsTable roundId={roundId} projectStates={projectStates} />
|
||||
|
||||
{/* Unassigned Queue */}
|
||||
<RoundUnassignedQueue roundId={roundId} />
|
||||
<RoundUnassignedQueue roundId={roundId} requiredReviews={(config.requiredReviewsPerProject as number) || 3} />
|
||||
|
||||
{/* Assignment Preview Sheet */}
|
||||
<AssignmentPreviewSheet
|
||||
roundId={roundId}
|
||||
open={previewSheetOpen}
|
||||
onOpenChange={setPreviewSheetOpen}
|
||||
requiredReviews={(config.requiredReviewsPerProject as number) || 3}
|
||||
/>
|
||||
|
||||
{/* CSV Export Dialog */}
|
||||
@@ -1822,9 +1841,9 @@ export default function RoundDetailPage() {
|
||||
|
||||
// ── Unassigned projects queue ────────────────────────────────────────────
|
||||
|
||||
function RoundUnassignedQueue({ roundId }: { roundId: string }) {
|
||||
function RoundUnassignedQueue({ roundId, requiredReviews = 3 }: { roundId: string; requiredReviews?: number }) {
|
||||
const { data: unassigned, isLoading } = trpc.roundAssignment.unassignedQueue.useQuery(
|
||||
{ roundId, requiredReviews: 3 },
|
||||
{ roundId, requiredReviews },
|
||||
{ refetchInterval: 15_000 },
|
||||
)
|
||||
|
||||
@@ -1832,7 +1851,7 @@ function RoundUnassignedQueue({ roundId }: { roundId: string }) {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Unassigned Projects</CardTitle>
|
||||
<CardDescription>Projects with fewer than 3 jury assignments</CardDescription>
|
||||
<CardDescription>Projects with fewer than {requiredReviews} jury assignments</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
|
||||
Reference in New Issue
Block a user