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:
co-authored by
Rotimi Best
Cursor Agent
parent
9681770270
commit
f3d1a885b9
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -52,6 +52,7 @@ import { TOrganization } from '@db/types';
|
|||||||
import { assetsRouter } from '@api/routes/organization/assets';
|
import { assetsRouter } from '@api/routes/organization/assets';
|
||||||
import { autoJoinOrg } from '@api/services/organization/auto-join';
|
import { autoJoinOrg } from '@api/services/organization/auto-join';
|
||||||
import { organizationAiTutorRouter } from '@api/routes/organization/ai-tutor';
|
import { organizationAiTutorRouter } from '@api/routes/organization/ai-tutor';
|
||||||
|
import { organizationMemberEmailNotificationsRouter } from '@api/routes/organization/member-email-notifications';
|
||||||
import { authMiddleware } from '@api/middlewares/auth';
|
import { authMiddleware } from '@api/middlewares/auth';
|
||||||
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
|
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
|
||||||
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
||||||
@@ -768,4 +769,5 @@ export const organizationRouter = new Hono()
|
|||||||
.route('/widgets', widgetsRouter)
|
.route('/widgets', widgetsRouter)
|
||||||
.route('/assets', assetsRouter)
|
.route('/assets', assetsRouter)
|
||||||
.route('/ai-tutor', organizationAiTutorRouter)
|
.route('/ai-tutor', organizationAiTutorRouter)
|
||||||
|
.route('/member/email-notifications', organizationMemberEmailNotificationsRouter)
|
||||||
.route('/:orgId/quiz', quizRouter);
|
.route('/:orgId/quiz', quizRouter);
|
||||||
|
|||||||
@@ -100,16 +100,7 @@ export async function getAccountData(userId: string): Promise<GetAccountDataResu
|
|||||||
*/
|
*/
|
||||||
export async function updateUser(userId: string, data: TUpdateProfile) {
|
export async function updateUser(userId: string, data: TUpdateProfile) {
|
||||||
try {
|
try {
|
||||||
const { emailNotifications, ...profileFields } = data;
|
const updatedProfile = await updateProfile(userId, data);
|
||||||
const updatePayload: Partial<Omit<TProfile, 'id' | 'email' | 'createdAt' | 'updatedAt'>> = {
|
|
||||||
...profileFields
|
|
||||||
};
|
|
||||||
|
|
||||||
if (emailNotifications !== undefined) {
|
|
||||||
updatePayload.settings = { emailNotifications };
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedProfile = await updateProfile(userId, updatePayload);
|
|
||||||
|
|
||||||
if (!updatedProfile) {
|
if (!updatedProfile) {
|
||||||
throw new AppError(`Profile not found for user ID: ${userId}`, ErrorCodes.PROFILE_NOT_FOUND, 404);
|
throw new AppError(`Profile not found for user ID: ${userId}`, ErrorCodes.PROFILE_NOT_FOUND, 404);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,30 +137,6 @@ export class ProfileApi extends BaseApiWithErrors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEmailNotifications(emailNotifications: Record<string, boolean>) {
|
|
||||||
try {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.errors = {};
|
|
||||||
this.success = false;
|
|
||||||
|
|
||||||
await this.execute<typeof classroomio.account.profile.$put>({
|
|
||||||
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) {
|
async changeEmail(fields: TChangeEmailForm) {
|
||||||
// Validate email format
|
// Validate email format
|
||||||
const result = ZChangeEmail.safeParse({ newEmail: fields.newEmail });
|
const result = ZChangeEmail.safeParse({ newEmail: fields.newEmail });
|
||||||
|
|||||||
@@ -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<GetMemberEmailNotificationsRequest>({
|
||||||
|
requestFn: () => classroomio.organization.member['email-notifications'].$get(),
|
||||||
|
logContext: 'fetching member email notification preferences'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(preferences: Record<string, boolean>) {
|
||||||
|
return this.execute<UpdateMemberEmailNotificationsRequest>({
|
||||||
|
requestFn: () => classroomio.organization.member['email-notifications'].$put({ json: preferences }),
|
||||||
|
logContext: 'updating member email notification preferences'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const memberEmailNotificationsApi = new MemberEmailNotificationsApi();
|
||||||
@@ -6,12 +6,11 @@
|
|||||||
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS,
|
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS,
|
||||||
TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS
|
TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS
|
||||||
} from '@cio/utils/notifications';
|
} from '@cio/utils/notifications';
|
||||||
import { profile } from '$lib/utils/store/user';
|
|
||||||
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
|
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
|
||||||
import { isOrgStudent } from '$lib/utils/store/app';
|
import { isOrgStudent } from '$lib/utils/store/app';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { snackbar } from '$features/ui/snackbar/store';
|
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 { orgApi } from '$features/org/api/org.svelte';
|
||||||
import * as Field from '@cio/ui/base/field';
|
import * as Field from '@cio/ui/base/field';
|
||||||
import { Switch } from '@cio/ui/base/switch';
|
import { Switch } from '@cio/ui/base/switch';
|
||||||
@@ -26,10 +25,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let personalToggles = $state(buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS));
|
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 orgToggles = $state(buildToggleState(EMAIL_NOTIFICATION_TOGGLE_KEYS));
|
||||||
let isSavingPersonal = $state(false);
|
let isSavingPersonal = $state(false);
|
||||||
let isSavingOrg = $state(false);
|
let isSavingOrg = $state(false);
|
||||||
let hasHydratedFromStores = $state(false);
|
let isLoadingPersonal = $state(false);
|
||||||
|
let hasHydratedOrgToggles = $state(false);
|
||||||
|
|
||||||
const tutorOnlyToggleKeys = new Set<string>(TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS);
|
const tutorOnlyToggleKeys = new Set<string>(TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS);
|
||||||
|
|
||||||
@@ -42,12 +43,18 @@
|
|||||||
: PERSONAL_EMAIL_NOTIFICATION_SECTIONS
|
: PERSONAL_EMAIL_NOTIFICATION_SECTIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
function syncPersonalTogglesFromProfile() {
|
const personalDescription = $derived(
|
||||||
const profileState = get(profile);
|
$currentOrg?.name
|
||||||
const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, profileState.settings?.emailNotifications);
|
? t.get('settings.notifications.personal.description_with_org', { orgName: $currentOrg.name })
|
||||||
|
: t.get('settings.notifications.personal.description')
|
||||||
|
);
|
||||||
|
|
||||||
|
function applyPersonalToggleState(source?: Partial<Record<string, boolean>> | null) {
|
||||||
|
const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, source);
|
||||||
|
|
||||||
for (const key of PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS) {
|
for (const key of PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS) {
|
||||||
personalToggles[key] = next[key];
|
personalToggles[key] = next[key];
|
||||||
|
savedPersonalToggles[key] = next[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,41 +70,53 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryHydrateFromStores() {
|
async function loadPersonalPreferences() {
|
||||||
if (hasHydratedFromStores) return;
|
if (!$currentOrg?.id) return;
|
||||||
|
|
||||||
const profileState = get(profile);
|
isLoadingPersonal = true;
|
||||||
if (!profileState.id) return;
|
|
||||||
|
|
||||||
syncPersonalTogglesFromProfile();
|
const result = await memberEmailNotificationsApi.fetch();
|
||||||
|
|
||||||
if (get(isOrgAdmin)) {
|
if (result?.data) {
|
||||||
if (!get(currentOrg)?.id) return;
|
applyPersonalToggleState(result.data);
|
||||||
syncOrgTogglesFromCurrentOrg();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasHydratedFromStores = true;
|
isLoadingPersonal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryHydrateOrgToggles() {
|
||||||
|
if (hasHydratedOrgToggles || !get(isOrgAdmin)) return;
|
||||||
|
|
||||||
|
if (!get(currentOrg)?.id) return;
|
||||||
|
|
||||||
|
syncOrgTogglesFromCurrentOrg();
|
||||||
|
hasHydratedOrgToggles = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
tryHydrateFromStores();
|
tryHydrateOrgToggles();
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (hasHydratedFromStores) return;
|
const orgId = $currentOrg?.id;
|
||||||
|
|
||||||
const profileId = $profile.id;
|
if (!orgId) return;
|
||||||
const orgId = $isOrgAdmin ? $currentOrg?.id : 'ready';
|
|
||||||
|
|
||||||
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(
|
const personalHasChanges = $derived(
|
||||||
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some(
|
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some((key) => personalToggles[key] !== savedPersonalToggles[key])
|
||||||
(key) => personalToggles[key] !== ($profile.settings?.emailNotifications?.[key] !== false)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const orgHasChanges = $derived(
|
const orgHasChanges = $derived(
|
||||||
@@ -117,14 +136,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSavePersonal() {
|
async function handleSavePersonal() {
|
||||||
|
if (!$currentOrg?.id) return;
|
||||||
|
|
||||||
isSavingPersonal = true;
|
isSavingPersonal = true;
|
||||||
|
|
||||||
await profileApi.updateEmailNotifications(
|
const result = await memberEmailNotificationsApi.update(
|
||||||
toStoredPreferences(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, personalToggles)
|
toStoredPreferences(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, personalToggles)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (profileApi.success) {
|
if (memberEmailNotificationsApi.success && result?.data) {
|
||||||
syncPersonalTogglesFromProfile();
|
applyPersonalToggleState(result.data);
|
||||||
snackbar.success(t.get('settings.notifications.personal.save_success'));
|
snackbar.success(t.get('settings.notifications.personal.save_success'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +185,7 @@
|
|||||||
{$t('settings.notifications.personal.heading')}
|
{$t('settings.notifications.personal.heading')}
|
||||||
</Field.Legend>
|
</Field.Legend>
|
||||||
<Field.Description>
|
<Field.Description>
|
||||||
{$t('settings.notifications.personal.description')}
|
{personalDescription}
|
||||||
</Field.Description>
|
</Field.Description>
|
||||||
|
|
||||||
<div class="mt-4 space-y-6">
|
<div class="mt-4 space-y-6">
|
||||||
@@ -182,7 +203,7 @@
|
|||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
{#each section.keys as key (key)}
|
{#each section.keys as key (key)}
|
||||||
<Field.Field orientation="horizontal">
|
<Field.Field orientation="horizontal">
|
||||||
<Switch bind:checked={personalToggles[key]} disabled={isSavingPersonal} />
|
<Switch bind:checked={personalToggles[key]} disabled={isSavingPersonal || isLoadingPersonal} />
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<Field.Label>{$t(`settings.notifications.toggles.${key}.label`)}</Field.Label>
|
<Field.Label>{$t(`settings.notifications.toggles.${key}.label`)}</Field.Label>
|
||||||
<Field.Description>
|
<Field.Description>
|
||||||
@@ -200,7 +221,7 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
loading={isSavingPersonal}
|
loading={isSavingPersonal}
|
||||||
disabled={!personalHasChanges || isSavingPersonal}
|
disabled={!personalHasChanges || isSavingPersonal || isLoadingPersonal || !$currentOrg?.id}
|
||||||
onclick={handleSavePersonal}
|
onclick={handleSavePersonal}
|
||||||
>
|
>
|
||||||
{$t('settings.notifications.personal.save')}
|
{$t('settings.notifications.personal.save')}
|
||||||
|
|||||||
@@ -16,3 +16,14 @@ export type AiUsageData = GetUsageSuccess['data'];
|
|||||||
export type PurchasedSummaryData = GetPurchasedSuccess['data'];
|
export type PurchasedSummaryData = GetPurchasedSuccess['data'];
|
||||||
export type LeaderboardData = GetLeaderboardSuccess['data'];
|
export type LeaderboardData = GetLeaderboardSuccess['data'];
|
||||||
export type LeaderboardEntry = LeaderboardData['entries'][number];
|
export type LeaderboardEntry = LeaderboardData['entries'][number];
|
||||||
|
|
||||||
|
export type GetMemberEmailNotificationsRequest =
|
||||||
|
(typeof classroomio.organization.member)['email-notifications']['$get'];
|
||||||
|
export type UpdateMemberEmailNotificationsRequest =
|
||||||
|
(typeof classroomio.organization.member)['email-notifications']['$put'];
|
||||||
|
|
||||||
|
export type GetMemberEmailNotificationsSuccess = Extract<
|
||||||
|
InferResponseType<GetMemberEmailNotificationsRequest>,
|
||||||
|
{ success: true }
|
||||||
|
>;
|
||||||
|
export type MemberEmailNotificationPreferences = GetMemberEmailNotificationsSuccess['data'];
|
||||||
|
|||||||
@@ -116,10 +116,24 @@ export const baseNavConfig: NavItemConfig[] = [
|
|||||||
titleKey: 'Profile',
|
titleKey: 'Profile',
|
||||||
path: '/settings'
|
path: '/settings'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
titleKey: 'Notifications',
|
||||||
|
path: '/settings/notifications'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
titleKey: 'Integrations',
|
titleKey: 'Integrations',
|
||||||
path: '/settings/integrations'
|
path: '/settings/integrations'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
nestedRoutes: [
|
||||||
|
{
|
||||||
|
path: 'notifications',
|
||||||
|
titleKey: 'settings.tabs.notifications_tab'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'integrations',
|
||||||
|
titleKey: 'settings.tabs.integrations_tab'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Hold",
|
"teams_tab": "Hold",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "AI underviser",
|
"ai_tutor_tab": "AI underviser",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Notifikationer"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Åbent Akademi"
|
"view_site": "Åbent Akademi"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Notifikationer",
|
||||||
"page_title": "Notifications",
|
"page_title": "Notifikationer",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Vælg hvilke e-mailnotifikationer du modtager, og som administrator hvilke e-mails din organisation sender.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Kursusafslutning og certifikater",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Send e-mail til studerende, når de gennemfører et kursus og opnår et certifikat."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Velkomst-e-mails ved tilmelding",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Send velkomst-e-mails, når personalet tilmelder studerende til kurser eller kohorter."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Påmindelser og opdateringer om livesessioner",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Send e-mail til studerende før livesessioner og når sessionsdetaljer ændres."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Påmindelser om kohortemål",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Send e-mail til studerende om kommende eller overskredne kohortlæringsmål."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Øvelse tildelt",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Send e-mail til studerende, når en øvelse tildeles eller påmindes."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Kursusmeddelelser og kommentarer",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Send e-mail til kursusmedlemmer om nye opslag og kommentarer på nyhedsfeedet."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Bedømmelsesresultater",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Send e-mail til studerende, når deres aflevering er bedømt eller opdateret."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Nye afleveringer til bedømmelse",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Send e-mail til undervisere, når en studerende afleverer arbejde, der skal bedømmes."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Ny studerende tilmelder sig",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Send e-mail til undervisere, når en studerende tilmelder sig et kursus."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Organisationsstandarder",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Når deaktiveret her, modtager ingen i din organisation den e-mail. Medlemmer kan stadig fravælge e-mails, du lader være aktiveret.",
|
||||||
"save": "Save organization defaults",
|
"save": "Gem organisationsstandarder",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Organisationsnotifikationsstandarder gemt"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Dine e-mailpræferencer",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Gem personlige præferencer",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Personlige notifikationspræferencer gemt",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Disse gælder for e-mails sendt til dig i denne organisation. Din organisation kan stadig deaktivere visse e-mails for alle.",
|
||||||
|
"description_with_org": "Disse gælder for e-mails sendt til dig i {orgName}. Din organisation kan stadig deaktivere visse e-mails for alle."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Tilmelding",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "Velkomst-e-mails sendt, når personalet tilføjer medlemmer til kurser eller kohorter."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Kohorte",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Påmindelser om kohortens læringsmål og deadlines."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Kursus",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Tilmeldinger, afleveringer, lektioner, meddelelser og gennemførelser i dine kurser."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Vælg hvilke e-mailnotifikationer du modtager fra ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Mannschaften",
|
"teams_tab": "Mannschaften",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "KI-Tutor",
|
"ai_tutor_tab": "KI-Tutor",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Benachrichtigungen"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Offene Akademie"
|
"view_site": "Offene Akademie"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Benachrichtigungen",
|
||||||
"page_title": "Notifications",
|
"page_title": "Benachrichtigungen",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Wählen Sie, welche E-Mail-Benachrichtigungen Sie erhalten, und als Administrator, welche E-Mails Ihre Organisation sendet.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Kursabschluss und Zertifikate",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "E-Mail an Studenten senden, wenn sie einen Kurs abschließen und ein Zertifikat erhalten."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Willkommens-E-Mails bei Anmeldung",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Willkommens-E-Mails senden, wenn Mitarbeitende Studenten zu Kursen oder Kohorten anmelden."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Erinnerungen und Updates zu Live-Sitzungen",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "E-Mail an Studenten vor Live-Sitzungen und bei Änderungen der Sitzungsdetails senden."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Erinnerungen an Kohortenlernziele",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "E-Mail an Studenten über bevorstehende oder überfällige Kohortenlernziele senden."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Übung zugewiesen",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "E-Mail an Studenten senden, wenn eine Übung zugewiesen oder erinnert wird."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Kursankündigungen und Kommentare",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "E-Mail an Kursmitglieder über neue Beiträge und Kommentare im Newsfeed senden."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Bewertungsergebnisse",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "E-Mail an Studenten senden, wenn ihre Abgabe bewertet oder aktualisiert wird."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Neue Abgaben zum Bewerten",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "E-Mail an Tutoren senden, wenn ein Student Arbeit zur Bewertung einreicht."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Neuer Student meldet sich an",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "E-Mail an Tutoren senden, wenn sich ein Student für einen Kurs anmeldet."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Organisationsstandards",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Wenn hier deaktiviert, erhält niemand in Ihrer Organisation diese E-Mail. Mitglieder können sich weiterhin von E-Mails abmelden, die Sie aktiviert lassen.",
|
||||||
"save": "Save organization defaults",
|
"save": "Organisationsstandards speichern",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Organisations-Benachrichtigungsstandards gespeichert"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Ihre E-Mail-Einstellungen",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Persönliche Einstellungen speichern",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Persönliche Benachrichtigungseinstellungen gespeichert",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Diese Einstellungen gelten für E-Mails, die Ihnen in dieser Organisation gesendet werden. Ihre Organisation kann bestimmte E-Mails für alle deaktivieren.",
|
||||||
|
"description_with_org": "Diese Einstellungen gelten für E-Mails, die Ihnen in {orgName} gesendet werden. Ihre Organisation kann bestimmte E-Mails für alle deaktivieren."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Anmeldung",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "Willkommens-E-Mails, die gesendet werden, wenn Mitarbeitende Mitglieder zu Kursen oder Kohorten hinzufügen."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Kohorte",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Erinnerungen an Lernziele und Fristen der Kohorte."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Kurs",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Anmeldungen, Abgaben, Lektionen, Ankündigungen und Abschlüsse in Ihren Kursen."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Wählen Sie, welche E-Mail-Benachrichtigungen Sie von ClassroomIO erhalten."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -440,9 +440,11 @@
|
|||||||
"heading": "Notifications",
|
"heading": "Notifications",
|
||||||
"page_title": "Notifications",
|
"page_title": "Notifications",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||||
|
"personal_page_subtitle": "Choose which email notifications you receive from ClassroomIO.",
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"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.",
|
"description": "These apply to emails sent to you in this organization. Your organization may still disable certain emails for everyone.",
|
||||||
|
"description_with_org": "These apply to emails sent to you in {orgName}. Your organization may still disable certain emails for everyone.",
|
||||||
"save": "Save personal preferences",
|
"save": "Save personal preferences",
|
||||||
"save_success": "Personal notification preferences saved"
|
"save_success": "Personal notification preferences saved"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "equipos",
|
"teams_tab": "equipos",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "Tutor de IA",
|
"ai_tutor_tab": "Tutor de IA",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Notificaciones"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Academia Abierta"
|
"view_site": "Academia Abierta"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Notificaciones",
|
||||||
"page_title": "Notifications",
|
"page_title": "Notificaciones",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Elige qué notificaciones por correo recibes y, como administrador, qué correos envía tu organización.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Finalización del curso y certificados",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Enviar correo a estudiantes cuando completan un curso y obtienen un certificado."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Correos de bienvenida por inscripción",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Enviar correos de bienvenida cuando el personal inscribe estudiantes en cursos o cohortes."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Recordatorios y actualizaciones de sesiones en vivo",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Enviar correo a estudiantes antes de sesiones en vivo y cuando cambian los detalles."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Recordatorios de objetivos de cohorte",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Enviar correo a estudiantes sobre objetivos de aprendizaje próximos o vencidos."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Ejercicio asignado",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Enviar correo a estudiantes cuando se asigna o recuerda un ejercicio."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Anuncios y comentarios del curso",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Enviar correo a los miembros del curso sobre nuevas publicaciones y comentarios en el feed."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Resultados de calificación",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Enviar correo a estudiantes cuando su entrega es calificada o actualizada."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Nuevas entregas para calificar",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Enviar correo a tutores cuando un estudiante entrega trabajo que necesita calificación."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Nuevo estudiante se inscribe",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Enviar correo a tutores cuando un estudiante se inscribe en un curso."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Valores predeterminados de la organización",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Si se desactiva aquí, nadie en tu organización recibirá ese correo. Los miembros aún pueden darse de baja de los correos que dejes habilitados.",
|
||||||
"save": "Save organization defaults",
|
"save": "Guardar valores predeterminados de la organización",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Valores predeterminados de notificación de la organización guardados"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Tus preferencias de correo",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Guardar preferencias personales",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Preferencias de notificación personales guardadas",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Se aplican a los correos electrónicos que recibes en esta organización. Tu organización aún puede desactivar ciertos correos para todos.",
|
||||||
|
"description_with_org": "Se aplican a los correos electrónicos que recibes en {orgName}. Tu organización aún puede desactivar ciertos correos para todos."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Inscripción",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "Correos de bienvenida enviados cuando el personal añade miembros a cursos o cohortes."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Cohorte",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Recordatorios sobre objetivos de aprendizaje y plazos de la cohorte."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Curso",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Inscripciones, entregas, lecciones, anuncios y finalizaciones en tus cursos."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Elige qué notificaciones por correo recibes de ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3641,71 +3641,73 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Notifications",
|
||||||
"page_title": "Notifications",
|
"page_title": "Notifications",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Choisissez les notifications par e-mail que vous recevez et, en tant qu’administrateur, celles que votre organisation envoie.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Achèvement du cours et certificats",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Envoyer un e-mail aux étudiants lorsqu'ils terminent un cours et obtiennent un certificat."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "E-mails de bienvenue d'inscription",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Envoyer des e-mails de bienvenue lorsque le personnel inscrit des étudiants à des cours ou cohortes."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Rappels et mises à jour des sessions en direct",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Envoyer un e-mail aux étudiants avant les sessions en direct et lors des changements de détails."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Rappels d'objectifs de cohorte",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Envoyer un e-mail aux étudiants concernant les objectifs d'apprentissage à venir ou en retard."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Exercice assigné",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Envoyer un e-mail aux étudiants lorsqu'un exercice est assigné ou rappelé."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Annonces et commentaires du cours",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Envoyer un e-mail aux membres du cours concernant les nouvelles publications et commentaires."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Résultats de notation",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Envoyer un e-mail aux étudiants lorsque leur soumission est notée ou mise à jour."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Nouvelles soumissions à noter",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Envoyer un e-mail aux tuteurs lorsqu'un étudiant soumet un travail à noter."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Nouvel étudiant s'inscrit",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Envoyer un e-mail aux tuteurs lorsqu'un étudiant s'inscrit à un cours."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Paramètres par défaut de l'organisation",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Lorsqu'ils sont désactivés ici, personne dans votre organisation ne reçoit cet e-mail. Les membres peuvent toujours se désabonner des e-mails que vous laissez activés.",
|
||||||
"save": "Save organization defaults",
|
"save": "Enregistrer les paramètres par défaut de l'organisation",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Paramètres de notification de l'organisation enregistrés"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Vos préférences e-mail",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Enregistrer les préférences personnelles",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Préférences de notification personnelles enregistrées",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Ces préférences s'appliquent aux e-mails qui vous sont envoyés dans cette organisation. Votre organisation peut toujours désactiver certains e-mails pour tout le monde.",
|
||||||
|
"description_with_org": "Ces préférences s'appliquent aux e-mails qui vous sont envoyés dans {orgName}. Votre organisation peut toujours désactiver certains e-mails pour tout le monde."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Inscription",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "E-mails de bienvenue envoyés lorsque le personnel ajoute des membres à des cours ou cohortes."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Cohorte",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Rappels concernant les objectifs d'apprentissage et les échéances de la cohorte."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Cours",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Inscriptions, soumissions, leçons, annonces et complétions dans vos cours."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Choisissez les notifications par e-mail que vous recevez de ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "टीमें",
|
"teams_tab": "टीमें",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "एआई ट्यूटर",
|
"ai_tutor_tab": "एआई ट्यूटर",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "सूचनाएँ"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "अकादमी खोलें"
|
"view_site": "अकादमी खोलें"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "सूचनाएँ",
|
||||||
"page_title": "Notifications",
|
"page_title": "सूचनाएँ",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "चुनें कि आप कौन सी ईमेल सूचनाएँ प्राप्त करते हैं और व्यवस्थापक के रूप में आपका संगठन कौन से ईमेल भेजता है।",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "पाठ्यक्रम पूर्णता और प्रमाणपत्र",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "जब छात्र पाठ्यक्रम पूरा करें और प्रमाणपत्र प्राप्त करें तो ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "नामांकन स्वागत ईमेल",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "जब स्टाफ छात्रों को पाठ्यक्रमों या समूहों में नामांकित करे तो स्वागत ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "लाइव सत्र अनुस्मारक और अपडेट",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "लाइव सत्रों से पहले और सत्र विवरण बदलने पर छात्रों को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "समूह लक्ष्य अनुस्मारक",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "आगामी या अतिदेय समूह सीखने के लक्ष्यों के बारे में छात्रों को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "अभ्यास असाइन किया गया",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "जब अभ्यास असाइन या याद दिलाया जाए तो छात्रों को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "पाठ्यक्रम घोषणाएँ और टिप्पणियाँ",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "न्यूज़फ़ीड पर नई पोस्ट और टिप्पणियों के बारे में पाठ्यक्रम सदस्यों को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "ग्रेडिंग परिणाम",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "जब उनका सबमिशन ग्रेड या अपडेट हो तो छात्रों को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "ग्रेडिंग के लिए नए सबमिशन",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "जब छात्र ग्रेडिंग की आवश्यकता वाला कार्य जमा करे तो ट्यूटर्स को ईमेल भेजें।"
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "नया छात्र शामिल होता है",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "जब कोई छात्र पाठ्यक्रम में नामांकित हो तो ट्यूटर्स को ईमेल भेजें।"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "संगठन डिफ़ॉल्ट",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "यहाँ अक्षम करने पर, आपके संगठन में कोई भी वह ईमेल नहीं प्राप्त करेगा। सदस्य अभी भी उन ईमेल से ऑप्ट आउट कर सकते हैं जिन्हें आप सक्षम रखते हैं।",
|
||||||
"save": "Save organization defaults",
|
"save": "संगठन डिफ़ॉल्ट सहेजें",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "संगठन सूचना डिफ़ॉल्ट सहेजे गए"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "आपकी ईमेल प्राथमिकताएँ",
|
||||||
"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": "Save personal preferences",
|
"save_success": "व्यक्तिगत सूचना प्राथमिकताएँ सहेजी गईं",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "ये उन ईमेल पर लागू होते हैं जो इस संगठन में आपको भेजे जाते हैं। आपका संगठन फिर भी कुछ ईमेल सभी के लिए बंद कर सकता है।",
|
||||||
|
"description_with_org": "ये उन ईमेल पर लागू होते हैं जो {orgName} में आपको भेजे जाते हैं। आपका संगठन फिर भी कुछ ईमेल सभी के लिए बंद कर सकता है।"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "नामांकन",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "स्वागत ईमेल जब स्टाफ सदस्यों को पाठ्यक्रमों या समूहों में जोड़ता है।"
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "समूह",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "समूह सीखने के लक्ष्यों और समय सीमा के बारे में अनुस्मारक।"
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "पाठ्यक्रम",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "आपके पाठ्यक्रमों में नामांकन, सबमिशन, पाठ, घोषणाएँ और पूर्णता।"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "चुनें कि आप ClassroomIO से कौन सी ईमेल सूचनाएँ प्राप्त करते हैं।"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Zespoły",
|
"teams_tab": "Zespoły",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "Nauczyciel AI",
|
"ai_tutor_tab": "Nauczyciel AI",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Powiadomienia"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Otwarta Akademia"
|
"view_site": "Otwarta Akademia"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Powiadomienia",
|
||||||
"page_title": "Notifications",
|
"page_title": "Powiadomienia",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Wybierz, które powiadomienia e-mail otrzymujesz, a jako administrator, które e-maile wysyła Twoja organizacja.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Ukończenie kursu i certyfikaty",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Wyślij e-mail do studentów, gdy ukończą kurs i otrzymają certyfikat."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Powitalne e-maile zapisów",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Wysyłaj e-maile powitalne, gdy personel zapisuje studentów na kursy lub kohorty."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Przypomnienia i aktualizacje sesji na żywo",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Wyślij e-mail do studentów przed sesjami na żywo i gdy zmienią się szczegóły sesji."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Przypomnienia o celach kohorty",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Wyślij e-mail do studentów o nadchodzących lub zaległych celach nauki kohorty."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Przypisane ćwiczenie",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Wyślij e-mail do studentów, gdy ćwiczenie zostanie przypisane lub przypomniane."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Ogłoszenia kursu i komentarze",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Wyślij e-mail do członków kursu o nowych postach i komentarzach w aktualnościach."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Wyniki oceniania",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Wyślij e-mail do studentów, gdy ich zgłoszenie zostanie ocenione lub zaktualizowane."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Nowe zgłoszenia do oceny",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Wyślij e-mail do tutorów, gdy student prześle pracę wymagającą oceny."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Nowy student dołącza",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Wyślij e-mail do tutorów, gdy student zapisze się na kurs."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Domyślne ustawienia organizacji",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Po wyłączeniu tutaj nikt w Twojej organizacji nie otrzyma tego e-maila. Członkowie nadal mogą zrezygnować z e-maili, które pozostawisz włączone.",
|
||||||
"save": "Save organization defaults",
|
"save": "Zapisz domyślne ustawienia organizacji",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Domyślne ustawienia powiadomień organizacji zapisane"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Twoje preferencje e-mail",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Zapisz preferencje osobiste",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Osobiste preferencje powiadomień zapisane",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Dotyczą wiadomości e-mail wysyłanych do Ciebie w tej organizacji. Twoja organizacja nadal może wyłączyć niektóre wiadomości dla wszystkich.",
|
||||||
|
"description_with_org": "Dotyczą wiadomości e-mail wysyłanych do Ciebie w {orgName}. Twoja organizacja nadal może wyłączyć niektóre wiadomości dla wszystkich."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Zapisy",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "E-maile powitalne wysyłane, gdy personel dodaje członków do kursów lub kohort."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Kohorta",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Przypomnienia o celach nauki kohorty i terminach."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Kurs",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Zapisy, zgłoszenia, lekcje, ogłoszenia i ukończenia w Twoich kursach."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Wybierz, które powiadomienia e-mail otrzymujesz od ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Equipes",
|
"teams_tab": "Equipes",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "Tutor de IA",
|
"ai_tutor_tab": "Tutor de IA",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Notificações"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Academia Aberta"
|
"view_site": "Academia Aberta"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Notificações",
|
||||||
"page_title": "Notifications",
|
"page_title": "Notificações",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Escolha quais notificações por e-mail você recebe e, como administrador, quais e-mails sua organização envia.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Conclusão do curso e certificados",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Enviar e-mail aos alunos quando concluírem um curso e receberem um certificado."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "E-mails de boas-vindas de matrícula",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Enviar e-mails de boas-vindas quando a equipe matricular alunos em cursos ou coortes."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Lembretes e atualizações de sessões ao vivo",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Enviar e-mail aos alunos antes de sessões ao vivo e quando os detalhes mudarem."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Lembretes de metas da coorte",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Enviar e-mail aos alunos sobre metas de aprendizagem próximas ou atrasadas."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Exercício atribuído",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Enviar e-mail aos alunos quando um exercício for atribuído ou lembrado."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Anúncios e comentários do curso",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Enviar e-mail aos membros do curso sobre novas publicações e comentários no feed."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Resultados de avaliação",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Enviar e-mail aos alunos quando o envio for avaliado ou atualizado."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Novos envios para avaliar",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Enviar e-mail aos tutores quando um aluno envia trabalho que precisa de avaliação."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Novo aluno se inscreve",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Enviar e-mail aos tutores quando um aluno se matricula em um curso."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Padrões da organização",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Quando desativado aqui, ninguém na sua organização recebe esse e-mail. Os membros ainda podem optar por não receber e-mails que você mantiver ativados.",
|
||||||
"save": "Save organization defaults",
|
"save": "Salvar padrões da organização",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Padrões de notificação da organização salvos"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Suas preferências de e-mail",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Salvar preferências pessoais",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Preferências de notificação pessoais salvas",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Aplicam-se aos e-mails enviados para você nesta organização. Sua organização ainda pode desativar certos e-mails para todos.",
|
||||||
|
"description_with_org": "Aplicam-se aos e-mails enviados para você em {orgName}. Sua organização ainda pode desativar certos e-mails para todos."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Matrícula",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "E-mails de boas-vindas enviados quando a equipe adiciona membros a cursos ou coortes."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Coorte",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Lembretes sobre metas de aprendizagem e prazos da coorte."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Curso",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Matrículas, envios, aulas, anúncios e conclusões nos seus cursos."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Escolha quais notificações por e-mail você recebe do ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Команды",
|
"teams_tab": "Команды",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "Репетитор по искусственному интеллекту",
|
"ai_tutor_tab": "Репетитор по искусственному интеллекту",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Уведомления"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Открытая Академия"
|
"view_site": "Открытая Академия"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Уведомления",
|
||||||
"page_title": "Notifications",
|
"page_title": "Уведомления",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете, а как администратор — какие письма отправляет ваша организация.",
|
||||||
"toggles": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Завершение курса и сертификаты",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"description": "Отправлять письмо студентам, когда они завершают курс и получают сертификат."
|
||||||
},
|
},
|
||||||
"enrollmentWelcome": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Приветственные письма при записи",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"description": "Отправлять приветственные письма, когда сотрудники записывают студентов на курсы или когорты."
|
||||||
},
|
},
|
||||||
"session": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Напоминания и обновления живых сессий",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"description": "Отправлять письмо студентам перед живыми сессиями и при изменении деталей сессии."
|
||||||
},
|
},
|
||||||
"cohortReminder": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Напоминания о целях когорты",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"description": "Отправлять письмо студентам о предстоящих или просроченных целях обучения когорты."
|
||||||
},
|
},
|
||||||
"quizAssigned": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Назначено упражнение",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Отправлять письмо студентам, когда упражнение назначено или напоминается."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Объявления курса и комментарии",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"description": "Отправлять письмо участникам курса о новых публикациях и комментариях в ленте."
|
||||||
},
|
},
|
||||||
"gradingResult": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Результаты оценки",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Отправлять письмо студентам, когда их работа оценена или обновлена."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Новые работы для оценки",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Отправлять письмо наставникам, когда студент сдаёт работу, требующую оценки."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Новый студент записывается",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Отправлять письмо наставникам, когда студент записывается на курс."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Настройки организации по умолчанию",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"description": "Если отключено здесь, никто в вашей организации не получит это письмо. Участники по-прежнему могут отказаться от писем, которые вы оставили включёнными.",
|
||||||
"save": "Save organization defaults",
|
"save": "Сохранить настройки организации по умолчанию",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Настройки уведомлений организации сохранены"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Ваши настройки электронной почты",
|
||||||
"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": "Save personal preferences",
|
"save_success": "Личные настройки уведомлений сохранены",
|
||||||
"save_success": "Personal notification preferences saved"
|
"description": "Они применяются к письмам, которые вам отправляют в этой организации. Организация по-прежнему может отключить некоторые письма для всех.",
|
||||||
|
"description_with_org": "Они применяются к письмам, которые вам отправляют в {orgName}. Организация по-прежнему может отключить некоторые письма для всех."
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Запись",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"description": "Приветственные письма, отправляемые, когда сотрудники добавляют участников в курсы или когорты."
|
||||||
},
|
},
|
||||||
"cohort": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Когорта",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"description": "Напоминания о целях обучения когорты и сроках."
|
||||||
},
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Курс",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"description": "Записи, сдачи, уроки, объявления и завершения в ваших курсах."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"personal_page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете от ClassroomIO."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profileMenu": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -3463,7 +3463,7 @@
|
|||||||
"teams_tab": "Đội",
|
"teams_tab": "Đội",
|
||||||
"ai_credits_tab": "AI Credits",
|
"ai_credits_tab": "AI Credits",
|
||||||
"ai_tutor_tab": "Gia sư AI",
|
"ai_tutor_tab": "Gia sư AI",
|
||||||
"notifications_tab": "Notifications"
|
"notifications_tab": "Thông báo"
|
||||||
},
|
},
|
||||||
"subheadings": {
|
"subheadings": {
|
||||||
"view_site": "Học viện mở"
|
"view_site": "Học viện mở"
|
||||||
@@ -3639,73 +3639,75 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"heading": "Notifications",
|
"heading": "Thông báo",
|
||||||
"page_title": "Notifications",
|
"page_title": "Thông báo",
|
||||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
"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": {
|
"toggles": {
|
||||||
"courseCompletion": {
|
"courseCompletion": {
|
||||||
"label": "Course completion and certificates",
|
"label": "Hoàn thành khóa học và chứng chỉ",
|
||||||
"description": "Email students when they complete a course and earn a certificate."
|
"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": {
|
"enrollmentWelcome": {
|
||||||
"label": "Enrollment welcome emails",
|
"label": "Email chào mừng ghi danh",
|
||||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
"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": {
|
"session": {
|
||||||
"label": "Live session reminders and updates",
|
"label": "Nhắc nhở và cập nhật buổi học trực tiếp",
|
||||||
"description": "Email students before live sessions and when session details change."
|
"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": {
|
"cohortReminder": {
|
||||||
"label": "Cohort goal reminders",
|
"label": "Nhắc nhở mục tiêu nhóm học",
|
||||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
"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": {
|
"quizAssigned": {
|
||||||
"label": "Exercise assigned",
|
"label": "Bài tập được giao",
|
||||||
"description": "Email students when an exercise is assigned or nudged."
|
"description": "Gửi email cho học viên khi bài tập được giao hoặc nhắc nhở."
|
||||||
},
|
},
|
||||||
"newsfeed": {
|
"newsfeed": {
|
||||||
"label": "Course announcements and comments",
|
"label": "Thông báo và bình luận khóa học",
|
||||||
"description": "Email course members about new posts and comments on the newsfeed."
|
"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": {
|
"gradingResult": {
|
||||||
"label": "Grading results",
|
"label": "Kết quả chấm điểm",
|
||||||
"description": "Email students when their submission is graded or updated."
|
"description": "Gửi email cho học viên khi bài nộp được chấm hoặc cập nhật."
|
||||||
},
|
},
|
||||||
"newSubmission": {
|
"newSubmission": {
|
||||||
"label": "New submissions to grade",
|
"label": "Bài nộp mới cần chấm",
|
||||||
"description": "Email tutors when a student submits work that needs grading."
|
"description": "Gửi email cho gia sư khi học viên nộp bài cần chấm điểm."
|
||||||
},
|
},
|
||||||
"newStudent": {
|
"newStudent": {
|
||||||
"label": "New student joins",
|
"label": "Học viên mới tham gia",
|
||||||
"description": "Email tutors when a student enrolls in a course."
|
"description": "Gửi email cho gia sư khi học viên ghi danh vào khóa học."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"heading": "Organization defaults",
|
"heading": "Mặc định của tổ chức",
|
||||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
"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": "Save organization defaults",
|
"save": "Lưu mặc định của tổ chức",
|
||||||
"save_success": "Organization notification defaults saved"
|
"save_success": "Đã lưu mặc định thông báo của tổ chức"
|
||||||
},
|
},
|
||||||
"personal": {
|
"personal": {
|
||||||
"heading": "Your email preferences",
|
"heading": "Tùy chọn email của bạn",
|
||||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
"save": "Lưu tùy chọn cá nhân",
|
||||||
"save": "Save personal preferences",
|
"save_success": "Đã lưu tùy chọn thông báo cá nhân",
|
||||||
"save_success": "Personal notification preferences saved"
|
"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": {
|
"sections": {
|
||||||
"enrollment": {
|
"enrollment": {
|
||||||
"heading": "Enrollment",
|
"heading": "Ghi danh",
|
||||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
"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": {
|
"cohort": {
|
||||||
"heading": "Cohort",
|
"heading": "Nhóm học",
|
||||||
"description": "Reminders about cohort learning goals and deadlines."
|
"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": {
|
"course": {
|
||||||
"heading": "Course",
|
"heading": "Khóa học",
|
||||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
"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": {
|
"profileMenu": {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { NotificationsPage } from '$features/settings/pages';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import * as Page from '@cio/ui/base/page';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{t.get('settings.notifications.page_title')} - ClassroomIO</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Page.Header>
|
||||||
|
<Page.HeaderContent>
|
||||||
|
<Page.Title>{$t('settings.notifications.heading')}</Page.Title>
|
||||||
|
<Page.Subtitle>{$t('settings.notifications.personal_page_subtitle')}</Page.Subtitle>
|
||||||
|
</Page.HeaderContent>
|
||||||
|
</Page.Header>
|
||||||
|
<Page.Body>
|
||||||
|
{#snippet child()}
|
||||||
|
<NotificationsPage />
|
||||||
|
{/snippet}
|
||||||
|
</Page.Body>
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
"db:login-link": "tsx src/scripts/generate-login-link.ts",
|
"db:login-link": "tsx src/scripts/generate-login-link.ts",
|
||||||
"db:sync-orphaned-profiles": "tsx src/scripts/sync-orphaned-profiles.ts",
|
"db:sync-orphaned-profiles": "tsx src/scripts/sync-orphaned-profiles.ts",
|
||||||
"db:backfill-profile-email-verification": "tsx src/scripts/backfill-profile-email-verification.ts",
|
"db:backfill-profile-email-verification": "tsx src/scripts/backfill-profile-email-verification.ts",
|
||||||
|
"db:backfill-member-email-notifications": "tsx src/scripts/backfill-member-email-notifications-from-profile.ts",
|
||||||
"db:update-user-email": "tsx src/scripts/update-user-email.ts",
|
"db:update-user-email": "tsx src/scripts/update-user-email.ts",
|
||||||
"db:notify-students-over-limit": "tsx src/scripts/notify-students-over-limit.ts",
|
"db:notify-students-over-limit": "tsx src/scripts/notify-students-over-limit.ts",
|
||||||
"db:downgrade-expired-subscriptions": "tsx src/scripts/downgrade-expired-subscriptions.ts",
|
"db:downgrade-expired-subscriptions": "tsx src/scripts/downgrade-expired-subscriptions.ts",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE "organizationmember_email_notifications" (
|
||||||
|
"organization_member_id" bigint PRIMARY KEY NOT NULL,
|
||||||
|
"new_student" boolean DEFAULT true NOT NULL,
|
||||||
|
"new_submission" boolean DEFAULT true NOT NULL,
|
||||||
|
"grading_result" boolean DEFAULT true NOT NULL,
|
||||||
|
"newsfeed" boolean DEFAULT true NOT NULL,
|
||||||
|
"quiz_assigned" boolean DEFAULT true NOT NULL,
|
||||||
|
"cohort_reminder" boolean DEFAULT true NOT NULL,
|
||||||
|
"session" boolean DEFAULT true NOT NULL,
|
||||||
|
"course_completion" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "organizationmember_email_notifications" ADD CONSTRAINT "organizationmember_email_notifications_organization_member_id_fkey" FOREIGN KEY ("organization_member_id") REFERENCES "public"."organizationmember"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1783909129074,
|
"when": 1783909129074,
|
||||||
"tag": "0003_profile_settings",
|
"tag": "0003_profile_settings",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1783938329367,
|
||||||
|
"tag": "0004_organizationmember_email_notifications",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { db } from '@db/drizzle';
|
||||||
|
import * as schema from '@db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import type { EmailNotificationSettings } from '@cio/utils/notifications';
|
||||||
|
|
||||||
|
export async function getOrganizationEmailNotificationSettings(
|
||||||
|
orgId: string
|
||||||
|
): Promise<EmailNotificationSettings | undefined> {
|
||||||
|
try {
|
||||||
|
const [organization] = await db
|
||||||
|
.select({ settings: schema.organization.settings })
|
||||||
|
.from(schema.organization)
|
||||||
|
.where(eq(schema.organization.id, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return organization?.settings?.emailNotifications;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getOrganizationEmailNotificationSettings error:', error);
|
||||||
|
throw new Error('Failed to get organization email notification settings');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,55 +4,11 @@ import {
|
|||||||
type PersonalEmailNotificationSettings
|
type PersonalEmailNotificationSettings
|
||||||
} from '@cio/utils/notifications';
|
} from '@cio/utils/notifications';
|
||||||
|
|
||||||
import { getProfileByEmail, getProfileById } from '../auth/profile';
|
import { getOrganizationEmailNotificationSettings } from './email-preferences-org';
|
||||||
import { db } from '@db/drizzle';
|
import { getPersonalEmailNotificationSettingsForOrg } from './organizationmember-email-notifications';
|
||||||
import * as schema from '@db/schema';
|
import { getProfileByEmail } from '../auth/profile';
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import type { EmailId } from '@cio/email';
|
import type { EmailId } from '@cio/email';
|
||||||
|
|
||||||
export async function getOrganizationEmailNotificationSettings(
|
|
||||||
orgId: string
|
|
||||||
): Promise<EmailNotificationSettings | undefined> {
|
|
||||||
try {
|
|
||||||
const [organization] = await db
|
|
||||||
.select({ settings: schema.organization.settings })
|
|
||||||
.from(schema.organization)
|
|
||||||
.where(eq(schema.organization.id, orgId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return organization?.settings?.emailNotifications;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getOrganizationEmailNotificationSettings error:', error);
|
|
||||||
throw new Error('Failed to get organization email notification settings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProfileEmailNotificationSettingsById(
|
|
||||||
profileId: string
|
|
||||||
): Promise<PersonalEmailNotificationSettings | undefined> {
|
|
||||||
try {
|
|
||||||
const profile = await getProfileById(profileId);
|
|
||||||
|
|
||||||
return profile?.settings?.emailNotifications;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getProfileEmailNotificationSettingsById error:', error);
|
|
||||||
throw new Error('Failed to get profile email notification settings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProfileEmailNotificationSettingsByEmail(
|
|
||||||
email: string
|
|
||||||
): Promise<PersonalEmailNotificationSettings | undefined> {
|
|
||||||
try {
|
|
||||||
const profile = await getProfileByEmail(email);
|
|
||||||
|
|
||||||
return profile?.settings?.emailNotifications;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getProfileEmailNotificationSettingsByEmail error:', error);
|
|
||||||
throw new Error('Failed to get profile email notification settings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function shouldSendEmail(input: {
|
export async function shouldSendEmail(input: {
|
||||||
emailId: EmailId;
|
emailId: EmailId;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
@@ -67,7 +23,7 @@ export async function shouldSendEmail(input: {
|
|||||||
*/
|
*/
|
||||||
export class EmailPreferenceLookupCache {
|
export class EmailPreferenceLookupCache {
|
||||||
private orgSettingsById = new Map<string, EmailNotificationSettings | undefined>();
|
private orgSettingsById = new Map<string, EmailNotificationSettings | undefined>();
|
||||||
private profileSettingsById = new Map<string, PersonalEmailNotificationSettings | undefined>();
|
private personalSettingsByOrgMember = new Map<string, PersonalEmailNotificationSettings | undefined>();
|
||||||
private profileIdByEmail = new Map<string, string | undefined>();
|
private profileIdByEmail = new Map<string, string | undefined>();
|
||||||
|
|
||||||
async shouldSend(input: {
|
async shouldSend(input: {
|
||||||
@@ -84,7 +40,8 @@ export class EmailPreferenceLookupCache {
|
|||||||
profileId = await this.getProfileIdByEmail(input.recipientEmail);
|
profileId = await this.getProfileIdByEmail(input.recipientEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
const personalSettings = profileId ? await this.getProfileSettings(profileId) : undefined;
|
const personalSettings =
|
||||||
|
profileId && input.organizationId ? await this.getPersonalSettings(profileId, input.organizationId) : undefined;
|
||||||
|
|
||||||
return resolveEmailDelivery(input.emailId, orgSettings, personalSettings);
|
return resolveEmailDelivery(input.emailId, orgSettings, personalSettings);
|
||||||
}
|
}
|
||||||
@@ -107,12 +64,17 @@ export class EmailPreferenceLookupCache {
|
|||||||
return this.profileIdByEmail.get(email);
|
return this.profileIdByEmail.get(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProfileSettings(profileId: string): Promise<PersonalEmailNotificationSettings | undefined> {
|
private async getPersonalSettings(
|
||||||
if (!this.profileSettingsById.has(profileId)) {
|
profileId: string,
|
||||||
const settings = await getProfileEmailNotificationSettingsById(profileId);
|
organizationId: string
|
||||||
this.profileSettingsById.set(profileId, settings);
|
): Promise<PersonalEmailNotificationSettings | undefined> {
|
||||||
|
const cacheKey = `${profileId}:${organizationId}`;
|
||||||
|
|
||||||
|
if (!this.personalSettingsByOrgMember.has(cacheKey)) {
|
||||||
|
const settings = await getPersonalEmailNotificationSettingsForOrg(profileId, organizationId);
|
||||||
|
this.personalSettingsByOrgMember.set(cacheKey, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.profileSettingsById.get(profileId);
|
return this.personalSettingsByOrgMember.get(cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export * from './email-preferences';
|
export * from './email-preferences';
|
||||||
|
export * from './email-preferences-org';
|
||||||
|
export * from './organizationmember-email-notifications';
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_MEMBER_EMAIL_NOTIFICATION_ROW,
|
||||||
|
memberEmailNotificationRowToApiResponse,
|
||||||
|
memberEmailNotificationRowToPersonalSettings,
|
||||||
|
personalSettingsToMemberEmailNotificationRow,
|
||||||
|
type MemberEmailNotificationRow
|
||||||
|
} from '@cio/utils/notifications';
|
||||||
|
import type { PersonalEmailNotificationSettings } from '@cio/utils/notifications';
|
||||||
|
import type { TMemberEmailNotificationPreferencesUpdate } from '@cio/utils/validation/notifications';
|
||||||
|
|
||||||
|
import { getOrganizationMemberIdByOrgAndProfile } from '../organization/organization';
|
||||||
|
import { db } from '@db/drizzle';
|
||||||
|
import * as schema from '@db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
function rowFromRecord(
|
||||||
|
record: typeof schema.organizationmemberEmailNotifications.$inferSelect | undefined
|
||||||
|
): MemberEmailNotificationRow | null {
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newStudent: record.newStudent,
|
||||||
|
newSubmission: record.newSubmission,
|
||||||
|
gradingResult: record.gradingResult,
|
||||||
|
newsfeed: record.newsfeed,
|
||||||
|
quizAssigned: record.quizAssigned,
|
||||||
|
cohortReminder: record.cohortReminder,
|
||||||
|
session: record.session,
|
||||||
|
courseCompletion: record.courseCompletion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrganizationMemberEmailNotifications(
|
||||||
|
profileId: string,
|
||||||
|
organizationId: string
|
||||||
|
): Promise<MemberEmailNotificationRow | null> {
|
||||||
|
try {
|
||||||
|
const organizationMemberId = await getOrganizationMemberIdByOrgAndProfile(organizationId, profileId);
|
||||||
|
|
||||||
|
if (!organizationMemberId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [record] = await db
|
||||||
|
.select()
|
||||||
|
.from(schema.organizationmemberEmailNotifications)
|
||||||
|
.where(eq(schema.organizationmemberEmailNotifications.organizationMemberId, organizationMemberId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return rowFromRecord(record);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getOrganizationMemberEmailNotifications error:', error);
|
||||||
|
throw new Error('Failed to get organization member email notification preferences');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrganizationMemberEmailNotificationsForApi(profileId: string, organizationId: string) {
|
||||||
|
const row = await getOrganizationMemberEmailNotifications(profileId, organizationId);
|
||||||
|
|
||||||
|
return memberEmailNotificationRowToApiResponse(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPersonalEmailNotificationSettingsForOrg(
|
||||||
|
profileId: string,
|
||||||
|
organizationId: string
|
||||||
|
): Promise<PersonalEmailNotificationSettings | undefined> {
|
||||||
|
const row = await getOrganizationMemberEmailNotifications(profileId, organizationId);
|
||||||
|
|
||||||
|
return memberEmailNotificationRowToPersonalSettings(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertOrganizationMemberEmailNotifications(
|
||||||
|
profileId: string,
|
||||||
|
organizationId: string,
|
||||||
|
preferences: TMemberEmailNotificationPreferencesUpdate
|
||||||
|
): Promise<MemberEmailNotificationRow> {
|
||||||
|
try {
|
||||||
|
const organizationMemberId = await getOrganizationMemberIdByOrgAndProfile(organizationId, profileId);
|
||||||
|
|
||||||
|
if (!organizationMemberId) {
|
||||||
|
throw new Error('Organization member not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await getOrganizationMemberEmailNotifications(profileId, organizationId);
|
||||||
|
const merged = personalSettingsToMemberEmailNotificationRow({
|
||||||
|
...(existing ?? DEFAULT_MEMBER_EMAIL_NOTIFICATION_ROW),
|
||||||
|
...preferences
|
||||||
|
});
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const [record] = await db
|
||||||
|
.insert(schema.organizationmemberEmailNotifications)
|
||||||
|
.values({
|
||||||
|
organizationMemberId,
|
||||||
|
...merged,
|
||||||
|
updatedAt
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: schema.organizationmemberEmailNotifications.organizationMemberId,
|
||||||
|
set: {
|
||||||
|
...merged,
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const row = rowFromRecord(record);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Failed to upsert organization member email notification preferences');
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('upsertOrganizationMemberEmailNotifications error:', error);
|
||||||
|
throw new Error('Failed to update organization member email notification preferences');
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-12
@@ -410,18 +410,7 @@ export const profile = pgTable(
|
|||||||
verifiedAt: timestamp('verified_at', { withTimezone: true, mode: 'string' }),
|
verifiedAt: timestamp('verified_at', { withTimezone: true, mode: 'string' }),
|
||||||
locale: locale().default('en'),
|
locale: locale().default('en'),
|
||||||
isRestricted: boolean('is_restricted').default(false).notNull(),
|
isRestricted: boolean('is_restricted').default(false).notNull(),
|
||||||
settings: jsonb().default({}).$type<{
|
settings: jsonb().default({}).$type<Record<string, unknown>>()
|
||||||
emailNotifications?: {
|
|
||||||
newStudent?: boolean;
|
|
||||||
newSubmission?: boolean;
|
|
||||||
gradingResult?: boolean;
|
|
||||||
newsfeed?: boolean;
|
|
||||||
quizAssigned?: boolean;
|
|
||||||
cohortReminder?: boolean;
|
|
||||||
session?: boolean;
|
|
||||||
courseCompletion?: boolean;
|
|
||||||
};
|
|
||||||
}>()
|
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
foreignKey({
|
foreignKey({
|
||||||
@@ -1860,6 +1849,30 @@ export const organizationmember = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const organizationmemberEmailNotifications = pgTable(
|
||||||
|
'organizationmember_email_notifications',
|
||||||
|
{
|
||||||
|
organizationMemberId: bigint('organization_member_id', { mode: 'number' }).primaryKey().notNull(),
|
||||||
|
newStudent: boolean('new_student').default(true).notNull(),
|
||||||
|
newSubmission: boolean('new_submission').default(true).notNull(),
|
||||||
|
gradingResult: boolean('grading_result').default(true).notNull(),
|
||||||
|
newsfeed: boolean('newsfeed').default(true).notNull(),
|
||||||
|
quizAssigned: boolean('quiz_assigned').default(true).notNull(),
|
||||||
|
cohortReminder: boolean('cohort_reminder').default(true).notNull(),
|
||||||
|
session: boolean('session').default(true).notNull(),
|
||||||
|
courseCompletion: boolean('course_completion').default(true).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull()
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
foreignKey({
|
||||||
|
columns: [table.organizationMemberId],
|
||||||
|
foreignColumns: [organizationmember.id],
|
||||||
|
name: 'organizationmember_email_notifications_organization_member_id_fkey'
|
||||||
|
}).onDelete('cascade')
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const question = pgTable(
|
export const question = pgTable(
|
||||||
'question',
|
'question',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Copies legacy profile.settings.emailNotifications into
|
||||||
|
* organizationmember_email_notifications (one row per membership), then
|
||||||
|
* removes the old jsonb key from profile.settings.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* pnpm db:backfill-member-email-notifications # dry run (default)
|
||||||
|
* pnpm db:backfill-member-email-notifications -- --execute # apply updates
|
||||||
|
*/
|
||||||
|
import 'dotenv/config';
|
||||||
|
|
||||||
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
const connectionString = process.env.DATABASE_URL ?? process.env.PRIVATE_DATABASE_URL ?? '';
|
||||||
|
const shouldExecute = process.argv.includes('--execute');
|
||||||
|
|
||||||
|
if (!connectionString) {
|
||||||
|
console.error('DATABASE_URL or PRIVATE_DATABASE_URL environment variable is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AFFECTED_MEMBERSHIPS_SQL = `
|
||||||
|
SELECT
|
||||||
|
om.id AS organization_member_id,
|
||||||
|
p.id AS profile_id,
|
||||||
|
om.organization_id,
|
||||||
|
p.settings->'emailNotifications' AS legacy_preferences
|
||||||
|
FROM organizationmember om
|
||||||
|
INNER JOIN profile p ON p.id = om.profile_id
|
||||||
|
WHERE p.settings ? 'emailNotifications'
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const sql = postgres(connectionString);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const affected = await sql.unsafe(AFFECTED_MEMBERSHIPS_SQL);
|
||||||
|
console.log(`Found ${affected.length} organization membership(s) with legacy profile email preferences.`);
|
||||||
|
|
||||||
|
if (affected.length === 0) {
|
||||||
|
console.log('Nothing to do.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldExecute) {
|
||||||
|
console.log('Dry run only. Re-run with --execute to apply updates.');
|
||||||
|
console.log('Sample (up to 10):');
|
||||||
|
for (const row of affected.slice(0, 10)) {
|
||||||
|
console.log(` member=${row.organization_member_id} profile=${row.profile_id} org=${row.organization_id}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = await sql`
|
||||||
|
INSERT INTO organizationmember_email_notifications (
|
||||||
|
organization_member_id,
|
||||||
|
new_student,
|
||||||
|
new_submission,
|
||||||
|
grading_result,
|
||||||
|
newsfeed,
|
||||||
|
quiz_assigned,
|
||||||
|
cohort_reminder,
|
||||||
|
session,
|
||||||
|
course_completion
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
om.id,
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'newStudent')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'newSubmission')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'gradingResult')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'newsfeed')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'quizAssigned')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'cohortReminder')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'session')::boolean, true),
|
||||||
|
COALESCE((p.settings->'emailNotifications'->>'courseCompletion')::boolean, true)
|
||||||
|
FROM organizationmember om
|
||||||
|
INNER JOIN profile p ON p.id = om.profile_id
|
||||||
|
WHERE p.settings ? 'emailNotifications'
|
||||||
|
ON CONFLICT (organization_member_id) DO NOTHING
|
||||||
|
RETURNING organization_member_id
|
||||||
|
`;
|
||||||
|
|
||||||
|
const clearedProfiles = await sql`
|
||||||
|
UPDATE profile
|
||||||
|
SET settings = settings - 'emailNotifications'
|
||||||
|
WHERE settings ? 'emailNotifications'
|
||||||
|
RETURNING id
|
||||||
|
`;
|
||||||
|
|
||||||
|
const remaining = await sql.unsafe(`SELECT COUNT(*)::int AS count FROM (${AFFECTED_MEMBERSHIPS_SQL}) AS affected`);
|
||||||
|
const remainingCount = remaining[0]?.count ?? 0;
|
||||||
|
|
||||||
|
console.log(`Inserted ${inserted.length} organizationmember_email_notifications row(s).`);
|
||||||
|
console.log(`Cleared legacy preferences from ${clearedProfiles.length} profile(s).`);
|
||||||
|
console.log(`Remaining memberships with legacy profile preferences: ${remainingCount}`);
|
||||||
|
|
||||||
|
if (remainingCount > 0) {
|
||||||
|
console.warn('Some rows were not migrated — inspect manually before retrying.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Backfill complete.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('backfill-member-email-notifications-from-profile error:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await sql.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -141,6 +141,8 @@ export type TNewLessonLanguageHistory = typeof schema.lessonLanguageHistory.$inf
|
|||||||
|
|
||||||
export type TOrganizationmember = typeof schema.organizationmember.$inferSelect;
|
export type TOrganizationmember = typeof schema.organizationmember.$inferSelect;
|
||||||
export type TNewOrganizationmember = typeof schema.organizationmember.$inferInsert;
|
export type TNewOrganizationmember = typeof schema.organizationmember.$inferInsert;
|
||||||
|
export type TOrganizationmemberEmailNotifications = typeof schema.organizationmemberEmailNotifications.$inferSelect;
|
||||||
|
export type TNewOrganizationmemberEmailNotifications = typeof schema.organizationmemberEmailNotifications.$inferInsert;
|
||||||
|
|
||||||
export type TQuestion = typeof schema.question.$inferSelect;
|
export type TQuestion = typeof schema.question.$inferSelect;
|
||||||
export type TNewQuestion = typeof schema.question.$inferInsert;
|
export type TNewQuestion = typeof schema.question.$inferInsert;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './email-toggles';
|
export * from './email-toggles';
|
||||||
|
export * from './member-email-notifications';
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { PersonalEmailNotificationSettings, PersonalEmailNotificationToggleKey } from './email-toggles';
|
||||||
|
import { PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS } from './email-toggles';
|
||||||
|
|
||||||
|
export type MemberEmailNotificationRow = {
|
||||||
|
newStudent: boolean;
|
||||||
|
newSubmission: boolean;
|
||||||
|
gradingResult: boolean;
|
||||||
|
newsfeed: boolean;
|
||||||
|
quizAssigned: boolean;
|
||||||
|
cohortReminder: boolean;
|
||||||
|
session: boolean;
|
||||||
|
courseCompletion: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_MEMBER_EMAIL_NOTIFICATION_ROW: MemberEmailNotificationRow = {
|
||||||
|
newStudent: true,
|
||||||
|
newSubmission: true,
|
||||||
|
gradingResult: true,
|
||||||
|
newsfeed: true,
|
||||||
|
quizAssigned: true,
|
||||||
|
cohortReminder: true,
|
||||||
|
session: true,
|
||||||
|
courseCompletion: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export function memberEmailNotificationRowToPersonalSettings(
|
||||||
|
row: MemberEmailNotificationRow | null | undefined
|
||||||
|
): PersonalEmailNotificationSettings | undefined {
|
||||||
|
if (!row) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings: Partial<Record<PersonalEmailNotificationToggleKey, boolean>> = {};
|
||||||
|
|
||||||
|
for (const key of PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS) {
|
||||||
|
if (row[key] === false) {
|
||||||
|
settings[key] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function personalSettingsToMemberEmailNotificationRow(
|
||||||
|
preferences: Partial<PersonalEmailNotificationSettings> | MemberEmailNotificationRow
|
||||||
|
): MemberEmailNotificationRow {
|
||||||
|
return {
|
||||||
|
newStudent: preferences.newStudent ?? true,
|
||||||
|
newSubmission: preferences.newSubmission ?? true,
|
||||||
|
gradingResult: preferences.gradingResult ?? true,
|
||||||
|
newsfeed: preferences.newsfeed ?? true,
|
||||||
|
quizAssigned: preferences.quizAssigned ?? true,
|
||||||
|
cohortReminder: preferences.cohortReminder ?? true,
|
||||||
|
session: preferences.session ?? true,
|
||||||
|
courseCompletion: preferences.courseCompletion ?? true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memberEmailNotificationRowToApiResponse(row: MemberEmailNotificationRow | null | undefined) {
|
||||||
|
return personalSettingsToMemberEmailNotificationRow(row ?? DEFAULT_MEMBER_EMAIL_NOTIFICATION_ROW);
|
||||||
|
}
|
||||||
@@ -1,22 +1,10 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
const ZEmailNotificationToggleFields = {
|
|
||||||
newStudent: z.boolean().optional(),
|
|
||||||
newSubmission: z.boolean().optional(),
|
|
||||||
gradingResult: z.boolean().optional(),
|
|
||||||
newsfeed: z.boolean().optional(),
|
|
||||||
quizAssigned: z.boolean().optional(),
|
|
||||||
cohortReminder: z.boolean().optional(),
|
|
||||||
session: z.boolean().optional(),
|
|
||||||
courseCompletion: z.boolean().optional()
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ZUpdateProfile = z.object({
|
export const ZUpdateProfile = z.object({
|
||||||
fullname: z.string().min(3).optional(),
|
fullname: z.string().min(3).optional(),
|
||||||
username: z.string().min(3).optional(),
|
username: z.string().min(3).optional(),
|
||||||
locale: z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']).optional(),
|
locale: z.enum(['en', 'hi', 'fr', 'pt', 'de', 'vi', 'ru', 'es', 'pl', 'da']).optional(),
|
||||||
avatarUrl: z.url().optional(),
|
avatarUrl: z.url().optional()
|
||||||
emailNotifications: z.object(ZEmailNotificationToggleFields).optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUpdateProfile = z.infer<typeof ZUpdateProfile>;
|
export type TUpdateProfile = z.infer<typeof ZUpdateProfile>;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const ZPersonalEmailNotificationToggleFields = {
|
||||||
|
newStudent: z.boolean(),
|
||||||
|
newSubmission: z.boolean(),
|
||||||
|
gradingResult: z.boolean(),
|
||||||
|
newsfeed: z.boolean(),
|
||||||
|
quizAssigned: z.boolean(),
|
||||||
|
cohortReminder: z.boolean(),
|
||||||
|
session: z.boolean(),
|
||||||
|
courseCompletion: z.boolean()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ZMemberEmailNotificationPreferences = z.object(ZPersonalEmailNotificationToggleFields);
|
||||||
|
|
||||||
|
export type TMemberEmailNotificationPreferences = z.infer<typeof ZMemberEmailNotificationPreferences>;
|
||||||
|
|
||||||
|
export const ZMemberEmailNotificationPreferencesUpdate = z.object(ZPersonalEmailNotificationToggleFields).partial();
|
||||||
|
|
||||||
|
export type TMemberEmailNotificationPreferencesUpdate = z.infer<typeof ZMemberEmailNotificationPreferencesUpdate>;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './email-notifications';
|
||||||
Reference in New Issue
Block a user