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:
@@ -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
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user