Fix evaluation criteria, jury preferences, assignment config, and dashboard stats
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:
Matt
2026-02-17 12:33:20 +01:00
parent f9016168e7
commit ef1bf24388
16 changed files with 761 additions and 588 deletions

View File

@@ -0,0 +1,144 @@
'use client'
import { useState } from 'react'
import { Scale, CheckCircle2, Loader2 } from 'lucide-react'
import { toast } from 'sonner'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
/**
* Shows a blocking banner when a juror has jury group memberships
* with unconfirmed preferences (selfServiceCap is null) linked to
* active rounds. The juror must confirm before proceeding.
*/
export function JuryPreferencesBanner() {
const { data: ctx, isLoading } = trpc.user.getOnboardingContext.useQuery()
const utils = trpc.useUtils()
const unconfirmed = (ctx?.memberships ?? []).filter(
(m) => m.selfServiceCap === null,
)
const [prefs, setPrefs] = useState<Map<string, { cap: number; ratio: number }>>(new Map())
const [confirming, setConfirming] = useState(false)
const updatePref = (id: string, field: 'cap' | 'ratio', value: number) => {
setPrefs((prev) => {
const next = new Map(prev)
const existing = next.get(id) ?? { cap: 15, ratio: 0.5 }
next.set(id, { ...existing, [field]: value })
return next
})
}
const confirmMutation = trpc.user.updateJuryPreferences.useMutation({
onSuccess: () => {
toast.success('Preferences confirmed')
utils.user.getOnboardingContext.invalidate()
setConfirming(false)
},
onError: (err) => {
toast.error(err.message)
setConfirming(false)
},
})
const handleConfirm = () => {
setConfirming(true)
const preferences = unconfirmed.map((m) => {
const pref = prefs.get(m.juryGroupMemberId)
return {
juryGroupMemberId: m.juryGroupMemberId,
selfServiceCap: pref?.cap ?? m.currentCap,
selfServiceRatio: pref?.ratio ?? m.preferredStartupRatio ?? 0.5,
}
})
confirmMutation.mutate({ preferences })
}
if (isLoading || unconfirmed.length === 0) return null
return (
<Card className="border-amber-300 bg-amber-50/50 dark:border-amber-800 dark:bg-amber-950/20">
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Scale className="h-5 w-5 text-amber-600" />
<CardTitle className="text-base">Confirm Your Evaluation Preferences</CardTitle>
</div>
<CardDescription>
Please review and confirm your assignment preferences before evaluations begin.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{unconfirmed.map((m) => {
const pref = prefs.get(m.juryGroupMemberId)
const capValue = pref?.cap ?? m.currentCap
const ratioValue = pref?.ratio ?? m.preferredStartupRatio ?? 0.5
return (
<div key={m.juryGroupMemberId} className="rounded-lg border bg-background p-4 space-y-4">
<h4 className="font-medium text-sm">{m.juryGroupName}</h4>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Maximum assignments: {capValue}
</Label>
<Slider
value={[capValue]}
onValueChange={([v]) => updatePref(m.juryGroupMemberId, 'cap', v)}
min={1}
max={50}
step={1}
/>
<p className="text-xs text-muted-foreground">
Admin suggestion: {m.currentCap}. Adjust to match your availability.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">
Category preference: {Math.round(ratioValue * 100)}% Startups / {Math.round((1 - ratioValue) * 100)}% Business Concepts
</Label>
<Slider
value={[ratioValue * 100]}
onValueChange={([v]) => updatePref(m.juryGroupMemberId, 'ratio', v / 100)}
min={0}
max={100}
step={10}
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>More Business Concepts</span>
<span>More Startups</span>
</div>
<p className="text-xs text-muted-foreground/70 italic">
This is a preference, not a guarantee. Due to the number of projects, the system will try to match your preference but exact ratios cannot be ensured.
</p>
</div>
</div>
)
})}
<Button
onClick={handleConfirm}
disabled={confirming}
className="w-full"
>
{confirming ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Confirming...
</>
) : (
<>
<CheckCircle2 className="h-4 w-4 mr-2" />
Confirm Preferences
</>
)}
</Button>
</CardContent>
</Card>
)
}