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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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": "Đội",
"ai_credits_tab": "AI Credits",
"ai_tutor_tab": "Gia sư AI",
"notifications_tab": "Notifications"
"notifications_tab": "Thông báo"
},
"subheadings": {
"view_site": "Học viện mở"
@@ -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": "Thông báo",
"page_title": "Thông báo",
"page_subtitle": "Chọn thông báo email bạn nhận và, với tư cách quản trị viên, email mà tổ chức của bạn gửi.",
"toggles": {
"courseCompletion": {
"label": "Course completion and certificates",
"description": "Email students when they complete a course and earn a certificate."
"label": "Hoàn thành khóa học và chứng chỉ",
"description": "Gửi email cho học viên khi họ hoàn thành khóa học và nhận chứng chỉ."
},
"enrollmentWelcome": {
"label": "Enrollment welcome emails",
"description": "Send welcome emails when staff enroll students in courses or cohorts."
"label": "Email chào mừng ghi danh",
"description": "Gửi email chào mừng khi nhân viên ghi danh học viên vào khóa học hoặc nhóm học."
},
"session": {
"label": "Live session reminders and updates",
"description": "Email students before live sessions and when session details change."
"label": "Nhắc nhở và cập nhật buổi học trực tiếp",
"description": "Gửi email cho học viên trước buổi học trực tiếp và khi chi tiết buổi học thay đổi."
},
"cohortReminder": {
"label": "Cohort goal reminders",
"description": "Email students about upcoming or overdue cohort learning goals."
"label": "Nhắc nhở mục tiêu nhóm học",
"description": "Gửi email cho học viên về mục tiêu học tập sắp tới hoặc quá hạn."
},
"quizAssigned": {
"label": "Exercise assigned",
"description": "Email students when an exercise is assigned or nudged."
"label": "Bài tập được giao",
"description": "Gửi email cho học viên khi bài tập được giao hoặc nhắc nhở."
},
"newsfeed": {
"label": "Course announcements and comments",
"description": "Email course members about new posts and comments on the newsfeed."
"label": "Thông báo và bình luận khóa học",
"description": "Gửi email cho thành viên khóa học về bài đăng và bình luận mới trên bảng tin."
},
"gradingResult": {
"label": "Grading results",
"description": "Email students when their submission is graded or updated."
"label": "Kết quả chấm điểm",
"description": "Gửi email cho học viên khi bài nộp được chấm hoặc cập nhật."
},
"newSubmission": {
"label": "New submissions to grade",
"description": "Email tutors when a student submits work that needs grading."
"label": "Bài nộp mới cần chấm",
"description": "Gửi email cho gia sư khi học viên nộp bài cần chấm điểm."
},
"newStudent": {
"label": "New student joins",
"description": "Email tutors when a student enrolls in a course."
"label": "Học viên mới tham gia",
"description": "Gửi email cho gia sư khi học viên ghi danh vào khóa học."
}
},
"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": "Mặc định của tổ chức",
"description": "Khi tắt ở đây, không ai trong tổ chức của bạn nhận email đó. Thành viên vẫn có thể từ chối các email bạn để bật.",
"save": "Lưu mặc định của tổ chức",
"save_success": "Đã lưu mặc định thông báo của tổ chức"
},
"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": "Tùy chọn email của bạn",
"save": "Lưu tùy chọn cá nhân",
"save_success": "Đã lưu tùy chọn thông báo cá nhân",
"description": "Áp dụng cho email gửi đến bạn trong tổ chức này. Tổ chức của bạn vẫn có thể tắt một số email cho mọi người.",
"description_with_org": "Áp dụng cho email gửi đến bạn trong {orgName}. Tổ chức của bạn vẫn có thể tắt một số email cho mọi người."
},
"sections": {
"enrollment": {
"heading": "Enrollment",
"description": "Welcome emails sent when staff add members to courses or cohorts."
"heading": "Ghi danh",
"description": "Email chào mừng được gửi khi nhân viên thêm thành viên vào khóa học hoặc nhóm học."
},
"cohort": {
"heading": "Cohort",
"description": "Reminders about cohort learning goals and deadlines."
"heading": "Nhóm học",
"description": "Nhắc nhở về mục tiêu học tập và hạn chót của nhóm học."
},
"course": {
"heading": "Course",
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
"heading": "Khóa học",
"description": "Ghi danh, bài nộp, bài học, thông báo và hoàn thành trong các khóa học của bạn."
}
}
},
"personal_page_subtitle": "Chọn thông báo email bạn nhận từ ClassroomIO."
}
},
"profileMenu": {