98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { CheckboxField, useToast } from "@nexarch/shl";
|
|
import {
|
|
ApiError,
|
|
KNOWN_CHANNELS,
|
|
KNOWN_EVENT_TYPES,
|
|
Preference,
|
|
fetchMyPreferences,
|
|
setPreference,
|
|
} from "../../lib/api";
|
|
|
|
const EVENT_LABELS: Record<string, string> = {
|
|
welcome: "Willkommen",
|
|
invoice_ready: "Rechnung verfügbar",
|
|
password_reset: "Passwort-Zurücksetzung",
|
|
security_alert: "Sicherheitshinweis",
|
|
};
|
|
|
|
const CHANNEL_LABELS: Record<string, string> = {
|
|
email: "E-Mail",
|
|
in_app: "In-App",
|
|
};
|
|
|
|
export default function SettingsPage() {
|
|
const { push } = useToast();
|
|
const [prefs, setPrefs] = useState<Preference[] | null>(null);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
const [pending, setPending] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchMyPreferences()
|
|
.then(setPrefs)
|
|
.catch(() => setLoadError("Einstellungen konnten nicht geladen werden. Bitte melden Sie sich an."));
|
|
}, []);
|
|
|
|
// Default: aktiviert, solange keine explizite Praeferenz existiert
|
|
// (Akzeptanzkriterium 1) — gleicher Opt-out-Default wie im Backend.
|
|
function isEnabled(eventType: string, channel: string): boolean {
|
|
const explicit = prefs?.find((p) => p.EventType === eventType && p.Channel === channel);
|
|
return explicit ? explicit.Enabled : true;
|
|
}
|
|
|
|
async function onToggle(eventType: string, channel: string, nextEnabled: boolean) {
|
|
const key = `${eventType}:${channel}`;
|
|
setPending(key);
|
|
try {
|
|
// Akzeptanzkriterium 2: sofort speichern, kein Sammel-Speichern-Button —
|
|
// wirkt unmittelbar auf künftige Zustellungen (EnqueueIfAllowed prüft
|
|
// bei jedem Aufruf live, kein Cache dazwischen).
|
|
await setPreference(eventType, channel, nextEnabled);
|
|
setPrefs((current) => {
|
|
const withoutThis = (current ?? []).filter((p) => !(p.EventType === eventType && p.Channel === channel));
|
|
return [...withoutThis, { TenantSlug: "", UserID: "", EventType: eventType, Channel: channel, Enabled: nextEnabled }];
|
|
});
|
|
push(nextEnabled ? "Benachrichtigung aktiviert." : "Benachrichtigung deaktiviert.", "success");
|
|
} catch (err) {
|
|
push(err instanceof ApiError ? err.message : "Einstellung konnte nicht gespeichert werden.", "danger");
|
|
} finally {
|
|
setPending(null);
|
|
}
|
|
}
|
|
|
|
if (loadError) {
|
|
return (
|
|
<main style={{ maxWidth: 640, margin: "40px auto", padding: "0 16px" }}>
|
|
<p role="alert">{loadError}</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<main style={{ maxWidth: 640, margin: "40px auto", padding: "0 16px" }}>
|
|
<h1>Benachrichtigungseinstellungen</h1>
|
|
<p>Wählen Sie je Ereignis, über welche Kanäle Sie benachrichtigt werden möchten.</p>
|
|
|
|
{KNOWN_EVENT_TYPES.map((eventType) => (
|
|
<fieldset key={eventType} style={{ marginBottom: 16 }}>
|
|
<legend>{EVENT_LABELS[eventType] ?? eventType}</legend>
|
|
{KNOWN_CHANNELS.map((channel) => {
|
|
const key = `${eventType}:${channel}`;
|
|
return (
|
|
<CheckboxField
|
|
key={key}
|
|
label={CHANNEL_LABELS[channel] ?? channel}
|
|
checked={isEnabled(eventType, channel)}
|
|
disabled={pending === key}
|
|
onChange={(e) => onToggle(eventType, channel, e.target.checked)}
|
|
/>
|
|
);
|
|
})}
|
|
</fieldset>
|
|
))}
|
|
</main>
|
|
);
|
|
}
|