feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
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:
@@ -13,14 +13,14 @@ import {
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { StatusTracker } from '@/components/shared/status-tracker'
|
||||
import { CompetitionTimelineSidebar } from '@/components/applicant/competition-timeline'
|
||||
import { WithdrawButton } from '@/components/applicant/withdraw-button'
|
||||
import { MentoringRequestCard } from '@/components/applicant/mentoring-request-card'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import {
|
||||
FileText,
|
||||
Calendar,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
Users,
|
||||
Crown,
|
||||
@@ -43,7 +43,7 @@ const statusColors: Record<string, 'default' | 'success' | 'secondary' | 'destru
|
||||
}
|
||||
|
||||
export default function ApplicantDashboardPage() {
|
||||
const { status: sessionStatus } = useSession()
|
||||
const { data: session, status: sessionStatus } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
|
||||
const { data, isLoading } = trpc.applicant.getMyDashboard.useQuery(undefined, {
|
||||
@@ -112,7 +112,6 @@ export default function ApplicantDashboardPage() {
|
||||
}
|
||||
|
||||
const { project, timeline, currentStatus, openRounds, hasPassedIntake } = data
|
||||
const isDraft = !project.submittedAt
|
||||
const programYear = project.program?.year
|
||||
const programName = project.program?.name
|
||||
const totalEvaluations = evaluations?.reduce((sum, r) => sum + r.evaluationCount, 0) ?? 0
|
||||
@@ -121,32 +120,34 @@ export default function ApplicantDashboardPage() {
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{project.title}</h1>
|
||||
{currentStatus && (
|
||||
<Badge variant={statusColors[currentStatus] || 'secondary'}>
|
||||
{currentStatus.replace('_', ' ')}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Project logo */}
|
||||
<div className="shrink-0 h-14 w-14 rounded-xl border bg-muted/50 flex items-center justify-center overflow-hidden">
|
||||
{data.logoUrl ? (
|
||||
<img src={data.logoUrl} alt={project.title} className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<FileText className="h-7 w-7 text-muted-foreground/60" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{programYear ? `${programYear} Edition` : ''}{programName ? ` - ${programName}` : ''}
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{project.title}</h1>
|
||||
{currentStatus && (
|
||||
<Badge variant={statusColors[currentStatus] || 'secondary'}>
|
||||
{currentStatus.replace('_', ' ')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{programYear ? `${programYear} Edition` : ''}{programName ? ` - ${programName}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{project.isTeamLead && currentStatus !== 'REJECTED' && (currentStatus as string) !== 'WINNER' && (
|
||||
<WithdrawButton projectId={project.id} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Draft warning */}
|
||||
{isDraft && (
|
||||
<Alert>
|
||||
<Clock className="h-4 w-4" />
|
||||
<AlertTitle>Draft Submission</AlertTitle>
|
||||
<AlertDescription>
|
||||
This submission has not been submitted yet. You can continue editing and submit when ready.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
@@ -205,16 +206,11 @@ export default function ApplicantDashboardPage() {
|
||||
<Calendar className="h-4 w-4" />
|
||||
Created {new Date(project.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
{project.submittedAt ? (
|
||||
{project.submittedAt && (
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
Submitted {new Date(project.submittedAt).toLocaleDateString()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-4 w-4 text-orange-500" />
|
||||
Draft
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="h-4 w-4" />
|
||||
@@ -310,23 +306,25 @@ export default function ApplicantDashboardPage() {
|
||||
<AnimatedCard index={3}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{hasPassedIntake ? 'Competition Progress' : 'Status Timeline'}
|
||||
</CardTitle>
|
||||
<CardTitle>Status Timeline</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{hasPassedIntake ? (
|
||||
<CompetitionTimelineSidebar />
|
||||
) : (
|
||||
<StatusTracker
|
||||
timeline={timeline}
|
||||
currentStatus={currentStatus || 'SUBMITTED'}
|
||||
/>
|
||||
)}
|
||||
<CompetitionTimelineSidebar />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Mentoring Request Card — show when there's an active MENTORING round */}
|
||||
{project.isTeamLead && openRounds.filter((r) => r.roundType === 'MENTORING').map((mentoringRound) => (
|
||||
<AnimatedCard key={mentoringRound.id} index={4}>
|
||||
<MentoringRequestCard
|
||||
projectId={project.id}
|
||||
roundId={mentoringRound.id}
|
||||
roundName={mentoringRound.name}
|
||||
/>
|
||||
</AnimatedCard>
|
||||
))}
|
||||
|
||||
{/* Jury Feedback Card */}
|
||||
{totalEvaluations > 0 && (
|
||||
<AnimatedCard index={4}>
|
||||
|
||||
Reference in New Issue
Block a user