fix: pipeline progress, message variables, jury invite flow, accept-invite UX

- Pipeline: SUBMISSION rounds count IN_PROGRESS + COMPLETED for progress %
- Round engine: remove phantom SubmissionFileRequirement check blocking auto-transition
- Messages: implement {{userName}}, {{projectName}}, {{roundName}}, {{programName}}, {{deadline}} substitution
- Email preview: show greeting, CTA button, and footer matching actual sent email
- Message composer: add green dot indicator for active rounds in round selector
- User create: generate invite token atomically (prevents stuck INVITED state on email failure)
- Jury invites: use jury-specific email template mentioning round context
- Bulk invite: animated progress bar, batch size hint, success/failure counts
- Accept invite: distinguish server errors (retry button) from expired tokens (redirect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-03-31 13:47:42 -04:00
parent 6b40fe7726
commit 7ead21114e
8 changed files with 269 additions and 120 deletions

View File

@@ -128,7 +128,7 @@ export default function MessagesPage() {
// Fetch supporting data
const { data: programs } = trpc.program.list.useQuery({ includeStages: true })
const rounds = programs?.flatMap((p) =>
((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ ...s, program: { name: p.name } }))
((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ ...s, program: { name: p.name } }))
) || []
const { data: templates } = trpc.message.listTemplates.useQuery()
const { data: users } = trpc.user.list.useQuery(
@@ -465,8 +465,9 @@ export default function MessagesPage() {
{rounds?.map((round) => {
const label = round.program ? `${round.program.name} - ${round.name}` : round.name
const isChecked = roundIds.includes(round.id)
const isActive = round.status === 'ROUND_ACTIVE'
return (
<div key={round.id} className="flex items-center gap-2">
<div key={round.id} className={`flex items-center gap-2 ${!isActive ? 'opacity-60' : ''}`}>
<Checkbox
id={`round-${round.id}`}
checked={isChecked}
@@ -478,7 +479,8 @@ export default function MessagesPage() {
)
}}
/>
<label htmlFor={`round-${round.id}`} className="text-sm cursor-pointer">
<label htmlFor={`round-${round.id}`} className="text-sm cursor-pointer flex items-center gap-1.5">
{isActive && <span className="inline-block h-2 w-2 rounded-full bg-green-500 shrink-0" />}
{label}
</label>
</div>