feat: multi-round messaging, login logo, applicant seed user
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m40s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m40s
- Communication hub now supports selecting multiple rounds when sending to Round Jury or Round Applicants (checkbox list instead of dropdown) - Recipients are unioned across selected rounds with deduplication - Recipient details grouped by round when multiple rounds selected - Added MOPC logo above "Welcome back" on login page - Added matt@letsbe.solutions as seeded APPLICANT account Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -316,6 +316,7 @@ async function main() {
|
||||
|
||||
const staffAccounts = [
|
||||
{ email: 'matt@monaco-opc.com', name: 'Matt', role: UserRole.SUPER_ADMIN, password: '195260Mp!' },
|
||||
{ email: 'matt@letsbe.solutions', name: 'Matt (Applicant)', role: UserRole.APPLICANT, password: '195260Mp!' },
|
||||
{ email: 'admin@monaco-opc.com', name: 'Admin', role: UserRole.PROGRAM_ADMIN, password: 'Admin123!' },
|
||||
{ email: 'awards@monaco-opc.com', name: 'Award Director', role: UserRole.AWARD_MASTER, password: 'Awards123!' },
|
||||
]
|
||||
@@ -323,10 +324,10 @@ async function main() {
|
||||
const staffUsers: Record<string, string> = {}
|
||||
for (const account of staffAccounts) {
|
||||
const passwordHash = await bcrypt.hash(account.password, 12)
|
||||
const isSuperAdmin = account.role === UserRole.SUPER_ADMIN
|
||||
const needsPassword = account.role === UserRole.SUPER_ADMIN || account.role === UserRole.APPLICANT
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: account.email },
|
||||
update: isSuperAdmin
|
||||
update: needsPassword
|
||||
? {
|
||||
status: UserStatus.ACTIVE,
|
||||
passwordHash,
|
||||
@@ -348,11 +349,11 @@ async function main() {
|
||||
name: account.name,
|
||||
role: account.role,
|
||||
roles: [account.role],
|
||||
status: isSuperAdmin ? UserStatus.ACTIVE : UserStatus.NONE,
|
||||
passwordHash: isSuperAdmin ? passwordHash : null,
|
||||
mustSetPassword: !isSuperAdmin,
|
||||
passwordSetAt: isSuperAdmin ? new Date() : null,
|
||||
onboardingCompletedAt: isSuperAdmin ? new Date() : null,
|
||||
status: needsPassword ? UserStatus.ACTIVE : UserStatus.NONE,
|
||||
passwordHash: needsPassword ? passwordHash : null,
|
||||
mustSetPassword: !needsPassword,
|
||||
passwordSetAt: needsPassword ? new Date() : null,
|
||||
onboardingCompletedAt: needsPassword ? new Date() : null,
|
||||
},
|
||||
})
|
||||
staffUsers[account.email] = user.id
|
||||
|
||||
@@ -109,7 +109,7 @@ const STATE_BADGE_VARIANT: Record<string, 'default' | 'success' | 'destructive'
|
||||
export default function MessagesPage() {
|
||||
const [recipientType, setRecipientType] = useState<RecipientType>('ALL')
|
||||
const [selectedRole, setSelectedRole] = useState('')
|
||||
const [roundId, setRoundId] = useState('')
|
||||
const [roundIds, setRoundIds] = useState<string[]>([])
|
||||
const [selectedProgramId, setSelectedProgramId] = useState('')
|
||||
const [selectedUserId, setSelectedUserId] = useState('')
|
||||
const [subject, setSubject] = useState('')
|
||||
@@ -155,14 +155,14 @@ export default function MessagesPage() {
|
||||
{
|
||||
recipientType,
|
||||
recipientFilter: buildRecipientFilterValue(),
|
||||
roundId: roundId || undefined,
|
||||
roundIds: roundIds.length > 0 ? roundIds : undefined,
|
||||
excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined,
|
||||
},
|
||||
{
|
||||
enabled: recipientType === 'ROUND_APPLICANTS'
|
||||
? !!roundId
|
||||
? roundIds.length > 0
|
||||
: recipientType === 'ROUND_JURY'
|
||||
? !!roundId
|
||||
? roundIds.length > 0
|
||||
: recipientType === 'ROLE'
|
||||
? !!selectedRole
|
||||
: recipientType === 'USER'
|
||||
@@ -179,15 +179,15 @@ export default function MessagesPage() {
|
||||
{
|
||||
recipientType,
|
||||
recipientFilter: buildRecipientFilterValue(),
|
||||
roundId: roundId || undefined,
|
||||
roundIds: roundIds.length > 0 ? roundIds : undefined,
|
||||
excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined,
|
||||
},
|
||||
{
|
||||
enabled: showRecipientDetails && (
|
||||
recipientType === 'ROUND_APPLICANTS'
|
||||
? !!roundId
|
||||
? roundIds.length > 0
|
||||
: recipientType === 'ROUND_JURY'
|
||||
? !!roundId
|
||||
? roundIds.length > 0
|
||||
: recipientType === 'ROLE'
|
||||
? !!selectedRole
|
||||
: recipientType === 'USER'
|
||||
@@ -224,7 +224,7 @@ export default function MessagesPage() {
|
||||
setBody('')
|
||||
setSelectedTemplateId('')
|
||||
setSelectedRole('')
|
||||
setRoundId('')
|
||||
setRoundIds([])
|
||||
setSelectedProgramId('')
|
||||
setSelectedUserId('')
|
||||
setIsScheduled(false)
|
||||
@@ -276,18 +276,24 @@ export default function MessagesPage() {
|
||||
return roleLabel ? `All ${roleLabel}s` : 'By Role (none selected)'
|
||||
}
|
||||
case 'ROUND_JURY': {
|
||||
if (!roundId) return 'Stage Jury (none selected)'
|
||||
const stage = rounds?.find((r) => r.id === roundId)
|
||||
return stage
|
||||
? `Jury of ${stage.program ? `${stage.program.name} - ` : ''}${stage.name}`
|
||||
: 'Stage Jury'
|
||||
if (roundIds.length === 0) return 'Stage Jury (none selected)'
|
||||
const selectedJuryRounds = rounds?.filter((r) => roundIds.includes(r.id))
|
||||
if (!selectedJuryRounds?.length) return 'Stage Jury'
|
||||
if (selectedJuryRounds.length === 1) {
|
||||
const s = selectedJuryRounds[0]
|
||||
return `Jury of ${s.program ? `${s.program.name} - ` : ''}${s.name}`
|
||||
}
|
||||
return `Jury across ${selectedJuryRounds.length} rounds`
|
||||
}
|
||||
case 'ROUND_APPLICANTS': {
|
||||
if (!roundId) return 'Round Applicants (none selected)'
|
||||
const appRound = rounds?.find((r) => r.id === roundId)
|
||||
return appRound
|
||||
? `Applicants in ${appRound.program ? `${appRound.program.name} - ` : ''}${appRound.name}`
|
||||
: 'Round Applicants'
|
||||
if (roundIds.length === 0) return 'Round Applicants (none selected)'
|
||||
const selectedAppRounds = rounds?.filter((r) => roundIds.includes(r.id))
|
||||
if (!selectedAppRounds?.length) return 'Round Applicants'
|
||||
if (selectedAppRounds.length === 1) {
|
||||
const ar = selectedAppRounds[0]
|
||||
return `Applicants in ${ar.program ? `${ar.program.name} - ` : ''}${ar.name}`
|
||||
}
|
||||
return `Applicants across ${selectedAppRounds.length} rounds`
|
||||
}
|
||||
case 'PROGRAM_TEAM': {
|
||||
if (!selectedProgramId) return 'Program Team (none selected)'
|
||||
@@ -324,8 +330,8 @@ export default function MessagesPage() {
|
||||
toast.error('Please select a role')
|
||||
return false
|
||||
}
|
||||
if ((recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && !roundId) {
|
||||
toast.error('Please select a round')
|
||||
if ((recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && roundIds.length === 0) {
|
||||
toast.error('Please select at least one round')
|
||||
return false
|
||||
}
|
||||
if (recipientType === 'PROGRAM_TEAM' && !selectedProgramId) {
|
||||
@@ -348,7 +354,7 @@ export default function MessagesPage() {
|
||||
sendMutation.mutate({
|
||||
recipientType,
|
||||
recipientFilter: buildRecipientFilterValue(),
|
||||
roundId: roundId || undefined,
|
||||
roundIds: roundIds.length > 0 ? roundIds : undefined,
|
||||
excludeStates: recipientType === 'ROUND_APPLICANTS' ? excludeStates : undefined,
|
||||
subject: subject.trim(),
|
||||
body: body.trim(),
|
||||
@@ -412,7 +418,7 @@ export default function MessagesPage() {
|
||||
onValueChange={(v) => {
|
||||
setRecipientType(v as RecipientType)
|
||||
setSelectedRole('')
|
||||
setRoundId('')
|
||||
setRoundIds([])
|
||||
setSelectedProgramId('')
|
||||
setSelectedUserId('')
|
||||
}}
|
||||
@@ -451,24 +457,44 @@ export default function MessagesPage() {
|
||||
|
||||
{(recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && (
|
||||
<div className="space-y-2">
|
||||
<Label>Select Round</Label>
|
||||
<Select value={roundId} onValueChange={setRoundId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a round..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rounds?.map((round) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.program ? `${round.program.name} - ${round.name}` : round.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label>Select Rounds</Label>
|
||||
<div className="rounded-lg border p-3 space-y-2 max-h-48 overflow-y-auto">
|
||||
{rounds?.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No rounds available</p>
|
||||
)}
|
||||
{rounds?.map((round) => {
|
||||
const label = round.program ? `${round.program.name} - ${round.name}` : round.name
|
||||
const isChecked = roundIds.includes(round.id)
|
||||
return (
|
||||
<div key={round.id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`round-${round.id}`}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setRoundIds((prev) =>
|
||||
checked
|
||||
? [...prev, round.id]
|
||||
: prev.filter((id) => id !== round.id)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`round-${round.id}`} className="text-sm cursor-pointer">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{roundIds.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{roundIds.length} round{roundIds.length > 1 ? 's' : ''} selected
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Exclude filters for Round Applicants */}
|
||||
{recipientType === 'ROUND_APPLICANTS' && roundId && (
|
||||
{recipientType === 'ROUND_APPLICANTS' && roundIds.length > 0 && (
|
||||
<div className="rounded-lg border p-3 space-y-2">
|
||||
<Label className="text-sm font-medium">Exclude Project States</Label>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
@@ -1119,6 +1145,8 @@ type RecipientDetailsData = {
|
||||
id: string
|
||||
title: string
|
||||
state: string
|
||||
roundId?: string
|
||||
roundName?: string
|
||||
members: Array<{ id: string; name: string | null; email: string }>
|
||||
}>
|
||||
users: Array<{
|
||||
@@ -1129,6 +1157,7 @@ type RecipientDetailsData = {
|
||||
projectNames?: string[]
|
||||
projectName?: string | null
|
||||
role?: string
|
||||
roundNames?: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -1162,12 +1191,38 @@ function RecipientDetailsList({
|
||||
) : !data || (data.projects.length === 0 && data.users.length === 0) ? (
|
||||
<p className="text-xs text-muted-foreground p-1">No recipients found.</p>
|
||||
) : data.type === 'projects' ? (
|
||||
// ROUND_APPLICANTS: projects with their members
|
||||
data.projects.map((project) => (
|
||||
<ProjectRecipientRow key={project.id} project={project} />
|
||||
))
|
||||
// ROUND_APPLICANTS: projects grouped by round (if multi-round)
|
||||
(() => {
|
||||
const roundGroups = new Map<string, { roundName: string; projects: typeof data.projects }>()
|
||||
for (const project of data.projects) {
|
||||
const key = project.roundId || '_unknown'
|
||||
const existing = roundGroups.get(key)
|
||||
if (existing) {
|
||||
existing.projects.push(project)
|
||||
} else {
|
||||
roundGroups.set(key, {
|
||||
roundName: project.roundName || 'Unknown Round',
|
||||
projects: [project],
|
||||
})
|
||||
}
|
||||
}
|
||||
const groups = Array.from(roundGroups.values())
|
||||
const isMultiRound = groups.length > 1
|
||||
return groups.map((group) => (
|
||||
<div key={group.roundName}>
|
||||
{isMultiRound && (
|
||||
<div className="sticky top-0 bg-muted/60 backdrop-blur-sm px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground border-b mb-1">
|
||||
{group.roundName} ({group.projects.length})
|
||||
</div>
|
||||
)}
|
||||
{group.projects.map((project) => (
|
||||
<ProjectRecipientRow key={`${project.roundId}-${project.id}`} project={project} />
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
})()
|
||||
) : data.type === 'jurors' ? (
|
||||
// ROUND_JURY: jurors with project counts
|
||||
// ROUND_JURY: jurors with project counts and round info
|
||||
data.users.map((user) => (
|
||||
<div key={user.id} className="flex items-center justify-between rounded px-2 py-1.5 text-xs hover:bg-muted/50">
|
||||
<div className="min-w-0">
|
||||
@@ -1177,11 +1232,14 @@ function RecipientDetailsList({
|
||||
>
|
||||
{user.name || user.email}
|
||||
</Link>
|
||||
{user.projectCount !== undefined && (
|
||||
<span className="text-muted-foreground">
|
||||
{user.projectCount} project{user.projectCount !== 1 ? 's' : ''} assigned
|
||||
</span>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{user.projectCount !== undefined && (
|
||||
<>{user.projectCount} project{user.projectCount !== 1 ? 's' : ''} assigned</>
|
||||
)}
|
||||
{user.roundNames && user.roundNames.length > 1 && (
|
||||
<> · {user.roundNames.length} rounds</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@@ -1219,6 +1277,8 @@ function ProjectRecipientRow({ project }: {
|
||||
id: string
|
||||
title: string
|
||||
state: string
|
||||
roundId?: string
|
||||
roundName?: string
|
||||
members: Array<{ id: string; name: string | null; email: string }>
|
||||
}
|
||||
}) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import type { Route } from 'next'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -165,6 +166,15 @@ export default function LoginPage() {
|
||||
<Card className="w-full max-w-md overflow-hidden">
|
||||
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-2">
|
||||
<Image
|
||||
src="/images/MOPC-blue-small.png"
|
||||
alt="MOPC"
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
||||
<CardDescription>
|
||||
{mode === 'password'
|
||||
|
||||
@@ -17,6 +17,7 @@ export const messageRouter = router({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
excludeStates: z.array(z.string()).optional(),
|
||||
subject: z.string().min(1).max(500),
|
||||
body: z.string().min(1),
|
||||
@@ -27,12 +28,19 @@ export const messageRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Resolve recipients based on type
|
||||
const recipientUserIds = await resolveRecipients(
|
||||
// Normalize: prefer roundIds array, fall back to single roundId
|
||||
const effectiveRoundIds = input.roundIds?.length
|
||||
? input.roundIds
|
||||
: input.roundId
|
||||
? [input.roundId]
|
||||
: []
|
||||
|
||||
// Resolve recipients based on type (union across all selected rounds)
|
||||
const recipientUserIds = await resolveRecipientsMultiRound(
|
||||
ctx.prisma,
|
||||
input.recipientType,
|
||||
input.recipientFilter,
|
||||
input.roundId,
|
||||
effectiveRoundIds,
|
||||
input.excludeStates
|
||||
)
|
||||
|
||||
@@ -52,7 +60,8 @@ export const messageRouter = router({
|
||||
senderId: ctx.user.id,
|
||||
recipientType: input.recipientType,
|
||||
recipientFilter: input.recipientFilter ?? undefined,
|
||||
roundId: input.roundId,
|
||||
roundId: effectiveRoundIds[0] ?? null,
|
||||
metadata: effectiveRoundIds.length > 1 ? { roundIds: effectiveRoundIds } : undefined,
|
||||
templateId: input.templateId,
|
||||
subject: input.subject,
|
||||
body: input.body,
|
||||
@@ -416,16 +425,24 @@ export const messageRouter = router({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
excludeStates: z.array(z.string()).optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const effectiveRoundIds = input.roundIds?.length
|
||||
? input.roundIds
|
||||
: input.roundId
|
||||
? [input.roundId]
|
||||
: []
|
||||
|
||||
// For ROUND_APPLICANTS, return a breakdown by project state
|
||||
if (input.recipientType === 'ROUND_APPLICANTS' && input.roundId) {
|
||||
if (input.recipientType === 'ROUND_APPLICANTS' && effectiveRoundIds.length > 0) {
|
||||
const projectStates = await ctx.prisma.projectRoundState.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: { in: effectiveRoundIds } },
|
||||
select: {
|
||||
state: true,
|
||||
projectId: true,
|
||||
roundId: true,
|
||||
project: {
|
||||
select: {
|
||||
submittedByUserId: true,
|
||||
@@ -458,11 +475,11 @@ export const messageRouter = router({
|
||||
}
|
||||
|
||||
// For other recipient types, just count resolved users
|
||||
const userIds = await resolveRecipients(
|
||||
const userIds = await resolveRecipientsMultiRound(
|
||||
ctx.prisma,
|
||||
input.recipientType,
|
||||
input.recipientFilter,
|
||||
input.roundId,
|
||||
effectiveRoundIds,
|
||||
input.excludeStates
|
||||
)
|
||||
|
||||
@@ -482,12 +499,19 @@ export const messageRouter = router({
|
||||
recipientType: z.enum(['USER', 'ROLE', 'ROUND_JURY', 'ROUND_APPLICANTS', 'PROGRAM_TEAM', 'ALL']),
|
||||
recipientFilter: z.any().optional(),
|
||||
roundId: z.string().optional(),
|
||||
roundIds: z.array(z.string()).optional(),
|
||||
excludeStates: z.array(z.string()).optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// For ROUND_APPLICANTS, return users grouped by project
|
||||
if (input.recipientType === 'ROUND_APPLICANTS' && input.roundId) {
|
||||
const stateWhere: Record<string, unknown> = { roundId: input.roundId }
|
||||
const effectiveRoundIds = input.roundIds?.length
|
||||
? input.roundIds
|
||||
: input.roundId
|
||||
? [input.roundId]
|
||||
: []
|
||||
|
||||
// For ROUND_APPLICANTS, return users grouped by project (and round if multi-round)
|
||||
if (input.recipientType === 'ROUND_APPLICANTS' && effectiveRoundIds.length > 0) {
|
||||
const stateWhere: Record<string, unknown> = { roundId: { in: effectiveRoundIds } }
|
||||
if (input.excludeStates && input.excludeStates.length > 0) {
|
||||
stateWhere.state = { notIn: input.excludeStates }
|
||||
}
|
||||
@@ -495,6 +519,8 @@ export const messageRouter = router({
|
||||
where: stateWhere,
|
||||
select: {
|
||||
state: true,
|
||||
roundId: true,
|
||||
round: { select: { id: true, name: true, competition: { select: { name: true } } } },
|
||||
project: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -513,6 +539,8 @@ export const messageRouter = router({
|
||||
id: ps.project.id,
|
||||
title: ps.project.title,
|
||||
state: ps.state,
|
||||
roundId: ps.roundId,
|
||||
roundName: ps.round ? `${ps.round.competition?.name ? ps.round.competition.name + ' - ' : ''}${ps.round.name}` : undefined,
|
||||
members: [
|
||||
...(ps.project.submittedBy ? [ps.project.submittedBy] : []),
|
||||
...ps.project.teamMembers
|
||||
@@ -524,11 +552,13 @@ export const messageRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
// For ROUND_JURY, return users grouped by their assignments
|
||||
if (input.recipientType === 'ROUND_JURY' && input.roundId) {
|
||||
// For ROUND_JURY, return users grouped by their assignments (and round if multi-round)
|
||||
if (input.recipientType === 'ROUND_JURY' && effectiveRoundIds.length > 0) {
|
||||
const assignments = await ctx.prisma.assignment.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
where: { roundId: { in: effectiveRoundIds } },
|
||||
select: {
|
||||
roundId: true,
|
||||
round: { select: { id: true, name: true, competition: { select: { name: true } } } },
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
project: { select: { id: true, title: true } },
|
||||
},
|
||||
@@ -537,13 +567,25 @@ export const messageRouter = router({
|
||||
const userMap = new Map<string, {
|
||||
user: { id: string; name: string | null; email: string };
|
||||
projects: { id: string; title: string }[];
|
||||
rounds: Set<string>;
|
||||
roundNames: string[];
|
||||
}>()
|
||||
for (const a of assignments) {
|
||||
const existing = userMap.get(a.user.id)
|
||||
const roundLabel = a.round ? `${a.round.competition?.name ? a.round.competition.name + ' - ' : ''}${a.round.name}` : a.roundId
|
||||
if (existing) {
|
||||
existing.projects.push(a.project)
|
||||
if (!existing.rounds.has(a.roundId)) {
|
||||
existing.rounds.add(a.roundId)
|
||||
existing.roundNames.push(roundLabel)
|
||||
}
|
||||
} else {
|
||||
userMap.set(a.user.id, { user: a.user, projects: [a.project] })
|
||||
userMap.set(a.user.id, {
|
||||
user: a.user,
|
||||
projects: [a.project],
|
||||
rounds: new Set([a.roundId]),
|
||||
roundNames: [roundLabel],
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -553,16 +595,17 @@ export const messageRouter = router({
|
||||
...entry.user,
|
||||
projectCount: entry.projects.length,
|
||||
projectNames: entry.projects.map((p) => p.title).slice(0, 5),
|
||||
roundNames: entry.roundNames,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// For all other types, just return the user list
|
||||
const userIds = await resolveRecipients(
|
||||
const userIds = await resolveRecipientsMultiRound(
|
||||
ctx.prisma,
|
||||
input.recipientType,
|
||||
input.recipientFilter,
|
||||
input.roundId,
|
||||
effectiveRoundIds,
|
||||
input.excludeStates
|
||||
)
|
||||
if (userIds.length === 0) return { type: 'users' as const, projects: [], users: [] }
|
||||
@@ -615,11 +658,31 @@ export const messageRouter = router({
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Helper: Resolve recipient user IDs based on recipientType
|
||||
// Helper: Resolve recipient user IDs based on recipientType (multi-round)
|
||||
// =============================================================================
|
||||
|
||||
type PrismaClient = Parameters<Parameters<typeof adminProcedure.mutation>[0]>[0]['ctx']['prisma']
|
||||
|
||||
async function resolveRecipientsMultiRound(
|
||||
prisma: PrismaClient,
|
||||
recipientType: string,
|
||||
recipientFilter: unknown,
|
||||
roundIds: string[],
|
||||
excludeStates?: string[]
|
||||
): Promise<string[]> {
|
||||
// For round-based types, union across all selected rounds
|
||||
if ((recipientType === 'ROUND_JURY' || recipientType === 'ROUND_APPLICANTS') && roundIds.length > 0) {
|
||||
const allUserIds = new Set<string>()
|
||||
for (const rid of roundIds) {
|
||||
const ids = await resolveRecipients(prisma, recipientType, recipientFilter, rid, excludeStates)
|
||||
for (const id of ids) allUserIds.add(id)
|
||||
}
|
||||
return [...allUserIds]
|
||||
}
|
||||
// For non-round types, delegate to single-round resolver
|
||||
return resolveRecipients(prisma, recipientType, recipientFilter, roundIds[0], excludeStates)
|
||||
}
|
||||
|
||||
async function resolveRecipients(
|
||||
prisma: PrismaClient,
|
||||
recipientType: string,
|
||||
|
||||
Reference in New Issue
Block a user