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

321
messages/en.json Normal file
View File

@@ -0,0 +1,321 @@
{
"common": {
"loading": "Loading...",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"close": "Close",
"confirm": "Confirm",
"back": "Back",
"next": "Next",
"submit": "Submit",
"search": "Search",
"filter": "Filter",
"export": "Export",
"import": "Import",
"refresh": "Refresh",
"actions": "Actions",
"status": "Status",
"name": "Name",
"email": "Email",
"role": "Role",
"date": "Date",
"description": "Description",
"settings": "Settings",
"yes": "Yes",
"no": "No",
"all": "All",
"none": "None",
"noResults": "No results found",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"required": "Required",
"optional": "Optional",
"total": "Total",
"page": "Page",
"of": "of",
"showing": "Showing",
"entries": "entries",
"perPage": "per page",
"language": "Language",
"english": "English",
"french": "French"
},
"auth": {
"signIn": "Sign In",
"signOut": "Sign Out",
"signUp": "Sign Up",
"email": "Email Address",
"password": "Password",
"forgotPassword": "Forgot Password?",
"resetPassword": "Reset Password",
"newPassword": "New Password",
"confirmPassword": "Confirm Password",
"currentPassword": "Current Password",
"magicLink": "Sign in with Magic Link",
"magicLinkSent": "Check your email for a sign-in link",
"invalidCredentials": "Invalid email or password",
"accountLocked": "Account locked. Try again later.",
"setPassword": "Set Password",
"passwordRequirements": "Password must be at least 8 characters",
"passwordsDoNotMatch": "Passwords do not match",
"welcomeBack": "Welcome back",
"signInDescription": "Enter your email to sign in to your account",
"orContinueWith": "Or continue with"
},
"nav": {
"dashboard": "Dashboard",
"programs": "Programs",
"rounds": "Rounds",
"projects": "Projects",
"users": "Users",
"evaluations": "Evaluations",
"assignments": "Assignments",
"analytics": "Analytics",
"settings": "Settings",
"audit": "Audit Log",
"myProjects": "My Projects",
"myEvaluations": "My Evaluations",
"profile": "Profile",
"help": "Help",
"notifications": "Notifications",
"mentoring": "Mentoring",
"liveVoting": "Live Voting",
"applications": "Applications",
"messages": "Messages"
},
"dashboard": {
"title": "Dashboard",
"welcome": "Welcome, {name}",
"overview": "Overview",
"recentActivity": "Recent Activity",
"pendingEvaluations": "Pending Evaluations",
"completedEvaluations": "Completed Evaluations",
"totalProjects": "Total Projects",
"activeRounds": "Active Rounds",
"assignedProjects": "Assigned Projects",
"upcomingDeadlines": "Upcoming Deadlines",
"quickActions": "Quick Actions",
"noActivity": "No recent activity"
},
"programs": {
"title": "Programs",
"createProgram": "Create Program",
"editProgram": "Edit Program",
"programName": "Program Name",
"year": "Year",
"status": "Status",
"rounds": "Rounds",
"projects": "Projects",
"noPrograms": "No programs found",
"deleteConfirm": "Are you sure you want to delete this program?"
},
"rounds": {
"title": "Rounds",
"createRound": "Create Round",
"editRound": "Edit Round",
"roundName": "Round Name",
"roundType": "Round Type",
"startDate": "Start Date",
"endDate": "End Date",
"votingWindow": "Voting Window",
"criteria": "Evaluation Criteria",
"status": "Status",
"active": "Active",
"closed": "Closed",
"upcoming": "Upcoming",
"noRounds": "No rounds found"
},
"projects": {
"title": "Projects",
"createProject": "Create Project",
"editProject": "Edit Project",
"projectName": "Project Title",
"teamName": "Team Name",
"country": "Country",
"category": "Category",
"status": "Status",
"description": "Description",
"files": "Files",
"evaluations": "Evaluations",
"noProjects": "No projects found",
"importCsv": "Import CSV",
"bulkStatusUpdate": "Bulk Status Update",
"viewDetails": "View Details",
"assignMentor": "Assign Mentor",
"oceanIssue": "Ocean Issue"
},
"evaluations": {
"title": "Evaluations",
"submitEvaluation": "Submit Evaluation",
"draft": "Draft",
"submitted": "Submitted",
"score": "Score",
"feedback": "Feedback",
"criteria": "Criteria",
"globalScore": "Global Score",
"decision": "Decision",
"recommend": "Recommend",
"doNotRecommend": "Do Not Recommend",
"saveAsDraft": "Save as Draft",
"finalSubmit": "Final Submit",
"confirmSubmit": "Are you sure? This action cannot be undone.",
"progress": "Progress",
"completionRate": "Completion Rate",
"noEvaluations": "No evaluations yet",
"evaluationSummary": "Evaluation Summary",
"strengths": "Strengths",
"weaknesses": "Weaknesses",
"overallAssessment": "Overall Assessment"
},
"users": {
"title": "Users",
"createUser": "Create User",
"editUser": "Edit User",
"inviteUser": "Invite User",
"bulkImport": "Bulk Import",
"sendInvitation": "Send Invitation",
"resendInvitation": "Resend Invitation",
"role": "Role",
"status": "Status",
"active": "Active",
"invited": "Invited",
"suspended": "Suspended",
"noUsers": "No users found",
"expertiseTags": "Expertise Tags",
"maxAssignments": "Max Assignments",
"lastLogin": "Last Login",
"deleteConfirm": "Are you sure you want to delete this user?"
},
"assignments": {
"title": "Assignments",
"assign": "Assign",
"unassign": "Unassign",
"bulkAssign": "Bulk Assign",
"smartAssign": "Smart Assignment",
"manual": "Manual",
"algorithm": "Algorithm",
"aiAuto": "AI Auto",
"noAssignments": "No assignments",
"assignedTo": "Assigned to",
"assignedBy": "Assigned by"
},
"files": {
"title": "Files",
"upload": "Upload File",
"download": "Download",
"delete": "Delete File",
"fileName": "File Name",
"fileType": "File Type",
"fileSize": "File Size",
"uploadDate": "Upload Date",
"noFiles": "No files uploaded",
"dragAndDrop": "Drag and drop files here",
"maxSize": "Maximum file size: {size}",
"version": "Version",
"versionHistory": "Version History",
"replaceFile": "Replace File",
"bulkDownload": "Bulk Download"
},
"settings": {
"title": "Settings",
"general": "General",
"branding": "Branding",
"email": "Email",
"security": "Security",
"ai": "AI Configuration",
"storage": "Storage",
"language": "Language",
"defaultLanguage": "Default Language",
"availableLanguages": "Available Languages",
"languageDescription": "Configure the platform's language settings",
"saved": "Settings saved successfully",
"saveFailed": "Failed to save settings"
},
"liveVoting": {
"title": "Live Voting",
"session": "Session",
"startVoting": "Start Voting",
"stopVoting": "Stop Voting",
"endSession": "End Session",
"timeRemaining": "Time Remaining",
"castVote": "Cast Your Vote",
"voteSubmitted": "Vote submitted",
"results": "Results",
"juryScore": "Jury Score",
"audienceScore": "Audience Score",
"weightedTotal": "Weighted Total",
"noVotes": "No votes yet",
"votingClosed": "Voting has closed",
"presentationSettings": "Presentation Settings",
"audienceVoting": "Audience Voting"
},
"mentor": {
"title": "Mentoring",
"myMentees": "My Mentees",
"projectDetails": "Project Details",
"sendMessage": "Send Message",
"notes": "Notes",
"addNote": "Add Note",
"milestones": "Milestones",
"completeMilestone": "Mark Complete",
"activity": "Activity",
"lastViewed": "Last Viewed",
"noMentees": "No mentees assigned"
},
"profile": {
"title": "Profile",
"editProfile": "Edit Profile",
"name": "Full Name",
"email": "Email",
"phone": "Phone Number",
"country": "Country",
"bio": "Bio",
"expertise": "Areas of Expertise",
"notifications": "Notification Preferences",
"digestFrequency": "Digest Frequency",
"availability": "Availability",
"workload": "Preferred Workload",
"changePassword": "Change Password",
"deleteAccount": "Delete Account",
"deleteAccountConfirm": "This action cannot be undone. All your data will be permanently deleted."
},
"onboarding": {
"welcome": "Welcome to MOPC",
"setupProfile": "Let's set up your profile",
"step1": "Personal Information",
"step2": "Expertise & Preferences",
"step3": "Review & Complete",
"complete": "Complete Setup",
"skip": "Skip for now"
},
"errors": {
"generic": "Something went wrong. Please try again.",
"notFound": "Page not found",
"unauthorized": "You are not authorized to access this page",
"forbidden": "Access denied",
"serverError": "Internal server error",
"networkError": "Network error. Please check your connection.",
"sessionExpired": "Your session has expired. Please sign in again.",
"validationError": "Please check the form for errors"
},
"notifications": {
"title": "Notifications",
"markAllRead": "Mark all as read",
"noNotifications": "No notifications",
"viewAll": "View all notifications"
},
"coi": {
"title": "Conflict of Interest",
"declaration": "COI Declaration",
"declareConflict": "Declare Conflict of Interest",
"noConflict": "No conflict of interest",
"hasConflict": "Conflict declared",
"reason": "Reason for conflict",
"confirmDeclaration": "I confirm this declaration is accurate"
}
}

