Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -13,6 +13,7 @@ import { logAudit } from '../utils/audit'
|
||||
import { sendInvitationEmail } from '@/lib/email'
|
||||
|
||||
const INVITE_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||
const STATUSES_WITH_TEAM_NOTIFICATIONS = ['SEMIFINALIST', 'FINALIST', 'REJECTED'] as const
|
||||
|
||||
// Valid project status transitions
|
||||
const VALID_PROJECT_TRANSITIONS: Record<string, string[]> = {
|
||||
@@ -245,6 +246,98 @@ export const projectRouter = router({
|
||||
return { ids: projects.map((p) => p.id) }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Preview project-team recipients before bulk status update notifications.
|
||||
* Used by admin UI confirmation dialog to verify notification audience.
|
||||
*/
|
||||
previewStatusNotificationRecipients: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
ids: z.array(z.string()).min(1).max(10000),
|
||||
status: z.enum([
|
||||
'SUBMITTED',
|
||||
'ELIGIBLE',
|
||||
'ASSIGNED',
|
||||
'SEMIFINALIST',
|
||||
'FINALIST',
|
||||
'REJECTED',
|
||||
]),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const statusTriggersNotification = STATUSES_WITH_TEAM_NOTIFICATIONS.includes(
|
||||
input.status as (typeof STATUSES_WITH_TEAM_NOTIFICATIONS)[number]
|
||||
)
|
||||
|
||||
if (!statusTriggersNotification) {
|
||||
return {
|
||||
status: input.status,
|
||||
statusTriggersNotification,
|
||||
totalProjects: 0,
|
||||
projectsWithRecipients: 0,
|
||||
totalRecipients: 0,
|
||||
projects: [] as Array<{
|
||||
id: string
|
||||
title: string
|
||||
recipientCount: number
|
||||
recipientsPreview: string[]
|
||||
hasMoreRecipients: boolean
|
||||
}>,
|
||||
}
|
||||
}
|
||||
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: { id: { in: input.ids } },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
teamMembers: {
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { title: 'asc' },
|
||||
})
|
||||
|
||||
const MAX_PREVIEW_RECIPIENTS_PER_PROJECT = 8
|
||||
|
||||
const mappedProjects = projects.map((project) => {
|
||||
const uniqueEmails = Array.from(
|
||||
new Set(
|
||||
project.teamMembers
|
||||
.map((member) => member.user?.email?.toLowerCase().trim() ?? '')
|
||||
.filter((email) => email.length > 0)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
recipientCount: uniqueEmails.length,
|
||||
recipientsPreview: uniqueEmails.slice(0, MAX_PREVIEW_RECIPIENTS_PER_PROJECT),
|
||||
hasMoreRecipients: uniqueEmails.length > MAX_PREVIEW_RECIPIENTS_PER_PROJECT,
|
||||
}
|
||||
})
|
||||
|
||||
const projectsWithRecipients = mappedProjects.filter((p) => p.recipientCount > 0).length
|
||||
const totalRecipients = mappedProjects.reduce((sum, project) => sum + project.recipientCount, 0)
|
||||
|
||||
return {
|
||||
status: input.status,
|
||||
statusTriggersNotification,
|
||||
totalProjects: mappedProjects.length,
|
||||
projectsWithRecipients,
|
||||
totalRecipients,
|
||||
projects: mappedProjects,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get filter options for the project list (distinct values)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user