feat: Email Team button + custom-email dialog on project page
Adds a PROJECT_TEAM recipient type to the message router (resolver returns team members + project lead) and an "Email Team" button on the admin project detail page that opens a self-contained dialog matching the look of /admin/messages: subject, body (pre-filled with "Hello [Project Title] team,\n\n"), live HTML preview iframe, "Send test to me" + "Send to N" actions. The composer reuses the existing message.previewEmail and message.send tRPC procedures end-to-end — no parallel email infrastructure introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ export const messageRouter = router({
|
||||
send: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'PROJECT_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
@@ -482,7 +482,7 @@ export const messageRouter = router({
|
||||
*/
|
||||
previewRecipients: adminProcedure
|
||||
.input(z.object({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'PROJECT_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
@@ -556,7 +556,7 @@ export const messageRouter = router({
|
||||
*/
|
||||
listRecipientDetails: adminProcedure
|
||||
.input(z.object({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'PROJECT_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
@@ -839,6 +839,25 @@ async function resolveRecipients(
|
||||
return [...ids]
|
||||
}
|
||||
|
||||
case 'PROJECT_TEAM': {
|
||||
const projectId = filter?.projectId as string
|
||||
if (!projectId) return []
|
||||
const [teamMembers, project] = await Promise.all([
|
||||
prisma.teamMember.findMany({
|
||||
where: { projectId },
|
||||
select: { userId: true },
|
||||
}),
|
||||
prisma.project.findUnique({
|
||||
where: { id: projectId },
|
||||
select: { submittedByUserId: true },
|
||||
}),
|
||||
])
|
||||
const ids = new Set<string>()
|
||||
for (const tm of teamMembers) ids.add(tm.userId)
|
||||
if (project?.submittedByUserId) ids.add(project.submittedByUserId)
|
||||
return [...ids]
|
||||
}
|
||||
|
||||
case 'ALL': {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { status: 'ACTIVE' },
|
||||
|
||||
Reference in New Issue
Block a user