292 lines
7.9 KiB
TypeScript
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,
|
|
},
|
|
});
|