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

- 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:
Matt
2026-02-18 18:23:54 +01:00
parent 735b841f4a
commit 6838b01724
3 changed files with 177 additions and 4 deletions

View File

@@ -1,10 +1,11 @@
'use client'
import { useState } from 'react'
import { Trash2, UserPlus } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { Trash2, UserPlus, Pencil } from 'lucide-react'
import { toast } from 'sonner'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Table,
TableBody,
@@ -45,6 +46,79 @@ interface JuryMembersTableProps {
members: JuryMember[]
}
function InlineCapEdit({
memberId,
currentValue,
juryGroupId,
}: {
memberId: string
currentValue: number | null | undefined
juryGroupId: string
}) {
const [editing, setEditing] = useState(false)
const [value, setValue] = useState(currentValue?.toString() ?? '')
const inputRef = useRef<HTMLInputElement>(null)
const utils = trpc.useUtils()
const { mutate: updateMember, isPending } = trpc.juryGroup.updateMember.useMutation({
onSuccess: () => {
utils.juryGroup.getById.invalidate({ id: juryGroupId })
toast.success('Cap updated')
setEditing(false)
},
onError: (err) => toast.error(err.message),
})
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 ?? null)) {
setEditing(false)
return
}
updateMember({ id: memberId, maxAssignmentsOverride: newVal })
}
if (editing) {
return (
<Input
ref={inputRef}
type="number"
min={1}
className="h-7 w-20 text-xs"
value={value}
placeholder="∞"
disabled={isPending}
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.5 rounded px-1.5 py-0.5 text-sm hover:bg-muted transition-colors group"
onClick={() => { setValue(currentValue?.toString() ?? ''); setEditing(true) }}
>
<span>{currentValue ?? '∞'}</span>
<Pencil className="h-3 w-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</button>
)
}
export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps) {
const [addDialogOpen, setAddDialogOpen] = useState(false)
const [removingMemberId, setRemovingMemberId] = useState<string | null>(null)
@@ -103,7 +177,11 @@ export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps
{member.user.email}
</TableCell>
<TableCell className="hidden sm:table-cell">
{member.maxAssignmentsOverride ?? '—'}
<InlineCapEdit
memberId={member.id}
currentValue={member.maxAssignmentsOverride}
juryGroupId={juryGroupId}
/>
</TableCell>
<TableCell className="hidden lg:table-cell">
{member.capModeOverride ? (