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

@@ -42,6 +42,8 @@ import {
Camera,
Lock,
Bell,
Mail,
Calendar,
Trash2,
User,
Tags,
@@ -61,6 +63,10 @@ export default function ProfileSettingsPage() {
const [phoneNumber, setPhoneNumber] = useState('')
const [notificationPreference, setNotificationPreference] = useState('EMAIL')
const [expertiseTags, setExpertiseTags] = useState<string[]>([])
const [digestFrequency, setDigestFrequency] = useState('none')
const [preferredWorkload, setPreferredWorkload] = useState<number | null>(null)
const [availabilityStart, setAvailabilityStart] = useState('')
const [availabilityEnd, setAvailabilityEnd] = useState('')
const [profileLoaded, setProfileLoaded] = useState(false)
// Password form state
@@ -81,6 +87,13 @@ export default function ProfileSettingsPage() {
setPhoneNumber(user.phoneNumber || '')
setNotificationPreference(user.notificationPreference || 'EMAIL')
setExpertiseTags(user.expertiseTags || [])
setDigestFrequency(user.digestFrequency || 'none')
setPreferredWorkload(user.preferredWorkload ?? null)
const avail = user.availabilityJson as { startDate?: string; endDate?: string } | null
if (avail) {
setAvailabilityStart(avail.startDate || '')
setAvailabilityEnd(avail.endDate || '')
}
setProfileLoaded(true)
}
}, [user, profileLoaded])
@@ -93,6 +106,12 @@ export default function ProfileSettingsPage() {
phoneNumber: phoneNumber || null,
notificationPreference: notificationPreference as 'EMAIL' | 'WHATSAPP' | 'BOTH' | 'NONE',
expertiseTags,
digestFrequency: digestFrequency as 'none' | 'daily' | 'weekly',
preferredWorkload: preferredWorkload ?? undefined,
availabilityJson: (availabilityStart || availabilityEnd) ? {
startDate: availabilityStart || undefined,
endDate: availabilityEnd || undefined,
} : undefined,
})
toast.success('Profile updated successfully')
refetch()
@@ -275,6 +294,89 @@ export default function ProfileSettingsPage() {
</CardContent>
</Card>
{/* Email Digest */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="h-5 w-5" />
Email Digest
</CardTitle>
<CardDescription>
Receive periodic email summaries of pending actions and updates
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="digest-frequency">Digest Frequency</Label>
<Select value={digestFrequency} onValueChange={setDigestFrequency}>
<SelectTrigger id="digest-frequency">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No digest</SelectItem>
<SelectItem value="daily">Daily summary</SelectItem>
<SelectItem value="weekly">Weekly summary</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Get a summary of pending evaluations, upcoming deadlines, and recent activity
</p>
</div>
</CardContent>
</Card>
{/* Availability & Workload */}
{(user.role === 'JURY_MEMBER' || user.role === 'SUPER_ADMIN' || user.role === 'PROGRAM_ADMIN') && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Availability & Workload
</CardTitle>
<CardDescription>
Set your availability window and preferred evaluation workload
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="avail-start">Available From</Label>
<Input
id="avail-start"
type="date"
value={availabilityStart}
onChange={(e) => setAvailabilityStart(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="avail-end">Available Until</Label>
<Input
id="avail-end"
type="date"
value={availabilityEnd}
onChange={(e) => setAvailabilityEnd(e.target.value)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="workload">Preferred Workload (projects per round)</Label>
<Input
id="workload"
type="number"
min={1}
max={50}
value={preferredWorkload ?? ''}
onChange={(e) => setPreferredWorkload(e.target.value ? parseInt(e.target.value) : null)}
placeholder="e.g. 10"
/>
<p className="text-xs text-muted-foreground">
How many projects you prefer to evaluate per round
</p>
</div>
</CardContent>
</Card>
)}
{/* Expertise Tags */}
<Card>
<CardHeader>