321
messages/fr.json Normal file
View File

@@ -0,0 +1,321 @@
{
"common": {
"loading": "Chargement...",
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer",
"edit": "Modifier",
"create": "Cr\u00e9er",
"close": "Fermer",
"confirm": "Confirmer",
"back": "Retour",
"next": "Suivant",
"submit": "Soumettre",
"search": "Rechercher",
"filter": "Filtrer",
"export": "Exporter",
"import": "Importer",
"refresh": "Actualiser",
"actions": "Actions",
"status": "Statut",
"name": "Nom",
"email": "E-mail",
"role": "R\u00f4le",
"date": "Date",
"description": "Description",
"settings": "Param\u00e8tres",
"yes": "Oui",
"no": "Non",
"all": "Tous",
"none": "Aucun",
"noResults": "Aucun r\u00e9sultat trouv\u00e9",
"error": "Erreur",
"success": "Succ\u00e8s",
"warning": "Avertissement",
"info": "Information",
"required": "Obligatoire",
"optional": "Facultatif",
"total": "Total",
"page": "Page",
"of": "de",
"showing": "Affichage de",
"entries": "entr\u00e9es",
"perPage": "par page",
"language": "Langue",
"english": "Anglais",
"french": "Fran\u00e7ais"
},
"auth": {
"signIn": "Se connecter",
"signOut": "Se d\u00e9connecter",
"signUp": "S'inscrire",
"email": "Adresse e-mail",
"password": "Mot de passe",
"forgotPassword": "Mot de passe oubli\u00e9 ?",
"resetPassword": "R\u00e9initialiser le mot de passe",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmer le mot de passe",
"currentPassword": "Mot de passe actuel",
"magicLink": "Se connecter avec un lien magique",
"magicLinkSent": "V\u00e9rifiez votre e-mail pour un lien de connexion",
"invalidCredentials": "E-mail ou mot de passe invalide",
"accountLocked": "Compte verrouill\u00e9. R\u00e9essayez plus tard.",
"setPassword": "D\u00e9finir le mot de passe",
"passwordRequirements": "Le mot de passe doit contenir au moins 8 caract\u00e8res",
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
"welcomeBack": "Bon retour",
"signInDescription": "Entrez votre e-mail pour vous connecter \u00e0 votre compte",
"orContinueWith": "Ou continuer avec"
},
"nav": {
"dashboard": "Tableau de bord",
"programs": "Programmes",
"rounds": "Tours",
"projects": "Projets",
"users": "Utilisateurs",
"evaluations": "\u00c9valuations",
"assignments": "Affectations",
"analytics": "Analytique",
"settings": "Param\u00e8tres",
"audit": "Journal d'audit",
"myProjects": "Mes projets",
"myEvaluations": "Mes \u00e9valuations",
"profile": "Profil",
"help": "Aide",
"notifications": "Notifications",
"mentoring": "Mentorat",
"liveVoting": "Vote en direct",
"applications": "Candidatures",
"messages": "Messages"
},
"dashboard": {
"title": "Tableau de bord",
"welcome": "Bienvenue, {name}",
"overview": "Aper\u00e7u",
"recentActivity": "Activit\u00e9 r\u00e9cente",
"pendingEvaluations": "\u00c9valuations en attente",
"completedEvaluations": "\u00c9valuations termin\u00e9es",
"totalProjects": "Total des projets",
"activeRounds": "Tours actifs",
"assignedProjects": "Projets assign\u00e9s",
"upcomingDeadlines": "\u00c9ch\u00e9ances \u00e0 venir",
"quickActions": "Actions rapides",
"noActivity": "Aucune activit\u00e9 r\u00e9cente"
},
"programs": {
"title": "Programmes",
"createProgram": "Cr\u00e9er un programme",
"editProgram": "Modifier le programme",
"programName": "Nom du programme",
"year": "Ann\u00e9e",
"status": "Statut",
"rounds": "Tours",
"projects": "Projets",
"noPrograms": "Aucun programme trouv\u00e9",
"deleteConfirm": "\u00cates-vous s\u00fbr de vouloir supprimer ce programme ?"
},
"rounds": {
"title": "Tours",
"createRound": "Cr\u00e9er un tour",
"editRound": "Modifier le tour",
"roundName": "Nom du tour",
"roundType": "Type de tour",
"startDate": "Date de d\u00e9but",
"endDate": "Date de fin",
"votingWindow": "Fen\u00eatre de vote",
"criteria": "Crit\u00e8res d'\u00e9valuation",
"status": "Statut",
"active": "Actif",
"closed": "Cl\u00f4tur\u00e9",
"upcoming": "\u00c0 venir",
"noRounds": "Aucun tour trouv\u00e9"
},
"projects": {
"title": "Projets",
"createProject": "Cr\u00e9er un projet",
"editProject": "Modifier le projet",
"projectName": "Titre du projet",
"teamName": "Nom de l'\u00e9quipe",
"country": "Pays",
"category": "Cat\u00e9gorie",
"status": "Statut",
"description": "Description",
"files": "Fichiers",
"evaluations": "\u00c9valuations",
"noProjects": "Aucun projet trouv\u00e9",
"importCsv": "Importer CSV",
"bulkStatusUpdate": "Mise \u00e0 jour en masse du statut",
"viewDetails": "Voir les d\u00e9tails",
"assignMentor": "Assigner un mentor",
"oceanIssue": "Probl\u00e9matique oc\u00e9anique"
},
"evaluations": {
"title": "\u00c9valuations",
"submitEvaluation": "Soumettre l'\u00e9valuation",
"draft": "Brouillon",
"submitted": "Soumis",
"score": "Note",
"feedback": "Commentaires",
"criteria": "Crit\u00e8res",
"globalScore": "Note globale",
"decision": "D\u00e9cision",
"recommend": "Recommander",
"doNotRecommend": "Ne pas recommander",
"saveAsDraft": "Enregistrer comme brouillon",
"finalSubmit": "Soumission finale",
"confirmSubmit": "\u00cates-vous s\u00fbr ? Cette action est irr\u00e9versible.",
"progress": "Progression",
"completionRate": "Taux de compl\u00e9tion",
"noEvaluations": "Aucune \u00e9valuation pour le moment",
"evaluationSummary": "R\u00e9sum\u00e9 de l'\u00e9valuation",
"strengths": "Points forts",
"weaknesses": "Points faibles",
"overallAssessment": "\u00c9valuation globale"
},
"users": {
"title": "Utilisateurs",
"createUser": "Cr\u00e9er un utilisateur",
"editUser": "Modifier l'utilisateur",
"inviteUser": "Inviter un utilisateur",
"bulkImport": "Importation en masse",
"sendInvitation": "Envoyer l'invitation",
"resendInvitation": "Renvoyer l'invitation",
"role": "R\u00f4le",
"status": "Statut",
"active": "Actif",
"invited": "Invit\u00e9",
"suspended": "Suspendu",
"noUsers": "Aucun utilisateur trouv\u00e9",
"expertiseTags": "Tags d'expertise",
"maxAssignments": "Affectations max.",
"lastLogin": "Derni\u00e8re connexion",
"deleteConfirm": "\u00cates-vous s\u00fbr de vouloir supprimer cet utilisateur ?"
},
"assignments": {
"title": "Affectations",
"assign": "Affecter",
"unassign": "D\u00e9saffecter",
"bulkAssign": "Affectation en masse",
"smartAssign": "Affectation intelligente",
"manual": "Manuel",
"algorithm": "Algorithme",
"aiAuto": "IA automatique",
"noAssignments": "Aucune affectation",
"assignedTo": "Affect\u00e9 \u00e0",
"assignedBy": "Affect\u00e9 par"
},
"files": {
"title": "Fichiers",
"upload": "T\u00e9l\u00e9charger un fichier",
"download": "T\u00e9l\u00e9charger",
"delete": "Supprimer le fichier",
"fileName": "Nom du fichier",
"fileType": "Type de fichier",
"fileSize": "Taille du fichier",
"uploadDate": "Date de t\u00e9l\u00e9chargement",
"noFiles": "Aucun fichier t\u00e9l\u00e9charg\u00e9",
"dragAndDrop": "Glissez-d\u00e9posez les fichiers ici",
"maxSize": "Taille maximale du fichier : {size}",
"version": "Version",
"versionHistory": "Historique des versions",
"replaceFile": "Remplacer le fichier",
"bulkDownload": "T\u00e9l\u00e9chargement en masse"
},
"settings": {
"title": "Param\u00e8tres",
"general": "G\u00e9n\u00e9ral",
"branding": "Image de marque",
"email": "E-mail",
"security": "S\u00e9curit\u00e9",
"ai": "Configuration IA",
"storage": "Stockage",
"language": "Langue",
"defaultLanguage": "Langue par d\u00e9faut",
"availableLanguages": "Langues disponibles",
"languageDescription": "Configurer les param\u00e8tres de langue de la plateforme",
"saved": "Param\u00e8tres enregistr\u00e9s avec succ\u00e8s",
"saveFailed": "\u00c9chec de l'enregistrement des param\u00e8tres"
},
"liveVoting": {
"title": "Vote en direct",
"session": "Session",
"startVoting": "D\u00e9marrer le vote",
"stopVoting": "Arr\u00eater le vote",
"endSession": "Terminer la session",
"timeRemaining": "Temps restant",
"castVote": "Voter",
"voteSubmitted": "Vote soumis",
"results": "R\u00e9sultats",
"juryScore": "Note du jury",
"audienceScore": "Note du public",
"weightedTotal": "Total pond\u00e9r\u00e9",
"noVotes": "Aucun vote pour le moment",
"votingClosed": "Le vote est clos",
"presentationSettings": "Param\u00e8tres de pr\u00e9sentation",
"audienceVoting": "Vote du public"
},
"mentor": {
"title": "Mentorat",
"myMentees": "Mes mentees",
"projectDetails": "D\u00e9tails du projet",
"sendMessage": "Envoyer un message",
"notes": "Notes",
"addNote": "Ajouter une note",
"milestones": "Jalons",
"completeMilestone": "Marquer comme termin\u00e9",
"activity": "Activit\u00e9",
"lastViewed": "Derni\u00e8re consultation",
"noMentees": "Aucun mentee assign\u00e9"
},
"profile": {
"title": "Profil",
"editProfile": "Modifier le profil",
"name": "Nom complet",
"email": "E-mail",
"phone": "Num\u00e9ro de t\u00e9l\u00e9phone",
"country": "Pays",
"bio": "Biographie",
"expertise": "Domaines d'expertise",
"notifications": "Pr\u00e9f\u00e9rences de notification",
"digestFrequency": "Fr\u00e9quence du digest",
"availability": "Disponibilit\u00e9",
"workload": "Charge de travail pr\u00e9f\u00e9r\u00e9e",
"changePassword": "Changer le mot de passe",
"deleteAccount": "Supprimer le compte",
"deleteAccountConfirm": "Cette action est irr\u00e9versible. Toutes vos donn\u00e9es seront d\u00e9finitivement supprim\u00e9es."
},
"onboarding": {
"welcome": "Bienvenue sur MOPC",
"setupProfile": "Configurons votre profil",
"step1": "Informations personnelles",
"step2": "Expertise et pr\u00e9f\u00e9rences",
"step3": "V\u00e9rification et finalisation",
"complete": "Terminer la configuration",
"skip": "Passer pour le moment"
},
"errors": {
"generic": "Une erreur s'est produite. Veuillez r\u00e9essayer.",
"notFound": "Page non trouv\u00e9e",
"unauthorized": "Vous n'\u00eates pas autoris\u00e9 \u00e0 acc\u00e9der \u00e0 cette page",
"forbidden": "Acc\u00e8s refus\u00e9",
"serverError": "Erreur interne du serveur",
"networkError": "Erreur r\u00e9seau. V\u00e9rifiez votre connexion.",
"sessionExpired": "Votre session a expir\u00e9. Veuillez vous reconnecter.",
"validationError": "Veuillez v\u00e9rifier le formulaire pour les erreurs"
},
"notifications": {
"title": "Notifications",
"markAllRead": "Tout marquer comme lu",
"noNotifications": "Aucune notification",
"viewAll": "Voir toutes les notifications"
},
"coi": {
"title": "Conflit d'int\u00e9r\u00eats",
"declaration": "D\u00e9claration de COI",
"declareConflict": "D\u00e9clarer un conflit d'int\u00e9r\u00eats",
"noConflict": "Aucun conflit d'int\u00e9r\u00eats",
"hasConflict": "Conflit d\u00e9clar\u00e9",
"reason": "Raison du conflit",
"confirmDeclaration": "Je confirme que cette d\u00e9claration est exacte"
}
}