feat: add email preview to award notification and finalization tab
- Award "Notify Pool" dialog now uses EmailPreviewDialog with live preview - Button shows eligible project count: "Notify Pool (38)" - Finalization tab email section has "Preview" buttons for both advancement and rejection messages - EmailPreviewDialog supports previewOnly mode (close button, no send) - Backend: previewAwardSelectionEmail, previewFinalizationAdvancementEmail, previewFinalizationRejectionEmail queries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,10 @@ import {
|
||||
getFinalizationSummary,
|
||||
confirmFinalization,
|
||||
} from '../services/round-finalization'
|
||||
import {
|
||||
getAdvancementNotificationTemplate,
|
||||
getRejectionNotificationTemplate,
|
||||
} from '@/lib/email'
|
||||
|
||||
const projectRoundStateEnum = z.enum([
|
||||
'PENDING',
|
||||
@@ -367,6 +371,66 @@ export const roundEngineRouter = router({
|
||||
return { updated }
|
||||
}),
|
||||
|
||||
previewFinalizationAdvancementEmail: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
customMessage: z.string().optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: input.roundId },
|
||||
select: {
|
||||
name: true,
|
||||
competition: {
|
||||
select: { rounds: { select: { id: true, name: true, sortOrder: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
},
|
||||
},
|
||||
})
|
||||
const rounds = round.competition.rounds
|
||||
const currentIdx = rounds.findIndex((r) => r.id === input.roundId)
|
||||
const nextRound = rounds[currentIdx + 1]
|
||||
const toRoundName = nextRound?.name ?? 'Next Round'
|
||||
|
||||
const passedCount = await ctx.prisma.projectRoundState.count({
|
||||
where: { roundId: input.roundId, proposedOutcome: 'PASSED' },
|
||||
})
|
||||
|
||||
const template = getAdvancementNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
round.name,
|
||||
toRoundName,
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
|
||||
return { html: template.html, subject: template.subject, recipientCount: passedCount }
|
||||
}),
|
||||
|
||||
previewFinalizationRejectionEmail: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
customMessage: z.string().optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: input.roundId },
|
||||
select: { name: true },
|
||||
})
|
||||
|
||||
const rejectedCount = await ctx.prisma.projectRoundState.count({
|
||||
where: { roundId: input.roundId, proposedOutcome: 'REJECTED' },
|
||||
})
|
||||
|
||||
const template = getRejectionNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
round.name,
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
|
||||
return { html: template.html, subject: template.subject, recipientCount: rejectedCount }
|
||||
}),
|
||||
|
||||
confirmFinalization: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Prisma } from '@prisma/client'
|
||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||
import { logAudit } from '../utils/audit'
|
||||
import { processEligibilityJob } from '../services/award-eligibility-job'
|
||||
import { sendStyledNotificationEmail } from '@/lib/email'
|
||||
import { sendStyledNotificationEmail, getAwardSelectionNotificationTemplate } from '@/lib/email'
|
||||
import { generateInviteToken, getInviteExpiryMs } from '@/server/utils/invite'
|
||||
import type { PrismaClient } from '@prisma/client'
|
||||
|
||||
@@ -1229,6 +1229,31 @@ export const specialAwardRouter = router({
|
||||
return { needsInvite, hasAccount, totalProjects: eligibilities.length }
|
||||
}),
|
||||
|
||||
previewAwardSelectionEmail: adminProcedure
|
||||
.input(z.object({
|
||||
awardId: z.string(),
|
||||
customMessage: z.string().optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
select: { name: true },
|
||||
})
|
||||
|
||||
const eligibleCount = await ctx.prisma.awardEligibility.count({
|
||||
where: { awardId: input.awardId, eligible: true },
|
||||
})
|
||||
|
||||
const template = getAwardSelectionNotificationTemplate(
|
||||
'Team Member',
|
||||
'Your Project',
|
||||
award.name,
|
||||
input.customMessage || undefined,
|
||||
)
|
||||
|
||||
return { html: template.html, subject: template.subject, recipientCount: eligibleCount }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Notify eligible projects that they've been selected for an award.
|
||||
* Generates invite tokens for passwordless users.
|
||||
|
||||
Reference in New Issue
Block a user