From 68c0ed00e4a078d7f1311b7a255e612ad64f896b Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Feb 2026 09:45:32 +0100 Subject: [PATCH] Add manual assignment feature to assignments page - Added "Manual Assignment" button with dialog - Select jury member and project from dropdowns - Shows current assignment counts and prevents duplicates - Disables full-capacity jurors and already-assigned combinations Co-Authored-By: Claude Opus 4.5 --- .../admin/rounds/[id]/assignments/page.tsx | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/src/app/(admin)/admin/rounds/[id]/assignments/page.tsx b/src/app/(admin)/admin/rounds/[id]/assignments/page.tsx index e7cf341..f3242de 100644 --- a/src/app/(admin)/admin/rounds/[id]/assignments/page.tsx +++ b/src/app/(admin)/admin/rounds/[id]/assignments/page.tsx @@ -15,6 +15,7 @@ import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Progress } from '@/components/ui/progress' import { Checkbox } from '@/components/ui/checkbox' +import { Label } from '@/components/ui/label' import { Table, TableBody, @@ -34,6 +35,22 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { ArrowLeft, Users, @@ -46,7 +63,9 @@ import { Plus, Trash2, RefreshCw, + UserPlus, } from 'lucide-react' +import { toast } from 'sonner' interface PageProps { params: Promise<{ id: string }> @@ -54,6 +73,9 @@ interface PageProps { function AssignmentManagementContent({ roundId }: { roundId: string }) { const [selectedSuggestions, setSelectedSuggestions] = useState>(new Set()) + const [manualDialogOpen, setManualDialogOpen] = useState(false) + const [selectedJuror, setSelectedJuror] = useState('') + const [selectedProject, setSelectedProject] = useState('') const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery({ id: roundId }) const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId }) @@ -63,6 +85,18 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) { { enabled: !!round } ) + // Get available jurors for manual assignment + const { data: availableJurors } = trpc.user.getJuryMembers.useQuery( + { roundId }, + { enabled: manualDialogOpen } + ) + + // Get projects in this round for manual assignment + const { data: roundProjects } = trpc.project.list.useQuery( + { roundId, perPage: 500 }, + { enabled: manualDialogOpen } + ) + const utils = trpc.useUtils() const deleteAssignment = trpc.assignment.delete.useMutation({ @@ -81,6 +115,33 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) { }, }) + const createAssignment = trpc.assignment.create.useMutation({ + onSuccess: () => { + utils.assignment.listByRound.invalidate({ roundId }) + utils.assignment.getStats.invalidate({ roundId }) + utils.assignment.getSuggestions.invalidate({ roundId }) + setManualDialogOpen(false) + setSelectedJuror('') + setSelectedProject('') + toast.success('Assignment created successfully') + }, + onError: (error) => { + toast.error(error.message || 'Failed to create assignment') + }, + }) + + const handleCreateManualAssignment = () => { + if (!selectedJuror || !selectedProject) { + toast.error('Please select both a juror and a project') + return + } + createAssignment.mutate({ + userId: selectedJuror, + projectId: selectedProject, + roundId, + }) + } + if (loadingRound || loadingAssignments) { return } @@ -180,6 +241,124 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) { Manage Assignments + + {/* Manual Assignment Button */} + + + + + + + Create Manual Assignment + + Assign a jury member to evaluate a specific project + + + +
+ {/* Juror Select */} +
+ + + {selectedJuror && availableJurors && ( +

+ {(() => { + const juror = availableJurors.find(j => j.id === selectedJuror) + if (!juror) return null + const available = (juror.maxAssignments ?? 10) - juror.currentAssignments + return `${available} assignment slot${available !== 1 ? 's' : ''} available` + })()} +

+ )} +
+ + {/* Project Select */} +
+ + +
+
+ + + + + +
+
{/* Stats */}