feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s

- processRoundClose EVALUATION uses ranking scores + advanceMode config
  (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED
- Advancement emails generate invite tokens for passwordless users with
  "Create Your Account" CTA; rejection emails have no link
- Finalization UI shows account stats (invite vs dashboard link counts)
- Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson)
- New award pool notification system: getAwardSelectionNotificationTemplate email,
  notifyEligibleProjects mutation with invite token generation,
  "Notify Pool" button on award detail page with custom message dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 19:14:41 +01:00
parent 7735f3ecdf
commit cfee3bc8a9
48 changed files with 5294 additions and 676 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useMemo, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -39,8 +40,17 @@ import {
Building2,
Flag,
ImageIcon,
Compass,
LayoutDashboard,
Upload,
ClipboardList,
Users,
Trophy,
BookOpen,
GraduationCap,
} from 'lucide-react'
import { AnimatedCard } from '@/components/shared/animated-container'
import { UserAvatar } from '@/components/shared/user-avatar'
type Step =
| 'name'
@@ -51,6 +61,7 @@ type Step =
| 'bio'
| 'logo'
| 'preferences'
| 'guide'
| 'complete'
type ApplicantWizardProps = {
@@ -136,7 +147,7 @@ export function ApplicantOnboardingWizard({
if (onboardingCtx?.projectId) {
base.push('logo')
}
base.push('preferences', 'complete')
base.push('preferences', 'guide', 'complete')
return base
}, [onboardingCtx?.projectId])
@@ -191,6 +202,7 @@ export function ApplicantOnboardingWizard({
bio: 'About',
logo: 'Logo',
preferences: 'Settings',
guide: 'Guide',
complete: 'Done',
}
@@ -203,11 +215,11 @@ export function ApplicantOnboardingWizard({
{/* Progress indicator */}
{step !== 'complete' && (
<div className="px-6 pt-6">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{steps.slice(0, -1).map((s, i) => (
<div key={s} className="flex items-center flex-1">
<div key={s} className="flex-1 flex flex-col items-center gap-1">
<div
className={`h-2 flex-1 rounded-full transition-colors ${
className={`h-2 w-full rounded-full transition-colors ${
i < currentIndex
? 'bg-primary'
: i === currentIndex
@@ -215,15 +227,9 @@ export function ApplicantOnboardingWizard({
: 'bg-muted'
}`}
/>
</div>
))}
</div>
<div className="flex items-center gap-2 mt-1">
{steps.slice(0, -1).map((s, i) => (
<div key={s} className="flex-1 text-center">
<span
className={cn(
'text-[10px]',
'text-[10px] leading-none',
i <= currentIndex ? 'text-primary font-medium' : 'text-muted-foreground'
)}
>
@@ -291,7 +297,16 @@ export function ApplicantOnboardingWizard({
}}
currentAvatarUrl={avatarUrl}
onUploadComplete={() => refetchUser()}
/>
>
<div className="cursor-pointer">
<UserAvatar
user={{ name: userData?.name, email: userData?.email }}
avatarUrl={avatarUrl}
size="2xl"
showEditOverlay
/>
</div>
</AvatarUpload>
</div>
<p className="text-sm text-muted-foreground text-center">
Click the avatar to upload a new photo.
@@ -555,6 +570,83 @@ export function ApplicantOnboardingWizard({
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={goBack} className="flex-1">
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
<Button onClick={goNext} className="flex-1">
Continue
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</>
)}
{/* Step: Portal Guide */}
{step === 'guide' && (
<>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Compass className="h-5 w-5 text-primary" />
Your Applicant Portal
</CardTitle>
<CardDescription>
Here&apos;s what you can do through the MOPC Applicant Portal.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
{[
{
icon: LayoutDashboard,
title: 'Dashboard',
desc: 'Overview of your project status, team, and upcoming deadlines.',
},
{
icon: Upload,
title: 'Documents',
desc: 'Upload required files for each round and track submission progress.',
},
{
icon: ClipboardList,
title: 'Evaluations',
desc: 'View anonymized jury feedback and scores for your project.',
},
{
icon: Users,
title: 'Team',
desc: 'Manage your team members, invite collaborators, and update your project logo.',
},
{
icon: Trophy,
title: 'Competition',
desc: 'Track your progress through competition rounds and milestones.',
},
{
icon: GraduationCap,
title: 'Mentorship',
desc: 'Connect with your assigned mentor for guidance and support.',
},
{
icon: BookOpen,
title: 'Resources',
desc: 'Access helpful materials, guides, and competition resources.',
},
].map(({ icon: Icon, title, desc }) => (
<div key={title} className="flex items-start gap-3 rounded-lg border p-3">
<div className="rounded-md bg-primary/10 p-2 shrink-0">
<Icon className="h-4 w-4 text-primary" />
</div>
<div>
<p className="font-medium text-sm">{title}</p>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
</div>
))}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={goBack} className="flex-1">
<ArrowLeft className="mr-2 h-4 w-4" />
@@ -580,8 +672,8 @@ export function ApplicantOnboardingWizard({
{/* Step: Complete */}
{step === 'complete' && (
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="rounded-2xl bg-emerald-50 p-4 mb-4 animate-in zoom-in-50 duration-500">
<CheckCircle className="h-12 w-12 text-green-600" />
<div className="mb-4 animate-in zoom-in-50 duration-500">
<Image src="/images/MOPC-blue-small.png" alt="MOPC Logo" width={64} height={64} className="h-16 w-16" />
</div>
<h2 className="text-xl font-semibold mb-2 animate-in fade-in slide-in-from-bottom-2 duration-500 delay-200">
Welcome, {name}!