diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
index 577890c..c016819 100644
--- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx
+++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx
@@ -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() {
{member.user.email}
+
+
updateJuryMemberMutation.mutate({
+ id: member.id,
+ maxAssignmentsOverride: val,
+ })}
+ />
+
))}
@@ -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(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 (
+ setValue(e.target.value)}
+ onBlur={save}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') save()
+ if (e.key === 'Escape') { setValue(currentValue?.toString() ?? ''); setEditing(false) }
+ }}
+ />
+ )
+ }
+
+ return (
+
+ )
+}
+
// ── Unassigned projects queue ────────────────────────────────────────────
function RoundUnassignedQueue({ roundId, requiredReviews = 3 }: { roundId: string; requiredReviews?: number }) {
diff --git a/src/components/admin/jury/jury-members-table.tsx b/src/components/admin/jury/jury-members-table.tsx
index 7c7a426..f2725e3 100644
--- a/src/components/admin/jury/jury-members-table.tsx
+++ b/src/components/admin/jury/jury-members-table.tsx
@@ -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(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 (
+ setValue(e.target.value)}
+ onBlur={save}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') save()
+ if (e.key === 'Escape') { setValue(currentValue?.toString() ?? ''); setEditing(false) }
+ }}
+ />
+ )
+ }
+
+ return (
+
+ )
+}
+
export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps) {
const [addDialogOpen, setAddDialogOpen] = useState(false)
const [removingMemberId, setRemovingMemberId] = useState(null)
@@ -103,7 +177,11 @@ export function JuryMembersTable({ juryGroupId, members }: JuryMembersTableProps
{member.user.email}
- {member.maxAssignmentsOverride ?? '—'}
+
{member.capModeOverride ? (
diff --git a/src/server/routers/roundAssignment.ts b/src/server/routers/roundAssignment.ts
index e66c009..5b2ef47 100644
--- a/src/server/routers/roundAssignment.ts
+++ b/src/server/routers/roundAssignment.ts
@@ -101,7 +101,7 @@ export const roundAssignmentRouter = router({
expertiseTags: (m.user.expertiseTags as string[]) ?? [],
bio: m.user.bio as string | null,
country: m.user.country as string | null,
- maxAssignments: (m as any).maxAssignments as number | null ?? null,
+ maxAssignments: m.maxAssignmentsOverride ?? null,
_count: {
assignments: existingAssignments.filter((a) => a.userId === m.user.id).length,
},
@@ -142,9 +142,18 @@ export const roundAssignmentRouter = router({
const calculatedMax = Math.ceil(totalNeeded / jurorCount) + 2
const maxPerJuror = configuredMax ?? calculatedMax
+ // Build per-juror cap overrides
+ const jurorLimits: Record = {}
+ for (const m of round.juryGroup.members) {
+ if (m.maxAssignmentsOverride != null) {
+ jurorLimits[m.user.id] = m.maxAssignmentsOverride
+ }
+ }
+
const constraints = {
requiredReviewsPerProject: input.requiredReviews,
maxAssignmentsPerJuror: maxPerJuror,
+ jurorLimits,
existingAssignments: existingAssignments.map((a) => ({
jurorId: a.userId,
projectId: a.projectId,