Fix per-juror assignment caps: read correct field + inline edit UI
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m50s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m50s
- Fix bug: AI assignment router read non-existent `(m as any).maxAssignments` instead of the actual schema field `m.maxAssignmentsOverride` - Wire `jurorLimits` record into AI assignment constraints so per-juror caps are respected during both AI scoring and algorithmic assignment - Add inline editable cap in jury members table (click to edit, blur/enter to save, empty = no cap / use group default) - Add inline editable cap badges on round page member list so admins can set caps right from the assignment workflow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,7 @@ import {
|
||||
MoreHorizontal,
|
||||
ShieldAlert,
|
||||
Eye,
|
||||
Pencil,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
Command,
|
||||
@@ -370,6 +371,14 @@ export default function RoundDetailPage() {
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const updateJuryMemberMutation = trpc.juryGroup.updateMember.useMutation({
|
||||
onSuccess: () => {
|
||||
if (juryGroupId) utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
||||
toast.success('Cap updated')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const advanceMutation = trpc.round.advanceProjects.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
@@ -1533,6 +1542,15 @@ export default function RoundDetailPage() {
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{member.user.email}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<InlineMemberCap
|
||||
memberId={member.id}
|
||||
currentValue={member.maxAssignmentsOverride as number | null}
|
||||
onSave={(val) => updateJuryMemberMutation.mutate({
|
||||
id: member.id,
|
||||
maxAssignmentsOverride: val,
|
||||
})}
|
||||
/>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
@@ -1561,6 +1579,7 @@ export default function RoundDetailPage() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -2057,6 +2076,73 @@ export default function RoundDetailPage() {
|
||||
// Sub-components
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── Inline cap editor for jury members on round page ─────────────────────
|
||||
|
||||
function InlineMemberCap({
|
||||
memberId,
|
||||
currentValue,
|
||||
onSave,
|
||||
}: {
|
||||
memberId: string
|
||||
currentValue: number | null
|
||||
onSave: (val: number | null) => void
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [value, setValue] = useState(currentValue?.toString() ?? '')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) inputRef.current?.focus()
|
||||
}, [editing])
|
||||
|
||||
const save = () => {
|
||||
const trimmed = value.trim()
|
||||
const newVal = trimmed === '' ? null : parseInt(trimmed, 10)
|
||||
if (newVal !== null && (isNaN(newVal) || newVal < 1)) {
|
||||
toast.error('Enter a positive number or leave empty for no cap')
|
||||
return
|
||||
}
|
||||
if (newVal === currentValue) {
|
||||
setEditing(false)
|
||||
return
|
||||
}
|
||||
onSave(newVal)
|
||||
setEditing(false)
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="number"
|
||||
min={1}
|
||||
className="h-6 w-16 text-xs"
|
||||
value={value}
|
||||
placeholder="\u221E"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={save}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') save()
|
||||
if (e.key === 'Escape') { setValue(currentValue?.toString() ?? ''); setEditing(false) }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs hover:bg-muted transition-colors group"
|
||||
title="Click to set max assignment cap"
|
||||
onClick={() => { setValue(currentValue?.toString() ?? ''); setEditing(true) }}
|
||||
>
|
||||
<span className="text-muted-foreground">max:</span>
|
||||
<span className="font-medium">{currentValue ?? '\u221E'}</span>
|
||||
<Pencil className="h-2.5 w-2.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Unassigned projects queue ────────────────────────────────────────────
|
||||
|
||||
function RoundUnassignedQueue({ roundId, requiredReviews = 3 }: { roundId: string; requiredReviews?: number }) {
|
||||
|
||||
Reference in New Issue
Block a user