feat(notifications): email notification preferences (#747)

* 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]>

---------
This commit is contained in:
Rotimi Best
2026-07-13 10:33:19 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 14db5363e7
commit fa060f5f6d
47 changed files with 14622 additions and 46 deletions
@@ -3395,7 +3395,8 @@
"domains_tab": "Domínios",
"teams_tab": "Equipes",
"ai_credits_tab": "AI Credits",
"ai_tutor_tab": "Tutor de IA"
"ai_tutor_tab": "Tutor de IA",
"notifications_tab": "Notifications"
},
"subheadings": {
"view_site": "Academia Aberta"
@@ -3569,6 +3570,75 @@
"preview": "{quantity} pack(s) · {tokens} credits · ${dollars}",
"buy_button": "Comprar por ${dollars}"
}
},
"notifications": {
"heading": "Notifications",
"page_title": "Notifications",
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
"toggles": {
"courseCompletion": {
"label": "Course completion and certificates",
"description": "Email students when they complete a course and earn a certificate."
},
"enrollmentWelcome": {
"label": "Enrollment welcome emails",
"description": "Send welcome emails when staff enroll students in courses or cohorts."
},
"session": {
"label": "Live session reminders and updates",
"description": "Email students before live sessions and when session details change."
},
"cohortReminder": {
"label": "Cohort goal reminders",
"description": "Email students about upcoming or overdue cohort learning goals."
},
"quizAssigned": {
"label": "Exercise assigned",
"description": "Email students when an exercise is assigned or nudged."
},
"newsfeed": {
"label": "Course announcements and comments",
"description": "Email course members about new posts and comments on the newsfeed."
},
"gradingResult": {
"label": "Grading results",
"description": "Email students when their submission is graded or updated."
},
"newSubmission": {
"label": "New submissions to grade",
"description": "Email tutors when a student submits work that needs grading."
},
"newStudent": {
"label": "New student joins",
"description": "Email tutors when a student enrolls in a course."
}
},
"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"
},
"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"
},
"sections": {
"enrollment": {
"heading": "Enrollment",
"description": "Welcome emails sent when staff add members to courses or cohorts."
},
"cohort": {
"heading": "Cohort",
"description": "Reminders about cohort learning goals and deadlines."
},
"course": {
"heading": "Course",
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
}
}
}
},
"profileMenu": {