feat(finalization): winner email + UI for terminal rounds
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m7s
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:
@@ -19,6 +19,7 @@ import {
|
||||
} from '../services/round-finalization'
|
||||
import {
|
||||
getAdvancementNotificationTemplate,
|
||||
getAwardWinnerNotificationTemplate,
|
||||
getRejectionNotificationTemplate,
|
||||
} from '@/lib/email'
|
||||
|
||||
@@ -384,30 +385,45 @@ export const roundEngineRouter = router({
|
||||
name: true,
|
||||
competition: {
|
||||
select: {
|
||||
name: true,
|
||||
rounds: {
|
||||
select: { id: true, name: true, sortOrder: true, roundType: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
specialAward: { select: { name: true } },
|
||||
},
|
||||
})
|
||||
const rounds = round.competition.rounds
|
||||
// Skip MENTORING rounds for display — those are opt-in and not the
|
||||
// shared destination for all advancing teams.
|
||||
const toRoundName = findDisplayNextRound(rounds, input.roundId)?.name ?? 'Next Round'
|
||||
const displayNext = findDisplayNextRound(rounds, input.roundId)
|
||||
|
||||
const passedCount = await ctx.prisma.projectRoundState.count({
|
||||
where: { roundId: input.roundId, proposedOutcome: 'PASSED' },
|
||||
})
|
||||
|
||||
const template = getAdvancementNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
round.name,
|
||||
toRoundName,
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
// No further round → passing teams are winners (of the special award if
|
||||
// attached, otherwise of the competition itself).
|
||||
const winnerLabel = !displayNext
|
||||
? round.specialAward?.name ?? round.competition.name
|
||||
: null
|
||||
|
||||
const template = winnerLabel
|
||||
? getAwardWinnerNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
winnerLabel,
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
: getAdvancementNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
round.name,
|
||||
displayNext?.name ?? 'Next Round',
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
|
||||
return { html: template.html, subject: template.subject, recipientCount: passedCount }
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user