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:
144
src/components/jury/preferences-banner.tsx
Normal file
144
src/components/jury/preferences-banner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user