Add styled notification emails and round-attached notifications

- Add 15+ styled email templates matching existing invite email design
- Wire up notification triggers in all routers (assignment, round, project, mentor, application, onboarding)
- Add test email button for each notification type in admin settings
- Add round-attached notifications: admins can configure which notification to send when projects enter a round
- Fall back to status-based notifications when round has no configured notification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 00:10:51 +01:00
parent 3be6a743ed
commit b0189cad92
13 changed files with 1892 additions and 28 deletions

View File

@@ -32,8 +32,24 @@ import {
type Criterion,
} from '@/components/forms/evaluation-form-builder'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ArrowLeft, Loader2, AlertCircle, AlertTriangle } from 'lucide-react'
import { ArrowLeft, Loader2, AlertCircle, AlertTriangle, Bell } from 'lucide-react'
import { DateTimePicker } from '@/components/ui/datetime-picker'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
// Available notification types for teams entering a round
const TEAM_NOTIFICATION_OPTIONS = [
{ value: '', label: 'No automatic notification', description: 'Teams will not receive a notification when entering this round' },
{ value: 'ADVANCED_SEMIFINAL', label: 'Advanced to Semi-Finals', description: 'Congratulates team for advancing to semi-finals' },
{ value: 'ADVANCED_FINAL', label: 'Selected as Finalist', description: 'Congratulates team for being selected as finalist' },
{ value: 'NOT_SELECTED', label: 'Not Selected', description: 'Informs team they were not selected to continue' },
{ value: 'WINNER_ANNOUNCEMENT', label: 'Winner Announcement', description: 'Announces the team as a winner' },
]
interface PageProps {
params: Promise<{ id: string }>
@@ -68,6 +84,7 @@ function EditRoundContent({ roundId }: { roundId: string }) {
const [formInitialized, setFormInitialized] = useState(false)
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
const [entryNotificationType, setEntryNotificationType] = useState<string>('')
// Fetch round data - disable refetch on focus to prevent overwriting user's edits
const { data: round, isLoading: loadingRound } = trpc.round.get.useQuery(
@@ -118,9 +135,10 @@ function EditRoundContent({ roundId }: { roundId: string }) {
votingStartAt: round.votingStartAt ? new Date(round.votingStartAt) : null,
votingEndAt: round.votingEndAt ? new Date(round.votingEndAt) : null,
})
// Set round type and settings
// Set round type, settings, and notification type
setRoundType((round.roundType as typeof roundType) || 'EVALUATION')
setRoundSettings((round.settingsJson as Record<string, unknown>) || {})
setEntryNotificationType(round.entryNotificationType || '')
setFormInitialized(true)
}
}, [round, form, formInitialized])
@@ -139,7 +157,7 @@ function EditRoundContent({ roundId }: { roundId: string }) {
}, [evaluationForm, loadingForm, criteriaInitialized])
const onSubmit = async (data: UpdateRoundForm) => {
// Update round with type and settings
// Update round with type, settings, and notification
await updateRound.mutateAsync({
id: roundId,
name: data.name,
@@ -148,6 +166,7 @@ function EditRoundContent({ roundId }: { roundId: string }) {
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ?? null,
votingEndAt: data.votingEndAt ?? null,
entryNotificationType: entryNotificationType || null,
})
// Update evaluation form if criteria changed and no evaluations exist
@@ -334,6 +353,39 @@ function EditRoundContent({ roundId }: { roundId: string }) {
</CardContent>
</Card>
{/* Team Notification */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Bell className="h-5 w-5" />
Team Notification
</CardTitle>
<CardDescription>
Notification sent to project teams when they enter this round
</CardDescription>
</CardHeader>
<CardContent>
<Select
value={entryNotificationType || 'none'}
onValueChange={(val) => setEntryNotificationType(val === 'none' ? '' : val)}
>
<SelectTrigger>
<SelectValue placeholder="No automatic notification" />
</SelectTrigger>
<SelectContent>
{TEAM_NOTIFICATION_OPTIONS.map((option) => (
<SelectItem key={option.value || 'none'} value={option.value || 'none'}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground mt-2">
When projects advance to this round, the selected notification will be sent to the project team automatically.
</p>
</CardContent>
</Card>
{/* Evaluation Criteria */}
<Card>
<CardHeader>