feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
- processRoundClose EVALUATION uses ranking scores + advanceMode config (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED - Advancement emails generate invite tokens for passwordless users with "Create Your Account" CTA; rejection emails have no link - Finalization UI shows account stats (invite vs dashboard link counts) - Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson) - New award pool notification system: getAwardSelectionNotificationTemplate email, notifyEligibleProjects mutation with invite token generation, "Notify Pool" button on award detail page with custom message dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||
import { logAudit } from '@/server/utils/audit'
|
||||
import { sendStyledNotificationEmail } from '@/lib/email'
|
||||
import { sendStyledNotificationEmail, getEmailPreviewHtml } from '@/lib/email'
|
||||
|
||||
export const messageRouter = router({
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ export const messageRouter = router({
|
||||
send: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
subject: z.string().min(1).max(500),
|
||||
@@ -371,6 +371,34 @@ export const messageRouter = router({
|
||||
|
||||
return template
|
||||
}),
|
||||
|
||||
/**
|
||||
* Preview styled email HTML for admin compose dialog.
|
||||
*/
|
||||
previewEmail: adminProcedure
|
||||
.input(z.object({ subject: z.string(), body: z.string() }))
|
||||
.query(({ input }) => {
|
||||
return { html: getEmailPreviewHtml(input.subject, input.body) }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Send a test email to the currently logged-in admin.
|
||||
*/
|
||||
sendTest: adminProcedure
|
||||
.input(z.object({ subject: z.string(), body: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await sendStyledNotificationEmail(
|
||||
ctx.user.email,
|
||||
ctx.user.name || '',
|
||||
'MESSAGE',
|
||||
{
|
||||
title: input.subject,
|
||||
message: input.body,
|
||||
linkUrl: '/admin/messages',
|
||||
}
|
||||
)
|
||||
return { sent: true, to: ctx.user.email }
|
||||
}),
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
@@ -419,6 +447,35 @@ async function resolveRecipients(
|
||||
return assignments.map((a) => a.userId)
|
||||
}
|
||||
|
||||
case 'ROUND_APPLICANTS': {
|
||||
const targetRoundId = roundId || (filter?.roundId as string)
|
||||
if (!targetRoundId) return []
|
||||
// Get all projects in this round
|
||||
const projectStates = await prisma.projectRoundState.findMany({
|
||||
where: { roundId: targetRoundId },
|
||||
select: { projectId: true },
|
||||
})
|
||||
const projectIds = projectStates.map((ps) => ps.projectId)
|
||||
if (projectIds.length === 0) return []
|
||||
// Get team members + submittedByUserId
|
||||
const [teamMembers, projects] = await Promise.all([
|
||||
prisma.teamMember.findMany({
|
||||
where: { projectId: { in: projectIds } },
|
||||
select: { userId: true },
|
||||
}),
|
||||
prisma.project.findMany({
|
||||
where: { id: { in: projectIds } },
|
||||
select: { submittedByUserId: true },
|
||||
}),
|
||||
])
|
||||
const userIds = new Set<string>()
|
||||
for (const tm of teamMembers) userIds.add(tm.userId)
|
||||
for (const p of projects) {
|
||||
if (p.submittedByUserId) userIds.add(p.submittedByUserId)
|
||||
}
|
||||
return [...userIds]
|
||||
}
|
||||
|
||||
case 'PROGRAM_TEAM': {
|
||||
const programId = filter?.programId as string
|
||||
if (!programId) return []
|
||||
|
||||
Reference in New Issue
Block a user