Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal

Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)

Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)

Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)

Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)

Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 21:58:27 +01:00
parent 002a9dbfc3
commit 699248e40b
38 changed files with 5437 additions and 533 deletions

View File

@@ -1,13 +1,14 @@
'use client'
import { cn } from '@/lib/utils'
import { CheckCircle, Circle, Clock } from 'lucide-react'
import { CheckCircle, Circle, Clock, XCircle, Trophy } from 'lucide-react'
interface TimelineItem {
status: string
label: string
date: Date | string | null
completed: boolean
isTerminal?: boolean
}
interface StatusTrackerProps {
@@ -39,6 +40,8 @@ export function StatusTracker({
const isCurrent =
isCompleted && !timeline[index + 1]?.completed
const isPending = !isCompleted
const isRejected = item.status === 'REJECTED' && item.isTerminal
const isWinner = item.status === 'WINNER' && isCompleted
return (
<div key={item.status} className="relative flex gap-4">
@@ -47,14 +50,26 @@ export function StatusTracker({
<div
className={cn(
'absolute left-[15px] top-[32px] h-full w-0.5',
isCompleted ? 'bg-primary' : 'bg-muted'
isRejected
? 'bg-destructive/30'
: isCompleted
? 'bg-primary'
: 'bg-muted'
)}
/>
)}
{/* Icon */}
<div className="relative z-10 flex h-8 w-8 shrink-0 items-center justify-center">
{isCompleted ? (
{isRejected ? (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive text-destructive-foreground">
<XCircle className="h-4 w-4" />
</div>
) : isWinner ? (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-500 text-white">
<Trophy className="h-4 w-4" />
</div>
) : isCompleted ? (
<div
className={cn(
'flex h-8 w-8 items-center justify-center rounded-full',
@@ -82,23 +97,35 @@ export function StatusTracker({
<p
className={cn(
'font-medium',
isPending && 'text-muted-foreground'
isRejected && 'text-destructive',
isWinner && 'text-yellow-600',
isPending && !isRejected && 'text-muted-foreground'
)}
>
{item.label}
</p>
{isCurrent && (
{isCurrent && !isRejected && !isWinner && (
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full">
Current
</span>
)}
{isRejected && (
<span className="text-xs bg-destructive/10 text-destructive px-2 py-0.5 rounded-full">
Final
</span>
)}
{isWinner && (
<span className="text-xs bg-yellow-100 text-yellow-700 px-2 py-0.5 rounded-full">
Winner
</span>
)}
</div>
{item.date && (
<p className="text-sm text-muted-foreground">
{formatDate(item.date)}
</p>
)}
{isPending && !isCurrent && (
{isPending && !isCurrent && !isRejected && (
<p className="text-sm text-muted-foreground">Pending</p>
)}
</div>