feat(broadcasts): add admin broadcast page and user opt-out switch

Admin page /admin/broadcasts to write, test and send broadcasts in both
languages; confirms the recipient count before send. History shows the
counts. A new switch in settings lets users opt out of platform mails;
account mails always come. Subject and body are plain text only.
This commit is contained in:
lobo committed 2026-10-01 14:12:34 +02:00
1 parent 0154202ddc
commit fa0857f55d
13 files changed
+642 -4

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import {apiFetch} from './api.js';
const AUTH = () => import.meta.env.VITE_API_URL;
const JSON_HEADERS = {'Content-Type': 'application/json'};
/**
* PLATFORM_ADMIN only (403 otherwise): {results: [{id, subjectEn, subjectDe, bodyEn, bodyDe, createdBy, createdAt,
* recipientCount, sentCount, failedCount}]}, newest first.
*/
export const fetchBroadcasts = () => apiFetch(`${AUTH()}/api/admin/broadcasts`);
/**
* {count}: the active users with platform mails on, among `userIds`; all of them when `userIds` is null.
*/
export const fetchRecipientCount = (userIds) =>
apiFetch(`${AUTH()}/api/admin/broadcasts/recipient-count${userIds ? `?userIds=${userIds.join(',')}` : ''}`);
/**
* `broadcast` is {subjectEn, subjectDe, bodyEn, bodyDe, userIds}. 201 {recipientCount}, 409 NO_RECIPIENTS, or 400 for a
* blank or too long field.
*/
export const sendBroadcast = (broadcast) =>
apiFetch(`${AUTH()}/api/admin/broadcasts`, {method: 'POST', headers: JSON_HEADERS, body: JSON.stringify(broadcast)});
/**
* 204: the draft goes to the admin's own address only, in the admin's language.
*/
export const sendTestBroadcast = (broadcast) =>
apiFetch(`${AUTH()}/api/admin/broadcasts/test`,
{method: 'POST', headers: JSON_HEADERS, body: JSON.stringify(broadcast)});
+3
View File
@@ -33,3 +33,6 @@ export const MAX_HINT_LENGTH = 500;
// Auth NewsRequest @Size(max = 100) and @Size(max = 2000)
export const MAX_NEWS_TITLE_LENGTH = 100;
export const MAX_NEWS_TEXT_LENGTH = 2000;
// Auth BroadcastRequest @Size(max = 150) and @Size(max = 5000)
export const MAX_BROADCAST_SUBJECT_LENGTH = 150;
export const MAX_BROADCAST_BODY_LENGTH = 5000;
+8
View File
@@ -58,3 +58,11 @@ export const markWelcomeSeen = () =>
method: 'PUT',
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
/** PUT on Auth, 204: turns the broadcast mails on or off. The caller handles the response status. */
export const savePlatformMails = (enabled) =>
apiFetch(`${import.meta.env.VITE_API_URL}/api/users/settings/platform-mails`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({enabled}),
});