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:
@@ -7,6 +7,11 @@ import {
|
||||
generateFallbackAssignments,
|
||||
} from '../services/ai-assignment'
|
||||
import { isOpenAIConfigured } from '@/lib/openai'
|
||||
import {
|
||||
createNotification,
|
||||
createBulkNotifications,
|
||||
NotificationTypes,
|
||||
} from '../services/in-app-notification'
|
||||
|
||||
export const assignmentRouter = router({
|
||||
/**
|
||||
@@ -193,6 +198,44 @@ export const assignmentRouter = router({
|
||||
},
|
||||
})
|
||||
|
||||
// Send notification to the assigned jury member
|
||||
const [project, round] = await Promise.all([
|
||||
ctx.prisma.project.findUnique({
|
||||
where: { id: input.projectId },
|
||||
select: { title: true },
|
||||
}),
|
||||
ctx.prisma.round.findUnique({
|
||||
where: { id: input.roundId },
|
||||
select: { name: true, votingEndAt: true },
|
||||
}),
|
||||
])
|
||||
|
||||
if (project && round) {
|
||||
const deadline = round.votingEndAt
|
||||
? new Date(round.votingEndAt).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined
|
||||
|
||||
await createNotification({
|
||||
userId: input.userId,
|
||||
type: NotificationTypes.ASSIGNED_TO_PROJECT,
|
||||
title: 'New Project Assignment',
|
||||
message: `You have been assigned to evaluate "${project.title}" for ${round.name}.`,
|
||||
linkUrl: `/jury/assignments`,
|
||||
linkLabel: 'View Assignment',
|
||||
metadata: {
|
||||
projectName: project.title,
|
||||
roundName: round.name,
|
||||
deadline,
|
||||
assignmentId: assignment.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return assignment
|
||||
}),
|
||||
|
||||
@@ -233,6 +276,51 @@ export const assignmentRouter = router({
|
||||
},
|
||||
})
|
||||
|
||||
// Send notifications to assigned jury members (grouped by user)
|
||||
if (result.count > 0 && input.assignments.length > 0) {
|
||||
// Group assignments by user to get counts
|
||||
const userAssignmentCounts = input.assignments.reduce(
|
||||
(acc, a) => {
|
||||
acc[a.userId] = (acc[a.userId] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
)
|
||||
|
||||
// Get round info for deadline
|
||||
const roundId = input.assignments[0].roundId
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: roundId },
|
||||
select: { name: true, votingEndAt: true },
|
||||
})
|
||||
|
||||
const deadline = round?.votingEndAt
|
||||
? new Date(round.votingEndAt).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined
|
||||
|
||||
// Send batch notification to each user
|
||||
for (const [userId, projectCount] of Object.entries(userAssignmentCounts)) {
|
||||
await createNotification({
|
||||
userId,
|
||||
type: NotificationTypes.BATCH_ASSIGNED,
|
||||
title: `${projectCount} Projects Assigned`,
|
||||
message: `You have been assigned ${projectCount} project${projectCount > 1 ? 's' : ''} to evaluate for ${round?.name || 'this round'}.`,
|
||||
linkUrl: `/jury/assignments`,
|
||||
linkLabel: 'View Assignments',
|
||||
metadata: {
|
||||
projectCount,
|
||||
roundName: round?.name,
|
||||
deadline,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { created: result.count }
|
||||
}),
|
||||
|
||||
@@ -602,6 +690,47 @@ export const assignmentRouter = router({
|
||||
},
|
||||
})
|
||||
|
||||
// Send notifications to assigned jury members
|
||||
if (created.count > 0) {
|
||||
const userAssignmentCounts = input.assignments.reduce(
|
||||
(acc, a) => {
|
||||
acc[a.userId] = (acc[a.userId] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
)
|
||||
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: input.roundId },
|
||||
select: { name: true, votingEndAt: true },
|
||||
})
|
||||
|
||||
const deadline = round?.votingEndAt
|
||||
? new Date(round.votingEndAt).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined
|
||||
|
||||
for (const [userId, projectCount] of Object.entries(userAssignmentCounts)) {
|
||||
await createNotification({
|
||||
userId,
|
||||
type: NotificationTypes.BATCH_ASSIGNED,
|
||||
title: `${projectCount} Projects Assigned`,
|
||||
message: `You have been assigned ${projectCount} project${projectCount > 1 ? 's' : ''} to evaluate for ${round?.name || 'this round'}.`,
|
||||
linkUrl: `/jury/assignments`,
|
||||
linkLabel: 'View Assignments',
|
||||
metadata: {
|
||||
projectCount,
|
||||
roundName: round?.name,
|
||||
deadline,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { created: created.count }
|
||||
}),
|
||||
|
||||
@@ -649,6 +778,47 @@ export const assignmentRouter = router({
|
||||
},
|
||||
})
|
||||
|
||||
// Send notifications to assigned jury members
|
||||
if (created.count > 0) {
|
||||
const userAssignmentCounts = input.assignments.reduce(
|
||||
(acc, a) => {
|
||||
acc[a.userId] = (acc[a.userId] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, number>
|
||||
)
|
||||
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: input.roundId },
|
||||
select: { name: true, votingEndAt: true },
|
||||
})
|
||||
|
||||
const deadline = round?.votingEndAt
|
||||
? new Date(round.votingEndAt).toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: undefined
|
||||
|
||||
for (const [userId, projectCount] of Object.entries(userAssignmentCounts)) {
|
||||
await createNotification({
|
||||
userId,
|
||||
type: NotificationTypes.BATCH_ASSIGNED,
|
||||
title: `${projectCount} Projects Assigned`,
|
||||
message: `You have been assigned ${projectCount} project${projectCount > 1 ? 's' : ''} to evaluate for ${round?.name || 'this round'}.`,
|
||||
linkUrl: `/jury/assignments`,
|
||||
linkLabel: 'View Assignments',
|
||||
metadata: {
|
||||
projectCount,
|
||||
roundName: round?.name,
|
||||
deadline,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { created: created.count }
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user