Add juror quick actions to Members section, redistribute button, dropout emails, and transfer duplicate detection
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Add mail/transfer/reshuffle/redistribute icons to each juror row in Members card - New redistributeJurorAssignments procedure: reassign all pending projects without dropping juror from group - New DROPOUT_REASSIGNED email template with project names, deadline, and dropped juror context - Update reassignDroppedJuror to send per-juror DROPOUT_REASSIGNED emails instead of generic BATCH_ASSIGNED - Transfer dialog now shows all candidates with "Already assigned" / "At cap" labels instead of hiding them - SQL script for prod DB insertion of new notification setting without seeding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRightLeft,
|
||||
Save,
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
@@ -64,6 +65,8 @@ import {
|
||||
Settings,
|
||||
Zap,
|
||||
Shield,
|
||||
Mail,
|
||||
Shuffle,
|
||||
UserPlus,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
@@ -99,6 +102,7 @@ import {
|
||||
import { InlineMemberCap } from '@/components/admin/jury/inline-member-cap'
|
||||
import { RoundUnassignedQueue } from '@/components/admin/assignment/round-unassigned-queue'
|
||||
import { JuryProgressTable } from '@/components/admin/assignment/jury-progress-table'
|
||||
import { TransferAssignmentsDialog } from '@/components/admin/assignment/transfer-assignments-dialog'
|
||||
import { ReassignmentHistory } from '@/components/admin/assignment/reassignment-history'
|
||||
import { ScoreDistribution } from '@/components/admin/round/score-distribution'
|
||||
import { SendRemindersButton } from '@/components/admin/assignment/send-reminders-button'
|
||||
@@ -355,6 +359,46 @@ export default function RoundDetailPage() {
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
// Jury member quick actions (same as in JuryProgressTable)
|
||||
const [memberTransferJuror, setMemberTransferJuror] = useState<{ id: string; name: string } | null>(null)
|
||||
|
||||
const notifyMemberMutation = trpc.assignment.notifySingleJurorOfAssignments.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Notified juror of ${data.projectCount} assignment(s)`)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const redistributeMemberMutation = trpc.assignment.redistributeJurorAssignments.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.assignment.listByStage.invalidate({ roundId })
|
||||
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
||||
utils.analytics.getJurorWorkload.invalidate({ roundId })
|
||||
utils.roundAssignment.unassignedQueue.invalidate({ roundId })
|
||||
if (data.failedCount > 0) {
|
||||
toast.warning(`Reassigned ${data.movedCount} project(s). ${data.failedCount} could not be reassigned.`)
|
||||
} else {
|
||||
toast.success(`Reassigned ${data.movedCount} project(s) to other jurors.`)
|
||||
}
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const reshuffleMemberMutation = trpc.assignment.reassignDroppedJuror.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.assignment.listByStage.invalidate({ roundId })
|
||||
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
||||
utils.analytics.getJurorWorkload.invalidate({ roundId })
|
||||
utils.roundAssignment.unassignedQueue.invalidate({ roundId })
|
||||
if (data.failedCount > 0) {
|
||||
toast.warning(`Dropped juror and reassigned ${data.movedCount} project(s). ${data.failedCount} could not be reassigned.`)
|
||||
} else {
|
||||
toast.success(`Dropped juror and reassigned ${data.movedCount} project(s) evenly across available jurors.`)
|
||||
}
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const advanceMutation = trpc.round.advanceProjects.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.round.getById.invalidate({ id: roundId })
|
||||
@@ -1655,6 +1699,93 @@ export default function RoundDetailPage() {
|
||||
maxAssignmentsOverride: val,
|
||||
})}
|
||||
/>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
disabled={notifyMemberMutation.isPending}
|
||||
onClick={() => notifyMemberMutation.mutate({ roundId, userId: member.userId })}
|
||||
>
|
||||
{notifyMemberMutation.isPending && notifyMemberMutation.variables?.userId === member.userId ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Mail className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Notify juror of assignments</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
disabled={redistributeMemberMutation.isPending}
|
||||
onClick={() => {
|
||||
const ok = window.confirm(
|
||||
`Reassign all pending projects from ${member.user.name || member.user.email} to other available jurors? The juror will remain in the group but lose unsubmitted assignments.`
|
||||
)
|
||||
if (!ok) return
|
||||
redistributeMemberMutation.mutate({ roundId, jurorId: member.userId })
|
||||
}}
|
||||
>
|
||||
{redistributeMemberMutation.isPending && redistributeMemberMutation.variables?.jurorId === member.userId ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Shuffle className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Reassign all pending projects to other jurors</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMemberTransferJuror({ id: member.userId, name: member.user.name || member.user.email })}
|
||||
>
|
||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Transfer specific assignments to other jurors</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
disabled={reshuffleMemberMutation.isPending}
|
||||
onClick={() => {
|
||||
const ok = window.confirm(
|
||||
`Remove ${member.user.name || member.user.email} from this jury pool and reassign all their unsubmitted projects to other jurors? Submitted evaluations will be preserved. This cannot be undone.`
|
||||
)
|
||||
if (!ok) return
|
||||
reshuffleMemberMutation.mutate({ roundId, jurorId: member.userId })
|
||||
}}
|
||||
>
|
||||
{reshuffleMemberMutation.isPending && reshuffleMemberMutation.variables?.jurorId === member.userId ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<UserPlus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Drop juror & reshuffle pending projects</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
@@ -1911,6 +2042,67 @@ export default function RoundDetailPage() {
|
||||
maxAssignmentsOverride: val,
|
||||
})}
|
||||
/>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
disabled={notifyMemberMutation.isPending}
|
||||
onClick={() => notifyMemberMutation.mutate({ roundId, userId: member.userId })}
|
||||
>
|
||||
{notifyMemberMutation.isPending && notifyMemberMutation.variables?.userId === member.userId ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Mail className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Notify juror of assignments</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setMemberTransferJuror({ id: member.userId, name: member.user.name || member.user.email })}
|
||||
>
|
||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Transfer assignments to other jurors</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||
disabled={reshuffleMemberMutation.isPending}
|
||||
onClick={() => {
|
||||
const ok = window.confirm(
|
||||
`Remove ${member.user.name || member.user.email} from this jury pool and reassign all their unsubmitted projects to other jurors? Submitted evaluations will be preserved. This cannot be undone.`
|
||||
)
|
||||
if (!ok) return
|
||||
reshuffleMemberMutation.mutate({ roundId, jurorId: member.userId })
|
||||
}}
|
||||
>
|
||||
{reshuffleMemberMutation.isPending && reshuffleMemberMutation.variables?.jurorId === member.userId ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<UserPlus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left"><p>Drop juror & reshuffle pending projects</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
@@ -2421,6 +2613,15 @@ export default function RoundDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{memberTransferJuror && (
|
||||
<TransferAssignmentsDialog
|
||||
roundId={roundId}
|
||||
sourceJuror={memberTransferJuror}
|
||||
open={!!memberTransferJuror}
|
||||
onClose={() => setMemberTransferJuror(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user