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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user