Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n

Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher

Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download

All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 23:31:41 +01:00
parent f038c95777
commit 59436ed67a
68 changed files with 14541 additions and 546 deletions

View File

@@ -13,6 +13,7 @@ import {
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Users,
@@ -23,8 +24,11 @@ import {
GraduationCap,
Waves,
Crown,
CheckCircle2,
Circle,
Clock,
} from 'lucide-react'
import { getInitials, formatDateOnly } from '@/lib/utils'
import { formatDateOnly } from '@/lib/utils'
// Status badge colors
const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
@@ -36,6 +40,13 @@ const statusColors: Record<string, 'default' | 'secondary' | 'destructive' | 'ou
REJECTED: 'destructive',
}
// Completion status display
const completionBadge: Record<string, { label: string; variant: 'default' | 'secondary' | 'outline' }> = {
in_progress: { label: 'In Progress', variant: 'secondary' },
completed: { label: 'Completed', variant: 'default' },
paused: { label: 'Paused', variant: 'outline' },
}
function DashboardSkeleton() {
return (
<div className="space-y-6">
@@ -44,7 +55,8 @@ function DashboardSkeleton() {
<Skeleton className="h-4 w-64 mt-2" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<Skeleton className="h-24" />
<Skeleton className="h-24" />
<Skeleton className="h-24" />
</div>
@@ -66,6 +78,8 @@ export default function MentorDashboard() {
}
const projects = assignments || []
const completedCount = projects.filter((a) => a.completionStatus === 'completed').length
const inProgressCount = projects.filter((a) => a.completionStatus === 'in_progress').length
return (
<div className="space-y-6">
@@ -80,7 +94,7 @@ export default function MentorDashboard() {
</div>
{/* Stats */}
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
@@ -96,6 +110,29 @@ export default function MentorDashboard() {
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Completed
</CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completedCount}</div>
<div className="flex items-center gap-2 mt-1">
{projects.length > 0 && (
<Progress
value={(completedCount / projects.length) * 100}
className="h-1.5 flex-1"
/>
)}
<span className="text-xs text-muted-foreground">
{projects.length > 0 ? Math.round((completedCount / projects.length) * 100) : 0}%
</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
@@ -141,6 +178,7 @@ export default function MentorDashboard() {
const teamLead = project.teamMembers?.find(
(m) => m.role === 'LEAD'
)
const badge = completionBadge[assignment.completionStatus] || completionBadge.in_progress
return (
<Card key={assignment.id}>
@@ -153,12 +191,12 @@ export default function MentorDashboard() {
</span>
{project.round && (
<>
<span></span>
<span>-</span>
<span>{project.round.name}</span>
</>
)}
</div>
<CardTitle className="flex items-center gap-2">
<CardTitle className="flex items-center gap-2 flex-wrap">
{project.title}
{project.status && (
<Badge
@@ -167,6 +205,18 @@ export default function MentorDashboard() {
{project.status.replace('_', ' ')}
</Badge>
)}
<Badge variant={badge.variant}>
{assignment.completionStatus === 'completed' && (
<CheckCircle2 className="mr-1 h-3 w-3" />
)}
{assignment.completionStatus === 'in_progress' && (
<Circle className="mr-1 h-3 w-3" />
)}
{assignment.completionStatus === 'paused' && (
<Clock className="mr-1 h-3 w-3" />
)}
{badge.label}
</Badge>
</CardTitle>
{project.teamName && (
<CardDescription>{project.teamName}</CardDescription>
@@ -242,10 +292,13 @@ export default function MentorDashboard() {
</div>
)}
{/* Assignment date */}
<p className="text-xs text-muted-foreground">
Assigned {formatDateOnly(assignment.assignedAt)}
</p>
{/* Assignment date + last viewed */}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Assigned {formatDateOnly(assignment.assignedAt)}</span>
{assignment.lastViewedAt && (
<span>Last viewed {formatDateOnly(assignment.lastViewedAt)}</span>
)}
</div>
</CardContent>
</Card>
)