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

@@ -2,6 +2,10 @@ import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import {
notifyRoundJury,
NotificationTypes,
} from '../services/in-app-notification'
export const roundRouter = router({
/**
@@ -70,6 +74,7 @@ export const roundRouter = router({
settingsJson: z.record(z.unknown()).optional(),
votingStartAt: z.date().optional(),
votingEndAt: z.date().optional(),
entryNotificationType: z.string().optional(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -158,6 +163,7 @@ export const roundRouter = router({
votingStartAt: z.date().optional().nullable(),
votingEndAt: z.date().optional().nullable(),
settingsJson: z.record(z.unknown()).optional(),
entryNotificationType: z.string().optional().nullable(),
})
)
.mutation(async ({ ctx, input }) => {
@@ -254,6 +260,50 @@ export const roundRouter = router({
},
})
// Notify jury members when round is activated
if (input.status === 'ACTIVE' && previousRound.status !== 'ACTIVE') {
// Get round details and assignment counts per user
const roundDetails = await ctx.prisma.round.findUnique({
where: { id: input.id },
include: {
_count: { select: { assignments: true } },
},
})
// Get count of distinct jury members assigned
const juryCount = await ctx.prisma.assignment.groupBy({
by: ['userId'],
where: { roundId: input.id },
_count: true,
})
if (roundDetails && juryCount.length > 0) {
const deadline = roundDetails.votingEndAt
? new Date(roundDetails.votingEndAt).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})
: undefined
// Notify all jury members with assignments in this round
await notifyRoundJury(input.id, {
type: NotificationTypes.ROUND_NOW_OPEN,
title: `${roundDetails.name} is Now Open`,
message: `The evaluation round is now open. Please review your assigned projects and submit your evaluations before the deadline.`,
linkUrl: `/jury/assignments`,
linkLabel: 'Start Evaluating',
priority: 'high',
metadata: {
roundName: roundDetails.name,
projectCount: roundDetails._count.assignments,
deadline,
},
})
}
}
return round
}),