feat: admin UX improvements — notify buttons, eval config, round finalization
Custom body support for advancement/rejection notification emails, evaluation config toggle fix, user actions improvements, round finalization with reorder support, project detail page enhancements, award pool duplicate prevention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,12 +10,11 @@
|
||||
import type { PrismaClient, ProjectRoundStateValue, RoundType, Prisma } from '@prisma/client'
|
||||
import { transitionProject, isTerminalState } from './round-engine'
|
||||
import { logAudit } from '@/server/utils/audit'
|
||||
import {
|
||||
sendStyledNotificationEmail,
|
||||
getRejectionNotificationTemplate,
|
||||
} from '@/lib/email'
|
||||
import { getRejectionNotificationTemplate } from '@/lib/email'
|
||||
import { createBulkNotifications } from '../services/in-app-notification'
|
||||
import { generateInviteToken, getInviteExpiryMs } from '@/server/utils/invite'
|
||||
import { sendBatchNotifications } from './notification-sender'
|
||||
import type { NotificationItem } from './notification-sender'
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -724,6 +723,7 @@ export async function confirmFinalization(
|
||||
|
||||
const advancedUserIds = new Set<string>()
|
||||
const rejectedUserIds = new Set<string>()
|
||||
const notificationItems: NotificationItem[] = []
|
||||
|
||||
for (const prs of finalizedStates) {
|
||||
type Recipient = { email: string; name: string | null; userId: string | null }
|
||||
@@ -748,53 +748,56 @@ export async function confirmFinalization(
|
||||
}
|
||||
|
||||
for (const recipient of recipients) {
|
||||
try {
|
||||
if (prs.state === 'PASSED') {
|
||||
// Build account creation URL for passwordless users
|
||||
const token = recipient.userId ? inviteTokenMap.get(recipient.userId) : undefined
|
||||
const accountUrl = token ? `/accept-invite?token=${token}` : undefined
|
||||
if (prs.state === 'PASSED') {
|
||||
const token = recipient.userId ? inviteTokenMap.get(recipient.userId) : undefined
|
||||
const accountUrl = token ? `/accept-invite?token=${token}` : undefined
|
||||
|
||||
await sendStyledNotificationEmail(
|
||||
recipient.email,
|
||||
recipient.name || '',
|
||||
'ADVANCEMENT_NOTIFICATION',
|
||||
{
|
||||
title: 'Your project has advanced!',
|
||||
message: '',
|
||||
linkUrl: accountUrl || '/applicant',
|
||||
metadata: {
|
||||
projectName: prs.project.title,
|
||||
fromRoundName: round.name,
|
||||
toRoundName: targetRoundName,
|
||||
customMessage: options.advancementMessage || undefined,
|
||||
accountUrl,
|
||||
},
|
||||
notificationItems.push({
|
||||
email: recipient.email,
|
||||
name: recipient.name || '',
|
||||
type: 'ADVANCEMENT_NOTIFICATION',
|
||||
context: {
|
||||
title: 'Your project has advanced!',
|
||||
message: '',
|
||||
linkUrl: accountUrl || '/applicant',
|
||||
metadata: {
|
||||
projectName: prs.project.title,
|
||||
fromRoundName: round.name,
|
||||
toRoundName: targetRoundName,
|
||||
customMessage: options.advancementMessage || undefined,
|
||||
accountUrl,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await sendStyledNotificationEmail(
|
||||
recipient.email,
|
||||
recipient.name || '',
|
||||
'REJECTION_NOTIFICATION',
|
||||
{
|
||||
title: `Update on your application: "${prs.project.title}"`,
|
||||
message: '',
|
||||
metadata: {
|
||||
projectName: prs.project.title,
|
||||
roundName: round.name,
|
||||
customMessage: options.rejectionMessage || undefined,
|
||||
},
|
||||
},
|
||||
projectId: prs.projectId,
|
||||
userId: recipient.userId || undefined,
|
||||
roundId: round.id,
|
||||
})
|
||||
} else {
|
||||
notificationItems.push({
|
||||
email: recipient.email,
|
||||
name: recipient.name || '',
|
||||
type: 'REJECTION_NOTIFICATION',
|
||||
context: {
|
||||
title: `Update on your application: "${prs.project.title}"`,
|
||||
message: '',
|
||||
metadata: {
|
||||
projectName: prs.project.title,
|
||||
roundName: round.name,
|
||||
customMessage: options.rejectionMessage || undefined,
|
||||
},
|
||||
)
|
||||
}
|
||||
emailsSent++
|
||||
} catch (err) {
|
||||
console.error(`[Finalization] Email failed for ${recipient.email}:`, err)
|
||||
emailsFailed++
|
||||
},
|
||||
projectId: prs.projectId,
|
||||
userId: recipient.userId || undefined,
|
||||
roundId: round.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const batchResult = await sendBatchNotifications(notificationItems)
|
||||
emailsSent = batchResult.sent
|
||||
emailsFailed = batchResult.failed
|
||||
|
||||
// Create in-app notifications
|
||||
if (advancedUserIds.size > 0) {
|
||||
void createBulkNotifications({
|
||||
|
||||
Reference in New Issue
Block a user