feat(awards): notify jurors on assignment + admin reminder button
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m41s
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m41s
The previous addJuror / bulkAddJurors / bulkInviteJurors flows silently created AwardJuror rows with no notification when the user already had an account. The result: assigned jurors had no idea they were assigned unless they happened to log in and check /jury/awards manually. Three changes: 1. New email template + sender (sendAwardJurorNotificationEmail). Tells the juror what the award is, how many projects are eligible, when voting closes, and links straight to /jury/awards/<id>. Reused for both the initial assignment notification and admin reminders. 2. Auto-send on assignment. addJuror / bulkAddJurors / bulkInviteJurors now send the email to newly-attached jurors. bulkInviteJurors checks for a prior AwardJuror row before sending so duplicate "Bulk Invite" clicks don't spam jurors who were already assigned. addJuror / bulkAddJurors accept a `sendEmail` flag so admin tooling can opt out. 3. New admin procedure specialAward.notifyJurors(awardId, userIds?, customMessage?). Surfaced in the Jurors tab as a "Send reminder to all" button at the top and a per-row mail icon for individual reminders. Audit-logged with action: 'JUROR_REMINDER'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -513,6 +513,13 @@ export default function AwardDetailPage({
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
const notifyJurors = trpc.specialAward.notifyJurors.useMutation({
|
||||
onSuccess: (data) => {
|
||||
const failedNote = data.failed > 0 ? ` (${data.failed} failed)` : ''
|
||||
toast.success(`Reminder sent to ${data.sent} of ${data.targeted} juror(s)${failedNote}`)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
const setWinner = trpc.specialAward.setWinner.useMutation({
|
||||
onSuccess: invalidateAward,
|
||||
})
|
||||
@@ -1335,7 +1342,7 @@ export default function AwardDetailPage({
|
||||
|
||||
{/* Jurors Tab */}
|
||||
<TabsContent value="jurors" className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select value={selectedJurorId} onValueChange={setSelectedJurorId}>
|
||||
<SelectTrigger className="w-64">
|
||||
<SelectValue placeholder="Select a juror..." />
|
||||
@@ -1355,6 +1362,19 @@ export default function AwardDetailPage({
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Add Juror
|
||||
</Button>
|
||||
{jurors && jurors.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => notifyJurors.mutate({ awardId })}
|
||||
disabled={notifyJurors.isPending}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
{notifyJurors.isPending
|
||||
? 'Sending...'
|
||||
: `Send reminder to all (${jurors.length})`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Import from Jury Group */}
|
||||
@@ -1549,11 +1569,23 @@ export default function AwardDetailPage({
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
notifyJurors.mutate({ awardId, userIds: [j.userId] })
|
||||
}
|
||||
disabled={notifyJurors.isPending}
|
||||
title="Send reminder email"
|
||||
>
|
||||
<Mail className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveJuror(j.userId)}
|
||||
disabled={removeJuror.isPending}
|
||||
title="Remove juror"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user