feat(finalization): winner email + UI for terminal rounds
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m7s

When finalizing a round with no further round to advance to, passing teams
are winners — not advancers. Detected for both special-award terminal rounds
(label = award name) and the main competition's terminal round (label =
competition name). Wording uses "a winner" so it works for both single-winner
awards and top-N main-track outcomes.

Adds AWARD_WINNER_NOTIFICATION email type + template ("Your project has won!"
with "our team will reach out about next steps" copy). Routes through the
notification dispatch table the same way ADVANCEMENT_NOTIFICATION does.

The FinalizationSummary gains a `winnerContext` field; the admin finalization
tab uses it to swap "X projects will advance to Y" → "X winners will be
notified for [label]" and renames "Advancement Message" → "Winner Message"
in the custom-message field. The email-preview button shows the winner
template when applicable.

In-app notification (bell icon) gets matching winner copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-05-05 20:30:35 +02:00
parent e8d0bb050f
commit 55e6abc161
4 changed files with 209 additions and 36 deletions

View File

@@ -666,7 +666,9 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
)}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-sm font-medium">Advancement Message</label>
<label className="text-sm font-medium">
{summary.winnerContext ? 'Winner Message' : 'Advancement Message'}
</label>
<Button
variant="ghost"
size="sm"
@@ -681,7 +683,11 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
</Button>
</div>
<Textarea
placeholder="Custom message for projects that are advancing (added to the standard email template)..."
placeholder={
summary.winnerContext
? 'Custom message for winners (added to the standard winner email template)...'
: 'Custom message for projects that are advancing (added to the standard email template)...'
}
value={advancementMessage}
onChange={(e) => setAdvancementMessage(e.target.value)}
rows={3}
@@ -715,7 +721,13 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
<CardContent className="pt-0">
<div className="flex items-center justify-between border-t pt-4">
<div className="text-sm text-muted-foreground">
{summary.nextRound ? (
{summary.winnerContext ? (
<span>
<strong>{passedCount}</strong>{' '}
{passedCount !== 1 ? 'winners' : 'winner'} will be notified for{' '}
<strong>{summary.winnerContext.label}</strong>
</span>
) : summary.nextRound ? (
<span>
<strong>{passedCount}</strong> project{passedCount !== 1 ? 's' : ''} will advance to{' '}
<strong>{summary.nextRound.name}</strong>
@@ -751,9 +763,11 @@ export function FinalizationTab({ roundId, roundStatus }: FinalizationTabProps)
<ul className="list-disc pl-5 space-y-1">
<li>Mark <strong>{passedCount}</strong> project{passedCount !== 1 ? 's' : ''} as <strong>PASSED</strong></li>
<li>Mark <strong>{rejectedCount}</strong> project{rejectedCount !== 1 ? 's' : ''} as <strong>REJECTED</strong></li>
{summary.nextRound && (
{summary.winnerContext ? (
<li>Notify <strong>{passedCount}</strong> {passedCount !== 1 ? 'winners' : 'winner'} for <strong>{summary.winnerContext.label}</strong> (no further round)</li>
) : summary.nextRound ? (
<li>Advance passed projects to <strong>{summary.nextRound.name}</strong></li>
)}
) : null}
<li>Send email notifications to all affected teams</li>
</ul>
{undecidedCount > 0 && (