Files
FEDEO/mobile/app/more/settings.tsx

292 lines
7.9 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { router } from 'expo-router';
import { DEFAULT_API_BASE_URL } from '@/src/config/env';
import { sendMobileTestPush } from '@/src/lib/api';
import { registerDeviceForPush } from '@/src/lib/push-registration';
import {
getApiBaseUrlSync,
hydrateApiBaseUrl,
resetApiBaseUrl,
serverStorageInfo,
setApiBaseUrl,
} from '@/src/lib/server-config';
import { useAuth } from '@/src/providers/auth-provider';
const PRIMARY = '#69c350';
function isValidServerUrl(value: string): boolean {
return /^https?:\/\/.+/i.test(value.trim());
}
export default function SettingsScreen() {
const { logout, token } = useAuth();
const [serverUrl, setServerUrlInput] = useState(getApiBaseUrlSync());
const [savedUrl, setSavedUrl] = useState(getApiBaseUrlSync());
const [submitting, setSubmitting] = useState(false);
const [pushSubmitting, setPushSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const loadConfig = useCallback(async () => {
const current = await hydrateApiBaseUrl();
setServerUrlInput(current);
setSavedUrl(current);
}, []);
useEffect(() => {
void loadConfig();
}, [loadConfig]);
async function onSave() {
setError(null);
setSuccess(null);
if (!isValidServerUrl(serverUrl)) {
setError('Bitte eine gültige URL mit http:// oder https:// eingeben.');
return;
}
setSubmitting(true);
try {
const normalized = await setApiBaseUrl(serverUrl);
setServerUrlInput(normalized);
setSavedUrl(normalized);
setSuccess('Server-Instanz gespeichert.');
if (token) {
await logout();
router.replace('/login');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Server-Instanz konnte nicht gespeichert werden.');
} finally {
setSubmitting(false);
}
}
async function onResetToDefault() {
setError(null);
setSuccess(null);
setSubmitting(true);
try {
const fallback = await resetApiBaseUrl();
setServerUrlInput(fallback);
setSavedUrl(fallback);
setSuccess('Auf Standard-Server zurückgesetzt.');
if (token) {
await logout();
router.replace('/login');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Zuruecksetzen fehlgeschlagen.');
} finally {
setSubmitting(false);
}
}
async function onRegisterPush() {
setError(null);
setSuccess(null);
if (!token) {
setError('Bitte zuerst anmelden, um dieses Gerät für Push zu registrieren.');
return;
}
setPushSubmitting(true);
try {
const result = await registerDeviceForPush(token);
setSuccess(`Push registriert: ${result.centralDeviceId || result.id || 'aktiv'}`);
} catch (err) {
setError(err instanceof Error ? err.message : 'Push-Registrierung fehlgeschlagen.');
} finally {
setPushSubmitting(false);
}
}
async function onSendTestPush() {
setError(null);
setSuccess(null);
if (!token) {
setError('Bitte zuerst anmelden, um eine Testnachricht zu senden.');
return;
}
setPushSubmitting(true);
try {
const result = await sendMobileTestPush(token);
setSuccess(`Testnachricht angefordert: ${result.accepted} akzeptiert, ${result.rejected} abgelehnt.`);
} catch (err) {
setError(err instanceof Error ? err.message : 'Testnachricht konnte nicht gesendet werden.');
} finally {
setPushSubmitting(false);
}
}
const isDirty = serverUrl.trim() !== savedUrl;
return (
<ScrollView contentContainerStyle={styles.container}>
<View style={styles.card}>
<Text style={styles.title}>Server-Instanz</Text>
<Text style={styles.hint}>
Hinterlege hier die URL deiner eigenen FEDEO-Server-Instanz. Nach dem Speichern wird die Session
neu gestartet.
</Text>
<Text style={styles.label}>Server URL</Text>
<TextInput
value={serverUrl}
onChangeText={setServerUrlInput}
autoCapitalize="none"
autoCorrect={false}
keyboardType="url"
placeholder="https://dein-server.tld"
placeholderTextColor="#9ca3af"
style={styles.input}
/>
<Text style={styles.meta}>Aktiv: {savedUrl}</Text>
<Text style={styles.meta}>Standard: {DEFAULT_API_BASE_URL}</Text>
<Text style={styles.meta}>Storage: {serverStorageInfo.mode}</Text>
{error ? <Text style={styles.error}>{error}</Text> : null}
{success ? <Text style={styles.success}>{success}</Text> : null}
<View style={styles.actions}>
<Pressable
style={[styles.saveButton, (!isDirty || submitting) ? styles.buttonDisabled : null]}
onPress={onSave}
disabled={!isDirty || submitting}>
<Text style={styles.saveButtonText}>{submitting ? 'Speichern...' : 'Speichern'}</Text>
</Pressable>
<Pressable
style={[styles.resetButton, submitting ? styles.buttonDisabled : null]}
onPress={onResetToDefault}
disabled={submitting}>
<Text style={styles.resetButtonText}>Auf Standard zurücksetzen</Text>
</Pressable>
</View>
</View>
<View style={styles.card}>
<Text style={styles.title}>Mobile Push</Text>
<Text style={styles.hint}>
Registriert dieses Gerät bei deiner FEDEO-Instanz und leitet den nativen Push-Token an den zentralen
Push-Server weiter.
</Text>
<View style={styles.actions}>
<Pressable
style={[styles.saveButton, (!token || pushSubmitting) ? styles.buttonDisabled : null]}
onPress={onRegisterPush}
disabled={!token || pushSubmitting}>
<Text style={styles.saveButtonText}>{pushSubmitting ? 'Bitte warten...' : 'Gerät registrieren'}</Text>
</Pressable>
<Pressable
style={[styles.resetButton, (!token || pushSubmitting) ? styles.buttonDisabled : null]}
onPress={onSendTestPush}
disabled={!token || pushSubmitting}>
<Text style={styles.resetButtonText}>Testnachricht senden</Text>
</Pressable>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
backgroundColor: '#f9fafb',
padding: 16,
gap: 12,
},
card: {
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 12,
padding: 14,
gap: 10,
},
title: {
color: '#111827',
fontSize: 18,
fontWeight: '700',
},
hint: {
color: '#6b7280',
fontSize: 13,
lineHeight: 18,
},
label: {
color: '#374151',
fontSize: 13,
fontWeight: '600',
marginTop: 2,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
color: '#111827',
backgroundColor: '#ffffff',
},
meta: {
color: '#6b7280',
fontSize: 12,
},
actions: {
gap: 8,
marginTop: 6,
},
saveButton: {
minHeight: 42,
backgroundColor: PRIMARY,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
saveButtonText: {
color: '#ffffff',
fontSize: 15,
fontWeight: '700',
},
resetButton: {
minHeight: 42,
borderRadius: 10,
borderWidth: 1,
borderColor: '#d1d5db',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#ffffff',
},
resetButtonText: {
color: '#374151',
fontSize: 14,
fontWeight: '600',
},
buttonDisabled: {
opacity: 0.6,
},
error: {
color: '#dc2626',
fontSize: 13,
},
success: {
color: '#166534',
fontSize: 13,
},
});