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

@@ -34,9 +34,18 @@ import {
FormMessage,
} from '@/components/ui/form'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ArrowLeft, Loader2, AlertCircle } from 'lucide-react'
import { ArrowLeft, Loader2, AlertCircle, Bell } from 'lucide-react'
import { DateTimePicker } from '@/components/ui/datetime-picker'
// 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' },
]
const createRoundSchema = z.object({
programId: z.string().min(1, 'Please select a program'),
name: z.string().min(1, 'Name is required').max(255),
@@ -61,6 +70,7 @@ function CreateRoundContent() {
const programIdParam = searchParams.get('program')
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
const [entryNotificationType, setEntryNotificationType] = useState<string>('')
const utils = trpc.useUtils()
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery()
@@ -92,6 +102,7 @@ function CreateRoundContent() {
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ?? undefined,
votingEndAt: data.votingEndAt ?? undefined,
entryNotificationType: entryNotificationType || undefined,
})
}
@@ -285,6 +296,39 @@ function CreateRoundContent() {
</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>
{/* Error */}
{createRound.error && (
<Card className="border-destructive">