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:
@@ -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 }>
|
||||
}
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user