feat(notifications): per-org member email preferences table (#751)
* feat(notifications): add email notification preferences Implement the email notification preferences PRD with org-level defaults and personal opt-outs enforced at the email enqueue chokepoint. - Add toggle map, profile.settings jsonb, and shouldSendEmail resolver - Extend profile and organization update APIs with emailNotifications - Pass preference context through all toggleable email send call sites - Add Settings > Notifications page with personal and org admin sections Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(notifications): group email toggles into course, cohort, and enrollment sections Organize personal and org notification settings into scannable sections so users can find course- or cohort-related emails quickly. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): use Exercise instead of Quiz in toggle copy Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): only mark certification email sent when enqueued Skip setMemberCertificationEmailSent when notification preferences or missing Redis prevent the completion email from being queued, so the email can still send if preferences change later. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): address PR review feedback - Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch processors and multi-recipient email enqueue paths - Replace $effect store hydration in notifications settings with onMount and explicit sync after save - Remove thin api email-preferences re-export; import from db queries - Hide tutor-only personal toggles (newStudent, newSubmission) for students Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): hydrate toggles after store load and add profile settings migration - One-time store hydration via onMount plus guarded effect when profile/org load after mount, without clobbering in-progress edits - Add drizzle migration for profile.settings jsonb column Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(notifications): add email preferences to LMS settings Students manage personal email opt-outs from /lms/settings/notifications, matching the org settings page but scoped to the LMS navigation students use. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(notifications): store per-org personal prefs in organizationmember_email_notifications - Add organizationmember_email_notifications table keyed by organizationmember - Expose GET/PUT /organization/member/email-notifications for current org member - Resolve personal email prefs by profileId + organizationId at send time - Migrate legacy profile.settings.emailNotifications into per-membership rows - Update org and LMS settings UI to load/save org-scoped member preferences Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(notifications): address PR review on locales and backfill script Move profile email-notification backfill out of the migration SQL into a dedicated db script with dry-run support. Translate personal preference description strings in all non-English locale files. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(i18n): translate notification preference strings in all locales Complete settings.notifications and settings.tabs.notifications_tab translations for da, de, es, fr, hi, pl, pt, ru, and vi. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
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 { autoJoinOrg } from '@api/services/organization/auto-join';
|
||||
import { organizationAiTutorRouter } from '@api/routes/organization/ai-tutor';
|
||||
import { organizationMemberEmailNotificationsRouter } from '@api/routes/organization/member-email-notifications';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
|
||||
import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key';
|
||||
@@ -768,4 +769,5 @@ export const organizationRouter = new Hono()
|
||||
.route('/widgets', widgetsRouter)
|
||||
.route('/assets', assetsRouter)
|
||||
.route('/ai-tutor', organizationAiTutorRouter)
|
||||
.route('/member/email-notifications', organizationMemberEmailNotificationsRouter)
|
||||
.route('/:orgId/quiz', quizRouter);
|
||||
|
||||
@@ -100,16 +100,7 @@ export async function getAccountData(userId: string): Promise<GetAccountDataResu
|
||||
*/
|
||||
export async function updateUser(userId: string, data: TUpdateProfile) {
|
||||
try {
|
||||
const { emailNotifications, ...profileFields } = data;
|
||||
const updatePayload: Partial<Omit<TProfile, 'id' | 'email' | 'createdAt' | 'updatedAt'>> = {
|
||||
...profileFields
|
||||
};
|
||||
|
||||
if (emailNotifications !== undefined) {
|
||||
updatePayload.settings = { emailNotifications };
|
||||
}
|
||||
|
||||
const updatedProfile = await updateProfile(userId, updatePayload);
|
||||
const updatedProfile = await updateProfile(userId, data);
|
||||
|
||||
if (!updatedProfile) {
|
||||
throw new AppError(`Profile not found for user ID: ${userId}`, ErrorCodes.PROFILE_NOT_FOUND, 404);
|
||||
|
||||
@@ -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) {
|
||||
// Validate email format
|
||||
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,
|
||||
TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS
|
||||
} from '@cio/utils/notifications';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { isOrgStudent } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { profileApi } from '$features/auth/api/profile.svelte';
|
||||
import { memberEmailNotificationsApi } from '$features/settings/api/member-email-notifications.svelte';
|
||||
import { orgApi } from '$features/org/api/org.svelte';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
@@ -26,10 +25,12 @@
|
||||
}
|
||||
|
||||
let personalToggles = $state(buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS));
|
||||
let savedPersonalToggles = $state(buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS));
|
||||
let orgToggles = $state(buildToggleState(EMAIL_NOTIFICATION_TOGGLE_KEYS));
|
||||
let isSavingPersonal = $state(false);
|
||||
let isSavingOrg = $state(false);
|
||||
let hasHydratedFromStores = $state(false);
|
||||
let isLoadingPersonal = $state(false);
|
||||
let hasHydratedOrgToggles = $state(false);
|
||||
|
||||
const tutorOnlyToggleKeys = new Set<string>(TUTOR_ONLY_PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS);
|
||||
|
||||
@@ -42,12 +43,18 @@
|
||||
: PERSONAL_EMAIL_NOTIFICATION_SECTIONS
|
||||
);
|
||||
|
||||
function syncPersonalTogglesFromProfile() {
|
||||
const profileState = get(profile);
|
||||
const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, profileState.settings?.emailNotifications);
|
||||
const personalDescription = $derived(
|
||||
$currentOrg?.name
|
||||
? t.get('settings.notifications.personal.description_with_org', { orgName: $currentOrg.name })
|
||||
: t.get('settings.notifications.personal.description')
|
||||
);
|
||||
|
||||
function applyPersonalToggleState(source?: Partial<Record<string, boolean>> | null) {
|
||||
const next = buildToggleState(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, source);
|
||||
|
||||
for (const key of PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS) {
|
||||
personalToggles[key] = next[key];
|
||||
savedPersonalToggles[key] = next[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,41 +70,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
function tryHydrateFromStores() {
|
||||
if (hasHydratedFromStores) return;
|
||||
async function loadPersonalPreferences() {
|
||||
if (!$currentOrg?.id) return;
|
||||
|
||||
const profileState = get(profile);
|
||||
if (!profileState.id) return;
|
||||
isLoadingPersonal = true;
|
||||
|
||||
syncPersonalTogglesFromProfile();
|
||||
const result = await memberEmailNotificationsApi.fetch();
|
||||
|
||||
if (get(isOrgAdmin)) {
|
||||
if (!get(currentOrg)?.id) return;
|
||||
syncOrgTogglesFromCurrentOrg();
|
||||
if (result?.data) {
|
||||
applyPersonalToggleState(result.data);
|
||||
}
|
||||
|
||||
hasHydratedFromStores = true;
|
||||
isLoadingPersonal = false;
|
||||
}
|
||||
|
||||
function tryHydrateOrgToggles() {
|
||||
if (hasHydratedOrgToggles || !get(isOrgAdmin)) return;
|
||||
|
||||
if (!get(currentOrg)?.id) return;
|
||||
|
||||
syncOrgTogglesFromCurrentOrg();
|
||||
hasHydratedOrgToggles = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
tryHydrateFromStores();
|
||||
tryHydrateOrgToggles();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (hasHydratedFromStores) return;
|
||||
const orgId = $currentOrg?.id;
|
||||
|
||||
const profileId = $profile.id;
|
||||
const orgId = $isOrgAdmin ? $currentOrg?.id : 'ready';
|
||||
if (!orgId) return;
|
||||
|
||||
if (!profileId || ($isOrgAdmin && !orgId)) return;
|
||||
void loadPersonalPreferences();
|
||||
});
|
||||
|
||||
tryHydrateFromStores();
|
||||
$effect(() => {
|
||||
if (hasHydratedOrgToggles) return;
|
||||
|
||||
const orgId = $isOrgAdmin ? $currentOrg?.id : null;
|
||||
|
||||
if (!orgId) return;
|
||||
|
||||
tryHydrateOrgToggles();
|
||||
});
|
||||
|
||||
const personalHasChanges = $derived(
|
||||
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some(
|
||||
(key) => personalToggles[key] !== ($profile.settings?.emailNotifications?.[key] !== false)
|
||||
)
|
||||
PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS.some((key) => personalToggles[key] !== savedPersonalToggles[key])
|
||||
);
|
||||
|
||||
const orgHasChanges = $derived(
|
||||
@@ -117,14 +136,16 @@
|
||||
}
|
||||
|
||||
async function handleSavePersonal() {
|
||||
if (!$currentOrg?.id) return;
|
||||
|
||||
isSavingPersonal = true;
|
||||
|
||||
await profileApi.updateEmailNotifications(
|
||||
const result = await memberEmailNotificationsApi.update(
|
||||
toStoredPreferences(PERSONAL_EMAIL_NOTIFICATION_TOGGLE_KEYS, personalToggles)
|
||||
);
|
||||
|
||||
if (profileApi.success) {
|
||||
syncPersonalTogglesFromProfile();
|
||||
if (memberEmailNotificationsApi.success && result?.data) {
|
||||
applyPersonalToggleState(result.data);
|
||||
snackbar.success(t.get('settings.notifications.personal.save_success'));
|
||||
}
|
||||
|
||||
@@ -164,7 +185,7 @@
|
||||
{$t('settings.notifications.personal.heading')}
|
||||
</Field.Legend>
|
||||
<Field.Description>
|
||||
{$t('settings.notifications.personal.description')}
|
||||
{personalDescription}
|
||||
</Field.Description>
|
||||
|
||||
<div class="mt-4 space-y-6">
|
||||
@@ -182,7 +203,7 @@
|
||||
<div class="mt-4 space-y-4">
|
||||
{#each section.keys as key (key)}
|
||||
<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">
|
||||
<Field.Label>{$t(`settings.notifications.toggles.${key}.label`)}</Field.Label>
|
||||
<Field.Description>
|
||||
@@ -200,7 +221,7 @@
|
||||
<Button
|
||||
variant="secondary"
|
||||
loading={isSavingPersonal}
|
||||
disabled={!personalHasChanges || isSavingPersonal}
|
||||
disabled={!personalHasChanges || isSavingPersonal || isLoadingPersonal || !$currentOrg?.id}
|
||||
onclick={handleSavePersonal}
|
||||
>
|
||||
{$t('settings.notifications.personal.save')}
|
||||
|
||||
@@ -16,3 +16,14 @@ export type AiUsageData = GetUsageSuccess['data'];
|
||||
export type PurchasedSummaryData = GetPurchasedSuccess['data'];
|
||||
export type LeaderboardData = GetLeaderboardSuccess['data'];
|
||||
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',
|
||||
path: '/settings'
|
||||
},
|
||||
{
|
||||
titleKey: 'Notifications',
|
||||
path: '/settings/notifications'
|
||||
},
|
||||
{
|
||||
titleKey: '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",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "AI underviser",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Notifikationer"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Åbent Akademi"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Notifikationer",
|
||||
"page_title": "Notifikationer",
|
||||
"page_subtitle": "Vælg hvilke e-mailnotifikationer du modtager, og som administrator hvilke e-mails din organisation sender.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Kursusafslutning og certifikater",
|
||||
"description": "Send e-mail til studerende, når de gennemfører et kursus og opnår et certifikat."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Velkomst-e-mails ved tilmelding",
|
||||
"description": "Send velkomst-e-mails, når personalet tilmelder studerende til kurser eller kohorter."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Påmindelser og opdateringer om livesessioner",
|
||||
"description": "Send e-mail til studerende før livesessioner og når sessionsdetaljer ændres."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Påmindelser om kohortemål",
|
||||
"description": "Send e-mail til studerende om kommende eller overskredne kohortlæringsmål."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Øvelse tildelt",
|
||||
"description": "Send e-mail til studerende, når en øvelse tildeles eller påmindes."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Kursusmeddelelser og kommentarer",
|
||||
"description": "Send e-mail til kursusmedlemmer om nye opslag og kommentarer på nyhedsfeedet."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Bedømmelsesresultater",
|
||||
"description": "Send e-mail til studerende, når deres aflevering er bedømt eller opdateret."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Nye afleveringer til bedømmelse",
|
||||
"description": "Send e-mail til undervisere, når en studerende afleverer arbejde, der skal bedømmes."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Ny studerende tilmelder sig",
|
||||
"description": "Send e-mail til undervisere, når en studerende tilmelder sig et kursus."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Organisationsstandarder",
|
||||
"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": "Gem organisationsstandarder",
|
||||
"save_success": "Organisationsnotifikationsstandarder gemt"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Dine e-mailpræferencer",
|
||||
"save": "Gem personlige præferencer",
|
||||
"save_success": "Personlige notifikationspræferencer gemt",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Tilmelding",
|
||||
"description": "Velkomst-e-mails sendt, når personalet tilføjer medlemmer til kurser eller kohorter."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Kohorte",
|
||||
"description": "Påmindelser om kohortens læringsmål og deadlines."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Kursus",
|
||||
"description": "Tilmeldinger, afleveringer, lektioner, meddelelser og gennemførelser i dine kurser."
|
||||
}
|
||||
},
|
||||
"personal_page_subtitle": "Vælg hvilke e-mailnotifikationer du modtager fra ClassroomIO."
|
||||
}
|
||||
},
|
||||
"profileMenu": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "Mannschaften",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "KI-Tutor",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Benachrichtigungen"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Offene Akademie"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Benachrichtigungen",
|
||||
"page_title": "Benachrichtigungen",
|
||||
"page_subtitle": "Wählen Sie, welche E-Mail-Benachrichtigungen Sie erhalten, und als Administrator, welche E-Mails Ihre Organisation sendet.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Kursabschluss und Zertifikate",
|
||||
"description": "E-Mail an Studenten senden, wenn sie einen Kurs abschließen und ein Zertifikat erhalten."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Willkommens-E-Mails bei Anmeldung",
|
||||
"description": "Willkommens-E-Mails senden, wenn Mitarbeitende Studenten zu Kursen oder Kohorten anmelden."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Erinnerungen und Updates zu Live-Sitzungen",
|
||||
"description": "E-Mail an Studenten vor Live-Sitzungen und bei Änderungen der Sitzungsdetails senden."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Erinnerungen an Kohortenlernziele",
|
||||
"description": "E-Mail an Studenten über bevorstehende oder überfällige Kohortenlernziele senden."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Übung zugewiesen",
|
||||
"description": "E-Mail an Studenten senden, wenn eine Übung zugewiesen oder erinnert wird."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Kursankündigungen und Kommentare",
|
||||
"description": "E-Mail an Kursmitglieder über neue Beiträge und Kommentare im Newsfeed senden."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Bewertungsergebnisse",
|
||||
"description": "E-Mail an Studenten senden, wenn ihre Abgabe bewertet oder aktualisiert wird."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Neue Abgaben zum Bewerten",
|
||||
"description": "E-Mail an Tutoren senden, wenn ein Student Arbeit zur Bewertung einreicht."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Neuer Student meldet sich an",
|
||||
"description": "E-Mail an Tutoren senden, wenn sich ein Student für einen Kurs anmeldet."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Organisationsstandards",
|
||||
"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": "Organisationsstandards speichern",
|
||||
"save_success": "Organisations-Benachrichtigungsstandards gespeichert"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Ihre E-Mail-Einstellungen",
|
||||
"save": "Persönliche Einstellungen speichern",
|
||||
"save_success": "Persönliche Benachrichtigungseinstellungen gespeichert",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Anmeldung",
|
||||
"description": "Willkommens-E-Mails, die gesendet werden, wenn Mitarbeitende Mitglieder zu Kursen oder Kohorten hinzufügen."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Kohorte",
|
||||
"description": "Erinnerungen an Lernziele und Fristen der Kohorte."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Kurs",
|
||||
"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": {
|
||||
|
||||
@@ -440,9 +440,11 @@
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"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": {
|
||||
"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_success": "Personal notification preferences saved"
|
||||
},
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "equipos",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "Tutor de IA",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Notificaciones"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Academia Abierta"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Notificaciones",
|
||||
"page_title": "Notificaciones",
|
||||
"page_subtitle": "Elige qué notificaciones por correo recibes y, como administrador, qué correos envía tu organización.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Finalización del curso y certificados",
|
||||
"description": "Enviar correo a estudiantes cuando completan un curso y obtienen un certificado."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Correos de bienvenida por inscripción",
|
||||
"description": "Enviar correos de bienvenida cuando el personal inscribe estudiantes en cursos o cohortes."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Recordatorios y actualizaciones de sesiones en vivo",
|
||||
"description": "Enviar correo a estudiantes antes de sesiones en vivo y cuando cambian los detalles."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Recordatorios de objetivos de cohorte",
|
||||
"description": "Enviar correo a estudiantes sobre objetivos de aprendizaje próximos o vencidos."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Ejercicio asignado",
|
||||
"description": "Enviar correo a estudiantes cuando se asigna o recuerda un ejercicio."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Anuncios y comentarios del curso",
|
||||
"description": "Enviar correo a los miembros del curso sobre nuevas publicaciones y comentarios en el feed."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Resultados de calificación",
|
||||
"description": "Enviar correo a estudiantes cuando su entrega es calificada o actualizada."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Nuevas entregas para calificar",
|
||||
"description": "Enviar correo a tutores cuando un estudiante entrega trabajo que necesita calificación."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Nuevo estudiante se inscribe",
|
||||
"description": "Enviar correo a tutores cuando un estudiante se inscribe en un curso."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Valores predeterminados de la organización",
|
||||
"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": "Guardar valores predeterminados de la organización",
|
||||
"save_success": "Valores predeterminados de notificación de la organización guardados"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Tus preferencias de correo",
|
||||
"save": "Guardar preferencias personales",
|
||||
"save_success": "Preferencias de notificación personales guardadas",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Inscripción",
|
||||
"description": "Correos de bienvenida enviados cuando el personal añade miembros a cursos o cohortes."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Cohorte",
|
||||
"description": "Recordatorios sobre objetivos de aprendizaje y plazos de la cohorte."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Curso",
|
||||
"description": "Inscripciones, entregas, lecciones, anuncios y finalizaciones en tus cursos."
|
||||
}
|
||||
},
|
||||
"personal_page_subtitle": "Elige qué notificaciones por correo recibes de ClassroomIO."
|
||||
}
|
||||
},
|
||||
"profileMenu": {
|
||||
|
||||
@@ -3641,71 +3641,73 @@
|
||||
"notifications": {
|
||||
"heading": "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": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Achèvement du cours et certificats",
|
||||
"description": "Envoyer un e-mail aux étudiants lorsqu'ils terminent un cours et obtiennent un certificat."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "E-mails de bienvenue d'inscription",
|
||||
"description": "Envoyer des e-mails de bienvenue lorsque le personnel inscrit des étudiants à des cours ou cohortes."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Rappels et mises à jour des sessions en direct",
|
||||
"description": "Envoyer un e-mail aux étudiants avant les sessions en direct et lors des changements de détails."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Rappels d'objectifs de cohorte",
|
||||
"description": "Envoyer un e-mail aux étudiants concernant les objectifs d'apprentissage à venir ou en retard."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Exercice assigné",
|
||||
"description": "Envoyer un e-mail aux étudiants lorsqu'un exercice est assigné ou rappelé."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Annonces et commentaires du cours",
|
||||
"description": "Envoyer un e-mail aux membres du cours concernant les nouvelles publications et commentaires."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Résultats de notation",
|
||||
"description": "Envoyer un e-mail aux étudiants lorsque leur soumission est notée ou mise à jour."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Nouvelles soumissions à noter",
|
||||
"description": "Envoyer un e-mail aux tuteurs lorsqu'un étudiant soumet un travail à noter."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Nouvel étudiant s'inscrit",
|
||||
"description": "Envoyer un e-mail aux tuteurs lorsqu'un étudiant s'inscrit à un cours."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Paramètres par défaut de l'organisation",
|
||||
"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": "Enregistrer les paramètres par défaut de l'organisation",
|
||||
"save_success": "Paramètres de notification de l'organisation enregistrés"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Vos préférences e-mail",
|
||||
"save": "Enregistrer les préférences personnelles",
|
||||
"save_success": "Préférences de notification personnelles enregistrées",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Inscription",
|
||||
"description": "E-mails de bienvenue envoyés lorsque le personnel ajoute des membres à des cours ou cohortes."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Cohorte",
|
||||
"description": "Rappels concernant les objectifs d'apprentissage et les échéances de la cohorte."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Cours",
|
||||
"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": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "टीमें",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "एआई ट्यूटर",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "सूचनाएँ"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "अकादमी खोलें"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "सूचनाएँ",
|
||||
"page_title": "सूचनाएँ",
|
||||
"page_subtitle": "चुनें कि आप कौन सी ईमेल सूचनाएँ प्राप्त करते हैं और व्यवस्थापक के रूप में आपका संगठन कौन से ईमेल भेजता है।",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "पाठ्यक्रम पूर्णता और प्रमाणपत्र",
|
||||
"description": "जब छात्र पाठ्यक्रम पूरा करें और प्रमाणपत्र प्राप्त करें तो ईमेल भेजें।"
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "नामांकन स्वागत ईमेल",
|
||||
"description": "जब स्टाफ छात्रों को पाठ्यक्रमों या समूहों में नामांकित करे तो स्वागत ईमेल भेजें।"
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "लाइव सत्र अनुस्मारक और अपडेट",
|
||||
"description": "लाइव सत्रों से पहले और सत्र विवरण बदलने पर छात्रों को ईमेल भेजें।"
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "समूह लक्ष्य अनुस्मारक",
|
||||
"description": "आगामी या अतिदेय समूह सीखने के लक्ष्यों के बारे में छात्रों को ईमेल भेजें।"
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "अभ्यास असाइन किया गया",
|
||||
"description": "जब अभ्यास असाइन या याद दिलाया जाए तो छात्रों को ईमेल भेजें।"
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "पाठ्यक्रम घोषणाएँ और टिप्पणियाँ",
|
||||
"description": "न्यूज़फ़ीड पर नई पोस्ट और टिप्पणियों के बारे में पाठ्यक्रम सदस्यों को ईमेल भेजें।"
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "ग्रेडिंग परिणाम",
|
||||
"description": "जब उनका सबमिशन ग्रेड या अपडेट हो तो छात्रों को ईमेल भेजें।"
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "ग्रेडिंग के लिए नए सबमिशन",
|
||||
"description": "जब छात्र ग्रेडिंग की आवश्यकता वाला कार्य जमा करे तो ट्यूटर्स को ईमेल भेजें।"
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "नया छात्र शामिल होता है",
|
||||
"description": "जब कोई छात्र पाठ्यक्रम में नामांकित हो तो ट्यूटर्स को ईमेल भेजें।"
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "संगठन डिफ़ॉल्ट",
|
||||
"description": "यहाँ अक्षम करने पर, आपके संगठन में कोई भी वह ईमेल नहीं प्राप्त करेगा। सदस्य अभी भी उन ईमेल से ऑप्ट आउट कर सकते हैं जिन्हें आप सक्षम रखते हैं।",
|
||||
"save": "संगठन डिफ़ॉल्ट सहेजें",
|
||||
"save_success": "संगठन सूचना डिफ़ॉल्ट सहेजे गए"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "आपकी ईमेल प्राथमिकताएँ",
|
||||
"save": "व्यक्तिगत प्राथमिकताएँ सहेजें",
|
||||
"save_success": "व्यक्तिगत सूचना प्राथमिकताएँ सहेजी गईं",
|
||||
"description": "ये उन ईमेल पर लागू होते हैं जो इस संगठन में आपको भेजे जाते हैं। आपका संगठन फिर भी कुछ ईमेल सभी के लिए बंद कर सकता है।",
|
||||
"description_with_org": "ये उन ईमेल पर लागू होते हैं जो {orgName} में आपको भेजे जाते हैं। आपका संगठन फिर भी कुछ ईमेल सभी के लिए बंद कर सकता है।"
|
||||
},
|
||||
"sections": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "नामांकन",
|
||||
"description": "स्वागत ईमेल जब स्टाफ सदस्यों को पाठ्यक्रमों या समूहों में जोड़ता है।"
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "समूह",
|
||||
"description": "समूह सीखने के लक्ष्यों और समय सीमा के बारे में अनुस्मारक।"
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "पाठ्यक्रम",
|
||||
"description": "आपके पाठ्यक्रमों में नामांकन, सबमिशन, पाठ, घोषणाएँ और पूर्णता।"
|
||||
}
|
||||
},
|
||||
"personal_page_subtitle": "चुनें कि आप ClassroomIO से कौन सी ईमेल सूचनाएँ प्राप्त करते हैं।"
|
||||
}
|
||||
},
|
||||
"profileMenu": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "Zespoły",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "Nauczyciel AI",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Powiadomienia"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Otwarta Akademia"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Powiadomienia",
|
||||
"page_title": "Powiadomienia",
|
||||
"page_subtitle": "Wybierz, które powiadomienia e-mail otrzymujesz, a jako administrator, które e-maile wysyła Twoja organizacja.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Ukończenie kursu i certyfikaty",
|
||||
"description": "Wyślij e-mail do studentów, gdy ukończą kurs i otrzymają certyfikat."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Powitalne e-maile zapisów",
|
||||
"description": "Wysyłaj e-maile powitalne, gdy personel zapisuje studentów na kursy lub kohorty."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Przypomnienia i aktualizacje sesji na żywo",
|
||||
"description": "Wyślij e-mail do studentów przed sesjami na żywo i gdy zmienią się szczegóły sesji."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Przypomnienia o celach kohorty",
|
||||
"description": "Wyślij e-mail do studentów o nadchodzących lub zaległych celach nauki kohorty."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Przypisane ćwiczenie",
|
||||
"description": "Wyślij e-mail do studentów, gdy ćwiczenie zostanie przypisane lub przypomniane."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Ogłoszenia kursu i komentarze",
|
||||
"description": "Wyślij e-mail do członków kursu o nowych postach i komentarzach w aktualnościach."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Wyniki oceniania",
|
||||
"description": "Wyślij e-mail do studentów, gdy ich zgłoszenie zostanie ocenione lub zaktualizowane."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Nowe zgłoszenia do oceny",
|
||||
"description": "Wyślij e-mail do tutorów, gdy student prześle pracę wymagającą oceny."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Nowy student dołącza",
|
||||
"description": "Wyślij e-mail do tutorów, gdy student zapisze się na kurs."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Domyślne ustawienia organizacji",
|
||||
"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": "Zapisz domyślne ustawienia organizacji",
|
||||
"save_success": "Domyślne ustawienia powiadomień organizacji zapisane"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Twoje preferencje e-mail",
|
||||
"save": "Zapisz preferencje osobiste",
|
||||
"save_success": "Osobiste preferencje powiadomień zapisane",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Zapisy",
|
||||
"description": "E-maile powitalne wysyłane, gdy personel dodaje członków do kursów lub kohort."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Kohorta",
|
||||
"description": "Przypomnienia o celach nauki kohorty i terminach."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Kurs",
|
||||
"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": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "Equipes",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "Tutor de IA",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Notificações"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Academia Aberta"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Notificações",
|
||||
"page_title": "Notificações",
|
||||
"page_subtitle": "Escolha quais notificações por e-mail você recebe e, como administrador, quais e-mails sua organização envia.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Conclusão do curso e certificados",
|
||||
"description": "Enviar e-mail aos alunos quando concluírem um curso e receberem um certificado."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "E-mails de boas-vindas de matrícula",
|
||||
"description": "Enviar e-mails de boas-vindas quando a equipe matricular alunos em cursos ou coortes."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Lembretes e atualizações de sessões ao vivo",
|
||||
"description": "Enviar e-mail aos alunos antes de sessões ao vivo e quando os detalhes mudarem."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Lembretes de metas da coorte",
|
||||
"description": "Enviar e-mail aos alunos sobre metas de aprendizagem próximas ou atrasadas."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Exercício atribuído",
|
||||
"description": "Enviar e-mail aos alunos quando um exercício for atribuído ou lembrado."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Anúncios e comentários do curso",
|
||||
"description": "Enviar e-mail aos membros do curso sobre novas publicações e comentários no feed."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Resultados de avaliação",
|
||||
"description": "Enviar e-mail aos alunos quando o envio for avaliado ou atualizado."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Novos envios para avaliar",
|
||||
"description": "Enviar e-mail aos tutores quando um aluno envia trabalho que precisa de avaliação."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Novo aluno se inscreve",
|
||||
"description": "Enviar e-mail aos tutores quando um aluno se matricula em um curso."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Padrões da organização",
|
||||
"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": "Salvar padrões da organização",
|
||||
"save_success": "Padrões de notificação da organização salvos"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Suas preferências de e-mail",
|
||||
"save": "Salvar preferências pessoais",
|
||||
"save_success": "Preferências de notificação pessoais salvas",
|
||||
"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": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Matrícula",
|
||||
"description": "E-mails de boas-vindas enviados quando a equipe adiciona membros a cursos ou coortes."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Coorte",
|
||||
"description": "Lembretes sobre metas de aprendizagem e prazos da coorte."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Curso",
|
||||
"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": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "Команды",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "Репетитор по искусственному интеллекту",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Уведомления"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Открытая Академия"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Уведомления",
|
||||
"page_title": "Уведомления",
|
||||
"page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете, а как администратор — какие письма отправляет ваша организация.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Завершение курса и сертификаты",
|
||||
"description": "Отправлять письмо студентам, когда они завершают курс и получают сертификат."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Приветственные письма при записи",
|
||||
"description": "Отправлять приветственные письма, когда сотрудники записывают студентов на курсы или когорты."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Напоминания и обновления живых сессий",
|
||||
"description": "Отправлять письмо студентам перед живыми сессиями и при изменении деталей сессии."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Напоминания о целях когорты",
|
||||
"description": "Отправлять письмо студентам о предстоящих или просроченных целях обучения когорты."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Назначено упражнение",
|
||||
"description": "Отправлять письмо студентам, когда упражнение назначено или напоминается."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Объявления курса и комментарии",
|
||||
"description": "Отправлять письмо участникам курса о новых публикациях и комментариях в ленте."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Результаты оценки",
|
||||
"description": "Отправлять письмо студентам, когда их работа оценена или обновлена."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Новые работы для оценки",
|
||||
"description": "Отправлять письмо наставникам, когда студент сдаёт работу, требующую оценки."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Новый студент записывается",
|
||||
"description": "Отправлять письмо наставникам, когда студент записывается на курс."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Настройки организации по умолчанию",
|
||||
"description": "Если отключено здесь, никто в вашей организации не получит это письмо. Участники по-прежнему могут отказаться от писем, которые вы оставили включёнными.",
|
||||
"save": "Сохранить настройки организации по умолчанию",
|
||||
"save_success": "Настройки уведомлений организации сохранены"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Ваши настройки электронной почты",
|
||||
"save": "Сохранить личные настройки",
|
||||
"save_success": "Личные настройки уведомлений сохранены",
|
||||
"description": "Они применяются к письмам, которые вам отправляют в этой организации. Организация по-прежнему может отключить некоторые письма для всех.",
|
||||
"description_with_org": "Они применяются к письмам, которые вам отправляют в {orgName}. Организация по-прежнему может отключить некоторые письма для всех."
|
||||
},
|
||||
"sections": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Запись",
|
||||
"description": "Приветственные письма, отправляемые, когда сотрудники добавляют участников в курсы или когорты."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Когорта",
|
||||
"description": "Напоминания о целях обучения когорты и сроках."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Курс",
|
||||
"description": "Записи, сдачи, уроки, объявления и завершения в ваших курсах."
|
||||
}
|
||||
},
|
||||
"personal_page_subtitle": "Выберите, какие уведомления по электронной почте вы получаете от ClassroomIO."
|
||||
}
|
||||
},
|
||||
"profileMenu": {
|
||||
|
||||
@@ -3463,7 +3463,7 @@
|
||||
"teams_tab": "Đội",
|
||||
"ai_credits_tab": "AI Credits",
|
||||
"ai_tutor_tab": "Gia sư AI",
|
||||
"notifications_tab": "Notifications"
|
||||
"notifications_tab": "Thông báo"
|
||||
},
|
||||
"subheadings": {
|
||||
"view_site": "Học viện mở"
|
||||
@@ -3639,73 +3639,75 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications",
|
||||
"page_title": "Notifications",
|
||||
"page_subtitle": "Choose which email notifications you receive and, as an admin, which emails your organization sends.",
|
||||
"heading": "Thông báo",
|
||||
"page_title": "Thông báo",
|
||||
"page_subtitle": "Chọn thông báo email bạn nhận và, với tư cách quản trị viên, email mà tổ chức của bạn gửi.",
|
||||
"toggles": {
|
||||
"courseCompletion": {
|
||||
"label": "Course completion and certificates",
|
||||
"description": "Email students when they complete a course and earn a certificate."
|
||||
"label": "Hoàn thành khóa học và chứng chỉ",
|
||||
"description": "Gửi email cho học viên khi họ hoàn thành khóa học và nhận chứng chỉ."
|
||||
},
|
||||
"enrollmentWelcome": {
|
||||
"label": "Enrollment welcome emails",
|
||||
"description": "Send welcome emails when staff enroll students in courses or cohorts."
|
||||
"label": "Email chào mừng ghi danh",
|
||||
"description": "Gửi email chào mừng khi nhân viên ghi danh học viên vào khóa học hoặc nhóm học."
|
||||
},
|
||||
"session": {
|
||||
"label": "Live session reminders and updates",
|
||||
"description": "Email students before live sessions and when session details change."
|
||||
"label": "Nhắc nhở và cập nhật buổi học trực tiếp",
|
||||
"description": "Gửi email cho học viên trước buổi học trực tiếp và khi chi tiết buổi học thay đổi."
|
||||
},
|
||||
"cohortReminder": {
|
||||
"label": "Cohort goal reminders",
|
||||
"description": "Email students about upcoming or overdue cohort learning goals."
|
||||
"label": "Nhắc nhở mục tiêu nhóm học",
|
||||
"description": "Gửi email cho học viên về mục tiêu học tập sắp tới hoặc quá hạn."
|
||||
},
|
||||
"quizAssigned": {
|
||||
"label": "Exercise assigned",
|
||||
"description": "Email students when an exercise is assigned or nudged."
|
||||
"label": "Bài tập được giao",
|
||||
"description": "Gửi email cho học viên khi bài tập được giao hoặc nhắc nhở."
|
||||
},
|
||||
"newsfeed": {
|
||||
"label": "Course announcements and comments",
|
||||
"description": "Email course members about new posts and comments on the newsfeed."
|
||||
"label": "Thông báo và bình luận khóa học",
|
||||
"description": "Gửi email cho thành viên khóa học về bài đăng và bình luận mới trên bảng tin."
|
||||
},
|
||||
"gradingResult": {
|
||||
"label": "Grading results",
|
||||
"description": "Email students when their submission is graded or updated."
|
||||
"label": "Kết quả chấm điểm",
|
||||
"description": "Gửi email cho học viên khi bài nộp được chấm hoặc cập nhật."
|
||||
},
|
||||
"newSubmission": {
|
||||
"label": "New submissions to grade",
|
||||
"description": "Email tutors when a student submits work that needs grading."
|
||||
"label": "Bài nộp mới cần chấm",
|
||||
"description": "Gửi email cho gia sư khi học viên nộp bài cần chấm điểm."
|
||||
},
|
||||
"newStudent": {
|
||||
"label": "New student joins",
|
||||
"description": "Email tutors when a student enrolls in a course."
|
||||
"label": "Học viên mới tham gia",
|
||||
"description": "Gửi email cho gia sư khi học viên ghi danh vào khóa học."
|
||||
}
|
||||
},
|
||||
"org": {
|
||||
"heading": "Organization defaults",
|
||||
"description": "When disabled here, nobody in your organization receives that email. Members can still opt out of emails you leave enabled.",
|
||||
"save": "Save organization defaults",
|
||||
"save_success": "Organization notification defaults saved"
|
||||
"heading": "Mặc định của tổ chức",
|
||||
"description": "Khi tắt ở đây, không ai trong tổ chức của bạn nhận email đó. Thành viên vẫn có thể từ chối các email bạn để bật.",
|
||||
"save": "Lưu mặc định của tổ chức",
|
||||
"save_success": "Đã lưu mặc định thông báo của tổ chức"
|
||||
},
|
||||
"personal": {
|
||||
"heading": "Your email preferences",
|
||||
"description": "These apply to emails sent to you across every organization you belong to. Your organization may still disable certain emails for everyone.",
|
||||
"save": "Save personal preferences",
|
||||
"save_success": "Personal notification preferences saved"
|
||||
"heading": "Tùy chọn email của bạn",
|
||||
"save": "Lưu tùy chọn cá nhân",
|
||||
"save_success": "Đã lưu tùy chọn thông báo cá nhân",
|
||||
"description": "Áp dụng cho email gửi đến bạn trong tổ chức này. Tổ chức của bạn vẫn có thể tắt một số email cho mọi người.",
|
||||
"description_with_org": "Áp dụng cho email gửi đến bạn trong {orgName}. Tổ chức của bạn vẫn có thể tắt một số email cho mọi người."
|
||||
},
|
||||
"sections": {
|
||||
"enrollment": {
|
||||
"heading": "Enrollment",
|
||||
"description": "Welcome emails sent when staff add members to courses or cohorts."
|
||||
"heading": "Ghi danh",
|
||||
"description": "Email chào mừng được gửi khi nhân viên thêm thành viên vào khóa học hoặc nhóm học."
|
||||
},
|
||||
"cohort": {
|
||||
"heading": "Cohort",
|
||||
"description": "Reminders about cohort learning goals and deadlines."
|
||||
"heading": "Nhóm học",
|
||||
"description": "Nhắc nhở về mục tiêu học tập và hạn chót của nhóm học."
|
||||
},
|
||||
"course": {
|
||||
"heading": "Course",
|
||||
"description": "Enrollments, submissions, lessons, announcements, and completions in your courses."
|
||||
}
|
||||
"heading": "Khóa học",
|
||||
"description": "Ghi danh, bài nộp, bài học, thông báo và hoàn thành trong các khóa học của bạn."
|
||||
}
|
||||
},
|
||||
"personal_page_subtitle": "Chọn thông báo email bạn nhận từ ClassroomIO."
|
||||
}
|
||||
},
|
||||
"profileMenu": {
|
||||
|
||||
@@ -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: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-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:notify-students-over-limit": "tsx src/scripts/notify-students-over-limit.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,
|
||||
"tag": "0003_profile_settings",
|
||||
"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
|
||||
} from '@cio/utils/notifications';
|
||||
|
||||
import { getProfileByEmail, getProfileById } from '../auth/profile';
|
||||
import { db } from '@db/drizzle';
|
||||
import * as schema from '@db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getOrganizationEmailNotificationSettings } from './email-preferences-org';
|
||||
import { getPersonalEmailNotificationSettingsForOrg } from './organizationmember-email-notifications';
|
||||
import { getProfileByEmail } from '../auth/profile';
|
||||
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: {
|
||||
emailId: EmailId;
|
||||
organizationId?: string;
|
||||
@@ -67,7 +23,7 @@ export async function shouldSendEmail(input: {
|
||||
*/
|
||||
export class EmailPreferenceLookupCache {
|
||||
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>();
|
||||
|
||||
async shouldSend(input: {
|
||||
@@ -84,7 +40,8 @@ export class EmailPreferenceLookupCache {
|
||||
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);
|
||||
}
|
||||
@@ -107,12 +64,17 @@ export class EmailPreferenceLookupCache {
|
||||
return this.profileIdByEmail.get(email);
|
||||
}
|
||||
|
||||
private async getProfileSettings(profileId: string): Promise<PersonalEmailNotificationSettings | undefined> {
|
||||
if (!this.profileSettingsById.has(profileId)) {
|
||||
const settings = await getProfileEmailNotificationSettingsById(profileId);
|
||||
this.profileSettingsById.set(profileId, settings);
|
||||
private async getPersonalSettings(
|
||||
profileId: string,
|
||||
organizationId: string
|
||||
): 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-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' }),
|
||||
locale: locale().default('en'),
|
||||
isRestricted: boolean('is_restricted').default(false).notNull(),
|
||||
settings: jsonb().default({}).$type<{
|
||||
emailNotifications?: {
|
||||
newStudent?: boolean;
|
||||
newSubmission?: boolean;
|
||||
gradingResult?: boolean;
|
||||
newsfeed?: boolean;
|
||||
quizAssigned?: boolean;
|
||||
cohortReminder?: boolean;
|
||||
session?: boolean;
|
||||
courseCompletion?: boolean;
|
||||
};
|
||||
}>()
|
||||
settings: jsonb().default({}).$type<Record<string, unknown>>()
|
||||
},
|
||||
(table) => [
|
||||
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(
|
||||
'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 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 TNewQuestion = typeof schema.question.$inferInsert;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
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';
|
||||
|
||||
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({
|
||||
fullname: 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(),
|
||||
avatarUrl: z.url().optional(),
|
||||
emailNotifications: z.object(ZEmailNotificationToggleFields).optional()
|
||||
avatarUrl: z.url().optional()
|
||||
});
|
||||
|
||||
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