feat(notifications): per-org member email preferences table (#751)

* feat(notifications): add email notification preferences

Implement the email notification preferences PRD with org-level defaults
and personal opt-outs enforced at the email enqueue chokepoint.

- Add toggle map, profile.settings jsonb, and shouldSendEmail resolver
- Extend profile and organization update APIs with emailNotifications
- Pass preference context through all toggleable email send call sites
- Add Settings > Notifications page with personal and org admin sections

Co-authored-by: Rotimi Best <[email protected]>

* feat(notifications): group email toggles into course, cohort, and enrollment sections

Organize personal and org notification settings into scannable sections
so users can find course- or cohort-related emails quickly.

Co-authored-by: Rotimi Best <[email protected]>

* fix(notifications): use Exercise instead of Quiz in toggle copy

Co-authored-by: Rotimi Best <[email protected]>

* fix(notifications): only mark certification email sent when enqueued

Skip setMemberCertificationEmailSent when notification preferences or
missing Redis prevent the completion email from being queued, so the
email can still send if preferences change later.

Co-authored-by: Rotimi Best <[email protected]>

* fix(notifications): address PR review feedback

- Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch
  processors and multi-recipient email enqueue paths
- Replace $effect store hydration in notifications settings with onMount
  and explicit sync after save
- Remove thin api email-preferences re-export; import from db queries
- Hide tutor-only personal toggles (newStudent, newSubmission) for students

Co-authored-by: Rotimi Best <[email protected]>

* fix(notifications): hydrate toggles after store load and add profile settings migration

- One-time store hydration via onMount plus guarded effect when profile/org
  load after mount, without clobbering in-progress edits
- Add drizzle migration for profile.settings jsonb column

Co-authored-by: Rotimi Best <[email protected]>

* feat(notifications): add email preferences to LMS settings

Students manage personal email opt-outs from /lms/settings/notifications,
matching the org settings page but scoped to the LMS navigation students use.

Co-authored-by: Rotimi Best <[email protected]>

* feat(notifications): store per-org personal prefs in organizationmember_email_notifications

- Add organizationmember_email_notifications table keyed by organizationmember
- Expose GET/PUT /organization/member/email-notifications for current org member
- Resolve personal email prefs by profileId + organizationId at send time
- Migrate legacy profile.settings.emailNotifications into per-membership rows
- Update org and LMS settings UI to load/save org-scoped member preferences

Co-authored-by: Rotimi Best <[email protected]>

* fix(notifications): address PR review on locales and backfill script

Move profile email-notification backfill out of the migration SQL into
a dedicated db script with dry-run support. Translate personal preference
description strings in all non-English locale files.

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): translate notification preference strings in all locales

Complete settings.notifications and settings.tabs.notifications_tab
translations for da, de, es, fr, hi, pl, pt, ru, and vi.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-16 10:04:28 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 9681770270
commit f3d1a885b9
36 changed files with 13197 additions and 475 deletions
@@ -3463,7 +3463,7 @@
"teams_tab": "Команды",
"ai_credits_tab": "AI Credits",
"ai_tutor_tab": "Репетитор по искусственному интеллекту",
"notifications_tab": "Notifications"
"notifications_tab": "Уведомления"
},
"subheadings": {
"view_site": "Открытая Академия"
@@ -3639,73 +3639,75 @@
}
},
"notifications": {
"heading": "Notifications",
"page_title": "Notifications",
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
"heading": "Уведомления",
"page_title": "Уведомления",
"page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете, а как администратор — какие письма отправляет ваша организация.",
"toggles": {
"courseCompletion": {
"label": "Course completion and certificates",
"description": "Email students when they complete a course and earn a certificate."
"label": "Завершение курса и сертификаты",
"description": "Отправлять письмо студентам, когда они завершают курс и получают сертификат."
},
"enrollmentWelcome": {
"label": "Enrollment welcome emails",
"description": "Send welcome emails when staff enroll students in courses or cohorts."
"label": "Приветственные письма при записи",
"description": "Отправлять приветственные письма, когда сотрудники записывают студентов на курсы или когорты."
},
"session": {
"label": "Live session reminders and updates",
"description": "Email students before live sessions and when session details change."
"label": "Напоминания и обновления живых сессий",
"description": "Отправлять письмо студентам перед живыми сессиями и при изменении деталей сессии."
},
"cohortReminder": {
"label": "Cohort goal reminders",
"description": "Email students about upcoming or overdue cohort learning goals."
"label": "Напоминания о целях когорты",
"description": "Отправлять письмо студентам о предстоящих или просроченных целях обучения когорты."
},
"quizAssigned": {
"label": "Exercise assigned",
"description": "Email students when an exercise is assigned or nudged."
"label": "Назначено упражнение",
"description": "Отправлять письмо студентам, когда упражнение назначено или напоминается."
},
"newsfeed": {
"label": "Course announcements and comments",
"description": "Email course members about new posts and comments on the newsfeed."
"label": "Объявления курса и комментарии",
"description": "Отправлять письмо участникам курса о новых публикациях и комментариях в ленте."
},
"gradingResult": {
"label": "Grading results",
"description": "Email students when their submission is graded or updated."
"label": "Результаты оценки",
"description": "Отправлять письмо студентам, когда их работа оценена или обновлена."
},
"newSubmission": {
"label": "New submissions to grade",
"description": "Email tutors when a student submits work that needs grading."
"label": "Новые работы для оценки",
"description": "Отправлять письмо наставникам, когда студент сдаёт работу, требующую оценки."
},
"newStudent": {
"label": "New student joins",
"description": "Email tutors when a student enrolls in a course."
"label": "Новый студент записывается",
"description": "Отправлять письмо наставникам, когда студент записывается на курс."
}
},
"org": {
"heading": "Organization defaults",
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
"save": "Save organization defaults",
"save_success": "Organization notification defaults saved"
"heading": "Настройки организации по умолчанию",
"description": "Если отключено здесь, никто в вашей организации не получит это письмо. Участники по-прежнему могут отказаться от писем, которые вы оставили включёнными.",
"save": "Сохранить настройки организации по умолчанию",
"save_success": "Настройки уведомлений организации сохранены"
},
"personal": {
"heading": "Your email preferences",
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
"save": "Save personal preferences",
"save_success": "Personal notification preferences saved"
"heading": "Ваши настройки электронной почты",
"save": "Сохранить личные настройки",
"save_success": "Личные настройки уведомлений сохранены",
"description": "Они применяются к письмам, которые вам отправляют в этой организации. Организация по-прежнему может отключить некоторые письма для всех.",
"description_with_org": "Они применяются к письмам, которые вам отправляют в {orgName}. Организация по-прежнему может отключить некоторые письма для всех."
},
"sections": {
"enrollment": {
"heading": "Enrollment",
"description": "Welcome emails sent when staff add members to courses or cohorts."
"heading": "Запись",
"description": "Приветственные письма, отправляемые, когда сотрудники добавляют участников в курсы или когорты."
},
"cohort": {
"heading": "Cohort",
"description": "Reminders about cohort learning goals and deadlines."
"heading": "Когорта",
"description": "Напоминания о целях обучения когорты и сроках."
},
"course": {
"heading": "Course",
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
"heading": "Курс",
"description": "Записи, сдачи, уроки, объявления и завершения в ваших курсах."
}
}
},
"personal_page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете от ClassroomIO."
}
},
"profileMenu": {