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:
@@ -15,6 +15,8 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -38,12 +40,18 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('')
|
||||
const [maxAssignments, setMaxAssignments] = useState<string>('')
|
||||
const [capMode, setCapMode] = useState<string>('')
|
||||
const [role, setRole] = useState<string>('MEMBER')
|
||||
const [startupRatio, setStartupRatio] = useState<number | null>(null)
|
||||
const [availabilityNotes, setAvailabilityNotes] = useState('')
|
||||
|
||||
// Invite new user state
|
||||
const [inviteName, setInviteName] = useState('')
|
||||
const [inviteEmail, setInviteEmail] = useState('')
|
||||
const [inviteMaxAssignments, setInviteMaxAssignments] = useState<string>('')
|
||||
const [inviteCapMode, setInviteCapMode] = useState<string>('')
|
||||
const [inviteRole, setInviteRole] = useState<string>('MEMBER')
|
||||
const [inviteStartupRatio, setInviteStartupRatio] = useState<number | null>(null)
|
||||
const [inviteAvailabilityNotes, setInviteAvailabilityNotes] = useState('')
|
||||
const [inviteExpertise, setInviteExpertise] = useState('')
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
@@ -73,9 +81,11 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
addMember({
|
||||
juryGroupId,
|
||||
userId: newUser.id,
|
||||
role: 'MEMBER',
|
||||
role: inviteRole as 'CHAIR' | 'MEMBER' | 'OBSERVER',
|
||||
maxAssignmentsOverride: inviteMaxAssignments ? parseInt(inviteMaxAssignments, 10) : null,
|
||||
capModeOverride: inviteCapMode && inviteCapMode !== 'DEFAULT' ? (inviteCapMode as 'HARD' | 'SOFT' | 'NONE') : null,
|
||||
preferredStartupRatio: inviteStartupRatio,
|
||||
availabilityNotes: inviteAvailabilityNotes.trim() || null,
|
||||
})
|
||||
// Send invitation email
|
||||
sendInvitation({ userId: newUser.id, juryGroupId })
|
||||
@@ -101,10 +111,16 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
setSelectedUserId('')
|
||||
setMaxAssignments('')
|
||||
setCapMode('')
|
||||
setRole('MEMBER')
|
||||
setStartupRatio(null)
|
||||
setAvailabilityNotes('')
|
||||
setInviteName('')
|
||||
setInviteEmail('')
|
||||
setInviteMaxAssignments('')
|
||||
setInviteCapMode('')
|
||||
setInviteRole('MEMBER')
|
||||
setInviteStartupRatio(null)
|
||||
setInviteAvailabilityNotes('')
|
||||
setInviteExpertise('')
|
||||
}
|
||||
|
||||
@@ -119,9 +135,11 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
addMember({
|
||||
juryGroupId,
|
||||
userId: selectedUserId,
|
||||
role: 'MEMBER',
|
||||
role: role as 'CHAIR' | 'MEMBER' | 'OBSERVER',
|
||||
maxAssignmentsOverride: maxAssignments ? parseInt(maxAssignments, 10) : null,
|
||||
capModeOverride: capMode && capMode !== 'DEFAULT' ? (capMode as 'HARD' | 'SOFT' | 'NONE') : null,
|
||||
preferredStartupRatio: startupRatio,
|
||||
availabilityNotes: availabilityNotes.trim() || null,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -212,6 +230,19 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select value={role} onValueChange={setRole}>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="CHAIR">Chair</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="capMode">Cap Mode</Label>
|
||||
<Select value={capMode || 'DEFAULT'} onValueChange={setCapMode}>
|
||||
@@ -240,6 +271,38 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Category Preference</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground w-20 shrink-0">Startup</span>
|
||||
<Slider
|
||||
value={[startupRatio !== null ? startupRatio * 100 : 50]}
|
||||
onValueChange={([v]) => setStartupRatio(v / 100)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground w-20 shrink-0 text-right">Concept</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{startupRatio !== null
|
||||
? `~${Math.round(startupRatio * 100)}% startups / ~${Math.round((1 - startupRatio) * 100)}% concepts`
|
||||
: 'No preference set (balanced distribution)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="availabilityNotes">Availability Notes (optional)</Label>
|
||||
<Textarea
|
||||
id="availabilityNotes"
|
||||
placeholder="e.g. Available only in March, limited to 5 reviews/week..."
|
||||
rows={2}
|
||||
value={availabilityNotes}
|
||||
onChange={(e) => setAvailabilityNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
@@ -281,6 +344,19 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteRole">Role</Label>
|
||||
<Select value={inviteRole} onValueChange={setInviteRole}>
|
||||
<SelectTrigger id="inviteRole">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="MEMBER">Member</SelectItem>
|
||||
<SelectItem value="CHAIR">Chair</SelectItem>
|
||||
<SelectItem value="OBSERVER">Observer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteCapMode">Cap Mode</Label>
|
||||
<Select value={inviteCapMode || 'DEFAULT'} onValueChange={setInviteCapMode}>
|
||||
@@ -309,6 +385,38 @@ export function AddMemberDialog({ juryGroupId, open, onOpenChange }: AddMemberDi
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Category Preference</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-muted-foreground w-20 shrink-0">Startup</span>
|
||||
<Slider
|
||||
value={[inviteStartupRatio !== null ? inviteStartupRatio * 100 : 50]}
|
||||
onValueChange={([v]) => setInviteStartupRatio(v / 100)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={10}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground w-20 shrink-0 text-right">Concept</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{inviteStartupRatio !== null
|
||||
? `~${Math.round(inviteStartupRatio * 100)}% startups / ~${Math.round((1 - inviteStartupRatio) * 100)}% concepts`
|
||||
: 'No preference set (balanced distribution)'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteAvailabilityNotes">Availability Notes (optional)</Label>
|
||||
<Textarea
|
||||
id="inviteAvailabilityNotes"
|
||||
placeholder="e.g. Available only in March, limited to 5 reviews/week..."
|
||||
rows={2}
|
||||
value={inviteAvailabilityNotes}
|
||||
onChange={(e) => setInviteAvailabilityNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteExpertise">Expertise Tags (optional)</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user