'use client' import { useState, useRef, useEffect } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Loader2, Pencil } from 'lucide-react' export type InlineMemberCapProps = { memberId: string currentValue: number | null onSave: (val: number | null) => void roundId?: string jurorUserId?: string } export function InlineMemberCap({ memberId, currentValue, onSave, roundId, jurorUserId, }: InlineMemberCapProps) { const utils = trpc.useUtils() const [editing, setEditing] = useState(false) const [value, setValue] = useState(currentValue?.toString() ?? '') const [overCapInfo, setOverCapInfo] = useState<{ total: number; overCapCount: number; movableOverCap: number; immovableOverCap: number } | null>(null) const [showBanner, setShowBanner] = useState(false) const inputRef = useRef(null) const redistributeMutation = trpc.assignment.redistributeOverCap.useMutation({ onSuccess: (data) => { utils.assignment.listByStage.invalidate() utils.analytics.getJurorWorkload.invalidate() utils.roundAssignment.unassignedQueue.invalidate() setShowBanner(false) setOverCapInfo(null) if (data.failed > 0) { toast.warning(`Redistributed ${data.redistributed} project(s). ${data.failed} could not be reassigned.`) } else { toast.success(`Redistributed ${data.redistributed} project(s) to other jurors.`) } }, onError: (err) => toast.error(err.message), }) useEffect(() => { if (editing) inputRef.current?.focus() }, [editing]) const save = async () => { 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 } // Check over-cap impact before saving if (newVal !== null && roundId && jurorUserId) { try { const preview = await utils.client.assignment.getOverCapPreview.query({ roundId, jurorId: jurorUserId, newCap: newVal, }) if (preview.overCapCount > 0) { setOverCapInfo(preview) setShowBanner(true) setEditing(false) return } } catch { // If preview fails, just save the cap normally } } onSave(newVal) setEditing(false) } const handleRedistribute = () => { const newVal = parseInt(value.trim(), 10) onSave(newVal) if (roundId && jurorUserId) { redistributeMutation.mutate({ roundId, jurorId: jurorUserId, newCap: newVal }) } } const handleJustSave = () => { const newVal = value.trim() === '' ? null : parseInt(value.trim(), 10) onSave(newVal) setShowBanner(false) setOverCapInfo(null) } if (showBanner && overCapInfo) { return (

New cap of {value} is below current load ({overCapInfo.total} assignments). {overCapInfo.movableOverCap} can be redistributed.

{overCapInfo.immovableOverCap > 0 && (

{overCapInfo.immovableOverCap} submitted evaluation(s) cannot be moved.

)}
) } 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 ( ) }