feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
- processRoundClose EVALUATION uses ranking scores + advanceMode config (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED - Advancement emails generate invite tokens for passwordless users with "Create Your Account" CTA; rejection emails have no link - Finalization UI shows account stats (invite vs dashboard link counts) - Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson) - New award pool notification system: getAwardSelectionNotificationTemplate email, notifyEligibleProjects mutation with invite token generation, "Notify Pool" button on award detail page with custom message dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,12 +64,13 @@ import {
|
||||
import { toast } from 'sonner'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
|
||||
type RecipientType = 'ALL' | 'ROLE' | 'ROUND_JURY' | 'PROGRAM_TEAM' | 'USER'
|
||||
type RecipientType = 'ALL' | 'ROLE' | 'ROUND_JURY' | 'ROUND_APPLICANTS' | 'PROGRAM_TEAM' | 'USER'
|
||||
|
||||
const RECIPIENT_TYPE_OPTIONS: { value: RecipientType; label: string }[] = [
|
||||
{ value: 'ALL', label: 'All Users' },
|
||||
{ value: 'ROLE', label: 'By Role' },
|
||||
{ value: 'ROUND_JURY', label: 'Round Jury' },
|
||||
{ value: 'ROUND_APPLICANTS', label: 'Round Applicants' },
|
||||
{ value: 'PROGRAM_TEAM', label: 'Program Team' },
|
||||
{ value: 'USER', label: 'Specific User' },
|
||||
]
|
||||
@@ -110,6 +111,16 @@ export default function MessagesPage() {
|
||||
{ refetchInterval: 30_000 }
|
||||
)
|
||||
|
||||
const emailPreview = trpc.message.previewEmail.useQuery(
|
||||
{ subject, body },
|
||||
{ enabled: showPreview && subject.length > 0 && body.length > 0 }
|
||||
)
|
||||
|
||||
const sendTestMutation = trpc.message.sendTest.useMutation({
|
||||
onSuccess: (data) => toast.success(`Test email sent to ${data.to}`),
|
||||
onError: (e) => toast.error(e.message),
|
||||
})
|
||||
|
||||
const sendMutation = trpc.message.send.useMutation({
|
||||
onSuccess: (data) => {
|
||||
const count = (data as Record<string, unknown>)?.recipientCount || ''
|
||||
@@ -183,6 +194,13 @@ export default function MessagesPage() {
|
||||
? `Jury of ${stage.program ? `${stage.program.name} - ` : ''}${stage.name}`
|
||||
: 'Stage Jury'
|
||||
}
|
||||
case 'ROUND_APPLICANTS': {
|
||||
if (!roundId) return 'Round Applicants (none selected)'
|
||||
const appRound = rounds?.find((r) => r.id === roundId)
|
||||
return appRound
|
||||
? `Applicants in ${appRound.program ? `${appRound.program.name} - ` : ''}${appRound.name}`
|
||||
: 'Round Applicants'
|
||||
}
|
||||
case 'PROGRAM_TEAM': {
|
||||
if (!selectedProgramId) return 'Program Team (none selected)'
|
||||
const program = (programs as Array<{ id: string; name: string }> | undefined)?.find(
|
||||
@@ -218,7 +236,7 @@ export default function MessagesPage() {
|
||||
toast.error('Please select a role')
|
||||
return
|
||||
}
|
||||
if (recipientType === 'ROUND_JURY' && !roundId) {
|
||||
if ((recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && !roundId) {
|
||||
toast.error('Please select a round')
|
||||
return
|
||||
}
|
||||
@@ -333,7 +351,7 @@ export default function MessagesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recipientType === 'ROUND_JURY' && (
|
||||
{(recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && (
|
||||
<div className="space-y-2">
|
||||
<Label>Select Round</Label>
|
||||
<Select value={roundId} onValueChange={setRoundId}>
|
||||
@@ -670,9 +688,20 @@ export default function MessagesPage() {
|
||||
<p className="text-sm font-medium mt-1">{subject}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Message</p>
|
||||
<div className="mt-1 rounded-lg border bg-muted/30 p-4">
|
||||
<p className="text-sm whitespace-pre-wrap">{body}</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Email Preview</p>
|
||||
<div className="mt-1 rounded-lg border overflow-hidden bg-gray-50">
|
||||
{emailPreview.data?.html ? (
|
||||
<iframe
|
||||
srcDoc={emailPreview.data.html}
|
||||
sandbox="allow-same-origin"
|
||||
className="w-full h-[500px] border-0"
|
||||
title="Email Preview"
|
||||
/>
|
||||
) : (
|
||||
<div className="p-4">
|
||||
<p className="text-sm whitespace-pre-wrap">{body}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -699,7 +728,21 @@ export default function MessagesPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => sendTestMutation.mutate({ subject, body })}
|
||||
disabled={sendTestMutation.isPending}
|
||||
className="sm:mr-auto"
|
||||
>
|
||||
{sendTestMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Send Test to Me
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setShowPreview(false)}>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user