feat(notifications): add per-tenant notification preferences

This commit is contained in:
2026-10-03 14:50:38 +02:00
parent efa6ea90a2
commit 4c67ddd908
12 changed files with 322 additions and 22 deletions

View File

@@ -15,6 +15,8 @@ import {
} from '@/src/lib/server-config';
import { useAuth } from '@/src/providers/auth-provider';
import { NotificationPreferences } from '@/src/components/NotificationPreferences';
const PRIMARY = '#69c350';
function isValidServerUrl(value: string): boolean {
@@ -153,6 +155,7 @@ export default function SettingsScreen() {
return (
<ScrollView contentContainerStyle={styles.container}>
{token && <NotificationPreferences key={String(activeTenantId)} token={token} />}
<View style={styles.card}>
<Text style={styles.title}>Server-Instanz</Text>
<Text style={styles.hint}>

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';
import { Pressable, Switch, Text, View } from 'react-native';
import { fetchNotificationPreferences, saveNotificationPreferences, type NotificationPreferenceEvent } from '@/src/lib/api';
export function NotificationPreferences({ token }: { token: string }) {
const [events, setEvents] = useState<NotificationPreferenceEvent[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
useEffect(() => {
let active = true;
setLoading(true);
fetchNotificationPreferences(token).then(result => { if (active) setEvents(result.events); })
.catch(error => { if (active) setMessage(error.message); })
.finally(() => { if (active) setLoading(false); });
return () => { active = false; };
}, [token]);
async function save() {
setSaving(true);
setMessage('');
try { await saveNotificationPreferences(token, events); setMessage('Einstellungen gespeichert.'); }
catch (error) { setMessage(error instanceof Error ? error.message : 'Speichern fehlgeschlagen.'); }
finally { setSaving(false); }
}
return <View style={{ gap: 12, padding: 16, backgroundColor: '#fff', borderRadius: 12 }}>
<Text style={{ fontSize: 18, fontWeight: '600' }}>Meine Benachrichtigungen</Text>
<Text>Gilt nur für dein Profil im aktuell ausgewählten Mandanten. E-Mail geht an deine Konto-Adresse, Push an deine registrierten Geräte.</Text>
{loading && <Text>Einstellungen werden geladen …</Text>}
{!!message && <Text accessibilityRole="alert">{message}</Text>}
{!loading && events.map(event => <View key={event.eventKey} style={{ gap: 4 }}>
<Text style={{ fontWeight: '600' }}>{event.displayName}</Text>
<Text>{event.description}</Text>
{(['email', 'push'] as const).filter(channel => channel in event.channels).map(channel => <View key={channel} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text>{channel === 'email' ? 'E-Mail' : 'Push'}</Text>
<Switch accessibilityLabel={`${event.displayName}: ${channel}`} disabled={saving} value={event.channels[channel] === true}
onValueChange={enabled => setEvents(rows => rows.map(row => row.eventKey === event.eventKey ? { ...row, channels: { ...row.channels, [channel]: enabled } } : row))} />
</View>)}
</View>)}
{!loading && events.length > 0 && <Pressable accessibilityRole="button" disabled={saving} onPress={save} style={{ padding: 12, backgroundColor: '#69c350', borderRadius: 8 }}>
<Text>{saving ? 'Speichert …' : 'Speichern'}</Text>
</Pressable>}
</View>;
}

View File

@@ -1301,3 +1301,17 @@ export async function uploadPlantFile(
return parsed as ProjectFile;
}
export type NotificationPreferenceEvent = {
eventKey: string;
displayName: string;
description: string | null;
channels: Partial<Record<'email' | 'push', boolean>>;
};
export async function fetchNotificationPreferences(token: string) {
return apiRequest<{ tenantId: number; events: NotificationPreferenceEvent[] }>('/api/notifications/preferences', { token });
}
export async function saveNotificationPreferences(token: string, events: NotificationPreferenceEvent[]) {
return apiRequest('/api/notifications/preferences', { token, method: 'PUT', body: events.flatMap(event =>
Object.entries(event.channels).map(([channel, enabled]) => ({ eventType: event.eventKey, channel, enabled }))) });
}