From 9681770270bb5ef57d181a819067250d6815f23c Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 16 Jul 2026 09:56:35 +0100 Subject: [PATCH 1/4] fix(dashboard): show locked notice instead of teacher empty state on lessons (#760) * fix(dashboard): show locked notice instead of teacher empty state on lessons Students viewing a locked or progression-blocked lesson were falling through to the teacher empty state with Get Started. Align lesson access checks with the shared getStudentContentLockReason helper used by exercises and the course layout, treat missing course content as inaccessible for students, and use org student role for sidebar lock blocking on the app host. Co-authored-by: Rotimi Best * fix(dashboard): only show lesson lock notice when lock reason is known Address Greptile review: avoid defaulting to teacher_locked when the lesson is missing from the content tree, and wait for course/lesson state before dismissing the student loading spinner. Co-authored-by: Rotimi Best --------- ]Co-authored-by: Rotimi Best --- .../lesson/content-navigation-actions.svelte | 8 ++- .../sidebar/course-content-tree.svelte | 7 ++- .../lib/features/course/pages/lesson.svelte | 60 +++++++++++++------ .../[id]/lessons/[lessonId]/+page.svelte | 11 +++- 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte b/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte index 105cec4b..e4368b49 100644 --- a/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte +++ b/apps/dashboard/src/lib/features/course/components/lesson/content-navigation-actions.svelte @@ -8,6 +8,7 @@ import * as Tooltip from '@cio/ui/base/tooltip'; import { PercentRingProgress } from '@cio/ui/custom/percent-ring-progress'; import { isOrgStudent } from '$lib/utils/store/app'; + import { getStudentContentLockReason } from '$features/ai-assistant/utils/content-ask-ai-bar'; import { t } from '$lib/utils/functions/translations'; import { courseApi, lessonApi } from '$features/course/api'; import { getOrderedNavigableContent, getContentRoute } from '$features/course/utils/content'; @@ -69,8 +70,10 @@ const showMarkComplete = $derived(!!lessonId && !exerciseId); const currentLessonItem = $derived(lessonId ? lessonItems.find((l) => l.id === lessonId) : null); - const isTeacherLocked = $derived($isOrgStudent && currentLessonItem && !(currentLessonItem.isUnlocked ?? false)); - const isProgressionLocked = $derived($isOrgStudent && currentLessonItem?.accessible === false && !isTeacherLocked); + const contentLockReason = $derived( + lessonId ? getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson) : null + ); + const isLessonLocked = $derived($isOrgStudent === true && contentLockReason !== null); const isVideoWatchLesson = $derived.by(() => { if (!lessonId) return false; @@ -98,7 +101,6 @@ const watchVideosRequired = $derived(lessonApi.lesson?.watchProgress?.videosRequired ?? 0); const watchVideosComplete = $derived(lessonApi.lesson?.watchProgress?.videosComplete ?? 0); - const isLessonLocked = $derived(isTeacherLocked || isProgressionLocked); const isVideoWatchComplete = $derived(isVideoWatchLesson && (lessonApi.lesson?.watchProgress?.isComplete ?? false)); const showVideoWatchCompleteState = $derived(isVideoWatchComplete || (isVideoWatchLesson && isLessonComplete)); const isMarkCompleteDisabled = $derived( diff --git a/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte b/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte index 27ff5dfd..8cb598b3 100644 --- a/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte +++ b/apps/dashboard/src/lib/features/course/components/sidebar/course-content-tree.svelte @@ -14,6 +14,7 @@ import { IconButton } from '@cio/ui/custom/icon-button'; import { RadioIcon } from '@cio/ui/custom/moving-icons'; import CourseContentIcon from '$features/course/components/course-content-icon.svelte'; + import { isOrgStudent } from '$lib/utils/store/app'; interface Props { path: string; @@ -85,7 +86,8 @@ {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} - {@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)} + {@const isLockedForStudent = + $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} {#snippet child({ props })} {@const isContentLocked = (contentItem.isUnlocked ?? true) === false} - {@const isLockedForStudent = isStudent && (isContentLocked || contentItem.accessible === false)} + {@const isLockedForStudent = + $isOrgStudent === true && (isContentLocked || contentItem.accessible === false)} { - if ($isOrgStudent && currentLessonContentItem) { - return (currentLessonContentItem.isUnlocked ?? false) === false; + const contentLockReason = $derived(getStudentContentLockReason(courseApi.course, lessonId, ContentType.Lesson)); + const isLessonLockedForStudent = $derived($isOrgStudent === true && contentLockReason !== null); + const isStudentLessonStateReady = $derived.by(() => { + if ($isOrgStudent !== true) { + return true; } - return (lessonApi.lesson?.isUnlocked ?? false) === false; + if (courseApi.course?.id !== courseId) { + return false; + } + + if (contentLockReason !== null) { + return true; + } + + if (currentLessonContentItem) { + return true; + } + + if (lessonApi.lesson?.id === lessonId) { + return true; + } + + return courseApi.course?.content != null; }); - const isProgressionLocked = $derived( - $isOrgStudent && currentLessonContentItem?.accessible === false && !isTeacherLocked - ); - const isLessonUnlocked = $derived(!isTeacherLocked && !isProgressionLocked); const lessonSlug = $derived(lessonApi.lesson?.slug ?? ''); const isPublicCourse = $derived(courseApi.course?.type === 'PUBLIC'); const isLiveSessionLesson = $derived(Boolean(lessonApi.lesson?.callUrl && lessonApi.lesson?.lessonAt)); @@ -450,7 +466,7 @@ {#snippet child()}
- {#if isLiveSessionLesson && mode === MODES.view && !($isOrgStudent && !isLessonUnlocked)} + {#if isLiveSessionLesson && mode === MODES.view && !isLessonLockedForStudent}
{/if} - {#if $isOrgStudent && !isLessonUnlocked} - + {#if $isOrgStudent === true && !isStudentLessonStateReady} +
+ +
+ {:else if isLessonLockedForStudent && contentLockReason} + {:else if mode === MODES.edit} {/key} + {:else if $isOrgStudent === true} + {:else} - {#if !$isOrgStudent} - - {/if} + {/if}
diff --git a/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte b/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte index 3aa76e87..7a13b964 100644 --- a/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/courses/[id]/lessons/[lessonId]/+page.svelte @@ -16,7 +16,16 @@ let { data }: Props = $props(); $effect(() => { - if (!data.lesson) return; + if (!data.lessonId) return; + + if (!data.lesson) { + if (lessonApi.lesson?.id === data.lessonId) { + lessonApi.lesson = null; + } + + return; + } + const lesson = data.lesson; if (lessonApi.lesson?.id === lesson.id) return; From f3d1a885b9c8eaec49baef5445b560bba81d1e87 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 16 Jul 2026 10:04:28 +0100 Subject: [PATCH 2/4] 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 * 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 * fix(notifications): use Exercise instead of Quiz in toggle copy Co-authored-by: Rotimi Best * 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 * 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 * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- .../member-email-notifications.ts | 48 + .../src/routes/organization/organization.ts | 2 + apps/api/src/services/account/profile.ts | 11 +- .../member-email-notifications.ts | 29 + .../lib/features/auth/api/profile.svelte.ts | 24 - .../api/member-email-notifications.svelte.ts | 23 + .../settings/pages/notifications.svelte | 81 +- .../src/lib/features/settings/utils/types.ts | 11 + .../features/ui/navigation/lms-navigation.ts | 14 + .../src/lib/utils/translations/da.json | 76 +- .../src/lib/utils/translations/de.json | 76 +- .../src/lib/utils/translations/en.json | 4 +- .../src/lib/utils/translations/es.json | 76 +- .../src/lib/utils/translations/fr.json | 70 +- .../src/lib/utils/translations/hi.json | 76 +- .../src/lib/utils/translations/pl.json | 76 +- .../src/lib/utils/translations/pt.json | 76 +- .../src/lib/utils/translations/ru.json | 76 +- .../src/lib/utils/translations/vi.json | 76 +- .../lms/settings/notifications/+page.svelte | 21 + packages/db/package.json | 1 + ...organizationmember_email_notifications.sql | 15 + .../db/src/migrations/meta/0004_snapshot.json | 12241 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 9 +- .../notifications/email-preferences-org.ts | 21 + .../notifications/email-preferences.ts | 70 +- .../db/src/queries/notifications/index.ts | 2 + .../organizationmember-email-notifications.ts | 120 + packages/db/src/schema.ts | 37 +- ...member-email-notifications-from-profile.ts | 111 + packages/db/src/types.ts | 2 + packages/utils/src/notifications/index.ts | 1 + .../member-email-notifications.ts | 61 + .../utils/src/validation/account/account.ts | 14 +- .../notifications/email-notifications.ts | 20 + .../src/validation/notifications/index.ts | 1 + 36 files changed, 13197 insertions(+), 475 deletions(-) create mode 100644 apps/api/src/routes/organization/member-email-notifications.ts create mode 100644 apps/api/src/services/notification/member-email-notifications.ts create mode 100644 apps/dashboard/src/lib/features/settings/api/member-email-notifications.svelte.ts create mode 100644 apps/dashboard/src/routes/(app)/lms/settings/notifications/+page.svelte create mode 100644 packages/db/src/migrations/0004_organizationmember_email_notifications.sql create mode 100644 packages/db/src/migrations/meta/0004_snapshot.json create mode 100644 packages/db/src/queries/notifications/email-preferences-org.ts create mode 100644 packages/db/src/queries/notifications/organizationmember-email-notifications.ts create mode 100644 packages/db/src/scripts/backfill-member-email-notifications-from-profile.ts create mode 100644 packages/utils/src/notifications/member-email-notifications.ts create mode 100644 packages/utils/src/validation/notifications/email-notifications.ts create mode 100644 packages/utils/src/validation/notifications/index.ts diff --git a/apps/api/src/routes/organization/member-email-notifications.ts b/apps/api/src/routes/organization/member-email-notifications.ts new file mode 100644 index 00000000..77e03907 --- /dev/null +++ b/apps/api/src/routes/organization/member-email-notifications.ts @@ -0,0 +1,48 @@ +import { Hono } from '@api/utils/hono'; +import { authMiddleware } from '@api/middlewares/auth'; +import { orgMemberMiddleware } from '@api/middlewares/org-member'; +import { handleError } from '@api/utils/errors'; +import { zValidator } from '@hono/zod-validator'; +import { ZMemberEmailNotificationPreferencesUpdate } from '@cio/utils/validation/notifications'; + +import { + getMemberEmailNotificationPreferences, + updateMemberEmailNotificationPreferences +} from '@api/services/notification/member-email-notifications'; + +/** + * Per-org personal email notification preferences for the authenticated member. + * + * GET /organization/member/email-notifications + * PUT /organization/member/email-notifications + */ +export const organizationMemberEmailNotificationsRouter = new Hono() + .get('/', authMiddleware, orgMemberMiddleware, async (c) => { + try { + const user = c.get('user')!; + const orgId = c.req.header('cio-org-id')!; + const preferences = await getMemberEmailNotificationPreferences(user.id, orgId); + + return c.json({ success: true as const, data: preferences }); + } catch (error) { + return handleError(c, error, 'Failed to fetch email notification preferences'); + } + }) + .put( + '/', + authMiddleware, + orgMemberMiddleware, + zValidator('json', ZMemberEmailNotificationPreferencesUpdate), + async (c) => { + try { + const user = c.get('user')!; + const orgId = c.req.header('cio-org-id')!; + const preferences = c.req.valid('json'); + const updated = await updateMemberEmailNotificationPreferences(user.id, orgId, preferences); + + return c.json({ success: true as const, data: updated }); + } catch (error) { + return handleError(c, error, 'Failed to update email notification preferences'); + } + } + ); diff --git a/apps/api/src/routes/organization/organization.ts b/apps/api/src/routes/organization/organization.ts index eb568def..0477e13a 100644 --- a/apps/api/src/routes/organization/organization.ts +++ b/apps/api/src/routes/organization/organization.ts @@ -52,6 +52,7 @@ import { TOrganization } from '@db/types'; import { assetsRouter } from '@api/routes/organization/assets'; import { autoJoinOrg } from '@api/services/organization/auto-join'; import { organizationAiTutorRouter } from '@api/routes/organization/ai-tutor'; +import { organizationMemberEmailNotificationsRouter } from '@api/routes/organization/member-email-notifications'; import { authMiddleware } from '@api/middlewares/auth'; import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key'; import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key'; @@ -768,4 +769,5 @@ export const organizationRouter = new Hono() .route('/widgets', widgetsRouter) .route('/assets', assetsRouter) .route('/ai-tutor', organizationAiTutorRouter) + .route('/member/email-notifications', organizationMemberEmailNotificationsRouter) .route('/:orgId/quiz', quizRouter); diff --git a/apps/api/src/services/account/profile.ts b/apps/api/src/services/account/profile.ts index dd9050c7..10d61d98 100644 --- a/apps/api/src/services/account/profile.ts +++ b/apps/api/src/services/account/profile.ts @@ -100,16 +100,7 @@ export async function getAccountData(userId: string): Promise> = { - ...profileFields - }; - - if (emailNotifications !== undefined) { - updatePayload.settings = { emailNotifications }; - } - - const updatedProfile = await updateProfile(userId, updatePayload); + const updatedProfile = await updateProfile(userId, data); if (!updatedProfile) { throw new AppError(`Profile not found for user ID: ${userId}`, ErrorCodes.PROFILE_NOT_FOUND, 404); diff --git a/apps/api/src/services/notification/member-email-notifications.ts b/apps/api/src/services/notification/member-email-notifications.ts new file mode 100644 index 00000000..b5742a31 --- /dev/null +++ b/apps/api/src/services/notification/member-email-notifications.ts @@ -0,0 +1,29 @@ +import { AppError, ErrorCodes } from '@api/utils/errors'; +import { + getOrganizationMemberEmailNotificationsForApi, + upsertOrganizationMemberEmailNotifications +} from '@cio/db/queries/notifications'; +import { memberEmailNotificationRowToApiResponse } from '@cio/utils/notifications'; +import type { TMemberEmailNotificationPreferencesUpdate } from '@cio/utils/validation/notifications'; + +export async function getMemberEmailNotificationPreferences(profileId: string, organizationId: string) { + return getOrganizationMemberEmailNotificationsForApi(profileId, organizationId); +} + +export async function updateMemberEmailNotificationPreferences( + profileId: string, + organizationId: string, + preferences: TMemberEmailNotificationPreferencesUpdate +) { + try { + const updated = await upsertOrganizationMemberEmailNotifications(profileId, organizationId, preferences); + + return memberEmailNotificationRowToApiResponse(updated); + } catch (error) { + if (error instanceof Error && error.message === 'Organization member not found') { + throw new AppError('Organization member not found', ErrorCodes.NOT_FOUND, 404); + } + + throw error; + } +} diff --git a/apps/dashboard/src/lib/features/auth/api/profile.svelte.ts b/apps/dashboard/src/lib/features/auth/api/profile.svelte.ts index 9e111add..fbe7ec7e 100644 --- a/apps/dashboard/src/lib/features/auth/api/profile.svelte.ts +++ b/apps/dashboard/src/lib/features/auth/api/profile.svelte.ts @@ -137,30 +137,6 @@ export class ProfileApi extends BaseApiWithErrors { } } - async updateEmailNotifications(emailNotifications: Record) { - try { - this.isLoading = true; - this.errors = {}; - this.success = false; - - await this.execute({ - requestFn: () => classroomio.account.profile.$put({ json: { emailNotifications } }), - logContext: 'updating email notification preferences', - onSuccess: (response) => { - profileStore.update((_profile) => ({ - ..._profile, - ...response.profile - })); - this.success = true; - } - }); - } catch (error) { - this.handleError(error); - } finally { - this.isLoading = false; - } - } - async changeEmail(fields: TChangeEmailForm) { // Validate email format const result = ZChangeEmail.safeParse({ newEmail: fields.newEmail }); diff --git a/apps/dashboard/src/lib/features/settings/api/member-email-notifications.svelte.ts b/apps/dashboard/src/lib/features/settings/api/member-email-notifications.svelte.ts new file mode 100644 index 00000000..5cde737a --- /dev/null +++ b/apps/dashboard/src/lib/features/settings/api/member-email-notifications.svelte.ts @@ -0,0 +1,23 @@ +import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api'; +import type { + GetMemberEmailNotificationsRequest, + UpdateMemberEmailNotificationsRequest +} from '$features/settings/utils/types'; + +class MemberEmailNotificationsApi extends BaseApiWithErrors { + async fetch() { + return this.execute({ + requestFn: () => classroomio.organization.member['email-notifications'].$get(), + logContext: 'fetching member email notification preferences' + }); + } + + async update(preferences: Record) { + return this.execute({ + requestFn: () => classroomio.organization.member['email-notifications'].$put({ json: preferences }), + logContext: 'updating member email notification preferences' + }); + } +} + +export const memberEmailNotificationsApi = new MemberEmailNotificationsApi(); diff --git a/apps/dashboard/src/lib/features/settings/pages/notifications.svelte b/apps/dashboard/src/lib/features/settings/pages/notifications.svelte index 406a138b..2c1c38ae 100644 --- a/apps/dashboard/src/lib/features/settings/pages/notifications.svelte +++ b/apps/dashboard/src/lib/features/settings/pages/notifications.svelte @@ -6,12 +6,11 @@ PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS } from '@cio/utils/notifications'; - import { profile } from '$lib/utils/store/user'; import { currentOrg, isOrgAdmin } from '$lib/utils/store/org'; import { isOrgStudent } from '$lib/utils/store/app'; import { t } from '$lib/utils/functions/translations'; import { snackbar } from '$features/ui/snackbar/store'; - import { profileApi } from '$features/auth/api/profile.svelte'; + import { memberEmailNotificationsApi } from '$features/settings/api/member-email-notifications.svelte'; import { orgApi } from '$features/org/api/org.svelte'; import * as Field from '@cio/ui/base/field'; import { Switch } from '@cio/ui/base/switch'; @@ -26,10 +25,12 @@ } let personalToggles = $state(buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS)); + let savedPersonalToggles = $state(buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS)); let orgToggles = $state(buildToggleState(EMAIL_NOTIFICATION_TOGGLE_KEYS)); let isSavingPersonal = $state(false); let isSavingOrg = $state(false); - let hasHydratedFromStores = $state(false); + let isLoadingPersonal = $state(false); + let hasHydratedOrgToggles = $state(false); const tutorOnlyToggleKeys = new Set(TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS); @@ -42,12 +43,18 @@ : PERSONAL_EMAIL_NOTIFICATION_SECTIONS ); - function syncPersonalTogglesFromProfile() { - const profileState = get(profile); - const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, profileState.settings?.emailNotifications); + const personalDescription = $derived( + $currentOrg?.name + ? t.get('settings.notifications.personal.description_with_org', { orgName: $currentOrg.name }) + : t.get('settings.notifications.personal.description') + ); + + function applyPersonalToggleState(source?: Partial> | null) { + const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, source); for (const key of PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS) { personalToggles[key] = next[key]; + savedPersonalToggles[key] = next[key]; } } @@ -63,41 +70,53 @@ } } - function tryHydrateFromStores() { - if (hasHydratedFromStores) return; + async function loadPersonalPreferences() { + if (!$currentOrg?.id) return; - const profileState = get(profile); - if (!profileState.id) return; + isLoadingPersonal = true; - syncPersonalTogglesFromProfile(); + const result = await memberEmailNotificationsApi.fetch(); - if (get(isOrgAdmin)) { - if (!get(currentOrg)?.id) return; - syncOrgTogglesFromCurrentOrg(); + if (result?.data) { + applyPersonalToggleState(result.data); } - hasHydratedFromStores = true; + isLoadingPersonal = false; + } + + function tryHydrateOrgToggles() { + if (hasHydratedOrgToggles || !get(isOrgAdmin)) return; + + if (!get(currentOrg)?.id) return; + + syncOrgTogglesFromCurrentOrg(); + hasHydratedOrgToggles = true; } onMount(() => { - tryHydrateFromStores(); + tryHydrateOrgToggles(); }); $effect(() => { - if (hasHydratedFromStores) return; + const orgId = $currentOrg?.id; - const profileId = $profile.id; - const orgId = $isOrgAdmin ? $currentOrg?.id : 'ready'; + if (!orgId) return; - if (!profileId || ($isOrgAdmin && !orgId)) return; + void loadPersonalPreferences(); + }); - tryHydrateFromStores(); + $effect(() => { + if (hasHydratedOrgToggles) return; + + const orgId = $isOrgAdmin ? $currentOrg?.id : null; + + if (!orgId) return; + + tryHydrateOrgToggles(); }); const personalHasChanges = $derived( - PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some( - (key) => personalToggles[key] !== ($profile.settings?.emailNotifications?.[key] !== false) - ) + PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some((key) => personalToggles[key] !== savedPersonalToggles[key]) ); const orgHasChanges = $derived( @@ -117,14 +136,16 @@ } async function handleSavePersonal() { + if (!$currentOrg?.id) return; + isSavingPersonal = true; - await profileApi.updateEmailNotifications( + const result = await memberEmailNotificationsApi.update( toStoredPreferences(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, personalToggles) ); - if (profileApi.success) { - syncPersonalTogglesFromProfile(); + if (memberEmailNotificationsApi.success && result?.data) { + applyPersonalToggleState(result.data); snackbar.success(t.get('settings.notifications.personal.save_success')); } @@ -164,7 +185,7 @@ {$t('settings.notifications.personal.heading')} - {$t('settings.notifications.personal.description')} + {personalDescription}
@@ -182,7 +203,7 @@
{#each section.keys as key (key)} - +
{$t(`settings.notifications.toggles.${key}.label`)} @@ -200,7 +221,7 @@