/** * Settings Page — User-level settings with tabs. * Route: /settings */ import React, { useState, useCallback, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useLanguage } from '../providers/language/LanguageContext'; import { useCurrentUser, useUser } from '../hooks/useUsers'; import { setUserDataCache, getUserDataCache } from '../utils/userCache'; import { FormGeneratorForm } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm'; import type { AttributeDefinition } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm'; import { useApiRequest } from '../hooks/useApi'; import styles from './Settings.module.css'; // ============================================================================= // TYPES // ============================================================================= type SettingsTab = 'profile' | 'appearance' | 'voice' | 'neutralization' | 'privacy'; function _getTabs(t: (key: string) => string): { key: SettingsTab; label: string }[] { return [ { key: 'profile', label: t('settings.tabProfil') }, { key: 'appearance', label: t('settings.tabDarstellung') }, { key: 'voice', label: t('settings.tabStimmeSprache') }, { key: 'neutralization', label: t('settings.tabNeutralisierung') }, { key: 'privacy', label: t('settings.tabDatenschutz') }, ]; } // ============================================================================= // PROFILE EDIT MODAL // ============================================================================= interface ProfileEditModalProps { isOpen: boolean; onClose: () => void; userData: any; onSave: (data: any) => Promise; } const ProfileEditModal: React.FC = ({ isOpen, onClose, userData, onSave }) => { const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const { t, availableLanguages } = useLanguage(); const languageOptions = availableLanguages.map((l) => ({ value: l.code, label: l.label || l.code })); const profileAttributes: AttributeDefinition[] = [ { name: 'fullName', type: 'string', label: t('settings.vollstaendigerName'), description: t('settings.ihrVollstaendigerName'), required: false, placeholder: t('settings.placeholderName') }, { name: 'email', type: 'email', label: t('settings.emailAdresse'), description: t('settings.emailBeschreibung'), required: true, placeholder: t('settings.placeholderEmail') }, { name: 'language', type: 'select', label: t('settings.sprache'), description: t('settings.anzeigespracheDerAnwendung'), required: true, options: languageOptions }, ]; const handleSubmit = async (formData: any) => { setIsSaving(true); setError(null); try { await onSave(formData); onClose(); } catch (err: any) { setError(err.message || 'Fehler beim Speichern des Profils'); } finally { setIsSaving(false); } }; if (!isOpen) return null; return (
e.stopPropagation()}>

{t('settings.profilBearbeiten')}

{error &&
{error}
}
); }; // ============================================================================= // VOICE SETTINGS TAB // ============================================================================= interface VoiceMapEntry { language: string; voiceName: string; } const VoiceSettingsTab: React.FC = () => { const { t } = useLanguage(); const { request } = useApiRequest(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(null); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [sttLanguage, setSttLanguage] = useState('de-DE'); const [languages, setLanguages] = useState([]); const [voiceMap, setVoiceMap] = useState([]); const [addLanguage, setAddLanguage] = useState('de-DE'); const [addVoices, setAddVoices] = useState([]); const [addVoiceName, setAddVoiceName] = useState(''); const [loadingVoices, setLoadingVoices] = useState(false); const _loadSettings = useCallback(async () => { setLoading(true); try { const [prefsData, languagesData] = await Promise.all([ request({ url: '/api/voice/preferences', method: 'get' }), request({ url: '/api/voice/languages', method: 'get' }), ]); const langList = (languagesData as any)?.languages || []; setLanguages(langList); const prefs = prefsData as any; setSttLanguage(prefs?.sttLanguage || 'de-DE'); const map: Record = prefs?.ttsVoiceMap || {}; const entries: VoiceMapEntry[] = Object.entries(map).map(([lang, cfg]) => ({ language: lang, voiceName: typeof cfg === 'string' ? cfg : (cfg as any)?.voiceName || '', })); setVoiceMap(entries); } catch (err: any) { setError(err.message || 'Fehler beim Laden der Voice-Einstellungen'); } finally { setLoading(false); } }, [request]); useEffect(() => { _loadSettings(); }, [_loadSettings]); const _loadVoicesForLanguage = useCallback(async (lang: string) => { setLoadingVoices(true); try { const result = await request({ url: '/api/voice/voices', method: 'get', params: { language: lang } }); setAddVoices((result as any)?.voices || []); setAddVoiceName(''); } catch { setAddVoices([]); } finally { setLoadingVoices(false); } }, [request]); useEffect(() => { _loadVoicesForLanguage(addLanguage); }, [addLanguage, _loadVoicesForLanguage]); const _handleAddEntry = useCallback(() => { if (!addLanguage) return; const exists = voiceMap.some(e => e.language === addLanguage); if (exists) { setVoiceMap(prev => prev.map(e => e.language === addLanguage ? { ...e, voiceName: addVoiceName } : e)); } else { setVoiceMap(prev => [...prev, { language: addLanguage, voiceName: addVoiceName }]); } setAddVoiceName(''); }, [addLanguage, addVoiceName, voiceMap]); const _handleRemoveEntry = useCallback((lang: string) => { setVoiceMap(prev => prev.filter(e => e.language !== lang)); }, []); const _handleSave = useCallback(async () => { setSaving(true); setError(null); setSuccess(null); try { const mapObj: Record = {}; voiceMap.forEach(e => { mapObj[e.language] = { voiceName: e.voiceName || '' }; }); await request({ url: '/api/voice/preferences', method: 'put', data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj }, }); setSuccess(t('settings.einstellungenGespeichert')); setTimeout(() => setSuccess(null), 3000); await _loadSettings(); } catch (err: any) { setError(err.message || 'Fehler beim Speichern'); } finally { setSaving(false); } }, [request, voiceMap, sttLanguage, _loadSettings]); const _handleTestVoice = useCallback(async (lang: string, voice: string) => { setTesting(lang); try { const result: any = await request({ url: '/api/voice/test', method: 'post', data: { language: lang, voiceId: voice || undefined }, }); if (result?.success && result?.audio) { const audio = new Audio(`data:audio/mp3;base64,${result.audio}`); audio.play(); } } catch { setError(t('settings.stimmtestFehlgeschlagen')); } finally { setTesting(null); } }, [request]); const _getLanguageName = useCallback((code: string) => { const found = languages.find((l: any) => (l.code || l) === code); return found?.name || found?.code || code; }, [languages]); const _defaultLangs = [ { code: 'de-DE', name: 'Deutsch' }, { code: 'en-US', name: 'English (US)' }, { code: 'fr-FR', name: 'Francais' }, { code: 'it-IT', name: 'Italiano' }, { code: 'es-ES', name: 'Espanol' }, ]; const _displayLanguages = languages.length > 0 ? languages : _defaultLangs; if (loading) return
{t('settings.einstellungenWerdenGeladen')}
; return ( <> {error &&
{error}
} {success &&
{success}
}

{t('settings.sttspracheSpracheingabe')}

{t('settings.wirdFuerDieSprachezutexterkennungVerwendet')}

{t('settings.ttsstimmenSprachausgabe')}

Die Sprache wird automatisch erkannt. Hier kann pro Sprache eine bevorzugte Stimme festgelegt werden.

{voiceMap.length === 0 ? (
Keine Stimmen konfiguriert. Die Standardstimme wird fuer alle Sprachen verwendet.
) : ( {voiceMap.map(entry => ( ))}
{t('settings.sprache')}{t('settings.stimme')}
{_getLanguageName(entry.language)} {entry.voiceName || 'Standard'}
)}
); }; // ============================================================================= // NEUTRALIZATION MAPPINGS TAB // ============================================================================= interface NeutralizationMapping { id: string; originalText: string; patternType: string; fileId?: string; featureInstanceId?: string; } const NeutralizationMappingsTab: React.FC = () => { const { t } = useLanguage(); const { request } = useApiRequest(); const [mappings, setMappings] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const _load = useCallback(async () => { setLoading(true); setError(null); try { const result: any = await request({ url: '/api/local/neutralization-mappings', method: 'get' }); const items = (result?.mappings || []).map((m: any) => ({ id: m.id, originalText: m.originalText || '', patternType: m.patternType || '', fileId: m.fileId, featureInstanceId: m.featureInstanceId, })); setMappings(items); } catch (err: any) { setError(err.message || 'Fehler beim Laden'); } finally { setLoading(false); } }, [request]); useEffect(() => { _load(); }, [_load]); const _handleDelete = useCallback(async (id: string) => { try { await request({ url: `/api/local/neutralization-mappings/${id}`, method: 'delete' }); setMappings(prev => prev.filter(m => m.id !== id)); } catch (err: any) { setError(err.message || 'Fehler beim Loeschen'); } }, [request]); const _maskText = (text: string) => { if (text.length <= 4) return '****'; return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2); }; if (loading) return
{t('settings.mappingsWerdenGeladen')}
; return ( <> {error &&
{error}
}

{t('settings.platzhaltermappingsLokal')}

AI-Workspace: Neutralisierter Chat-Text, Dokumente und Platzhalter-Mappings finden Sie unter{' '} {t('settings.mandantAiworkspaceinstanzEinstellungenTabNeutralisierung')} (nicht auf dieser Seite). Dieser Tab zeigt nur die lokale Liste über /api/local/neutralization-mappings.

Bei der Datenneutralisierung werden personenbezogene Daten durch Platzhalter ersetzt, bevor Text an KI-Modelle geht; die Antwort wird anschliessend wieder mit Ihren Originalbegriffen angereichert (zentrale Pipeline ueber den AI-Service). Die Tabelle unten betrifft nur lokale Entwickler-/Test-Mappings — hier einsehbar und loeschbar.

{mappings.length === 0 ? (
Keine Neutralisierungs-Mappings vorhanden.
) : ( {mappings.map(m => ( ))}
Platzhalter-ID Originaltext Typ
{m.id.slice(0, 12)}... {_maskText(m.originalText)} {m.patternType}
)}
); }; // ============================================================================= // SETTINGS PAGE // ============================================================================= export const SettingsPage: React.FC = () => { const { t, currentLanguage, setLanguage, availableLanguages, refreshAvailableLanguages } = useLanguage(); const { user: currentUser, refetch: refetchUser } = useCurrentUser(); const { updateUser } = useUser(); const [activeTab, setActiveTab] = useState('profile'); const [theme, setTheme] = useState<'light' | 'dark'>(() => (localStorage.getItem('theme') as 'light' | 'dark') || 'light'); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const [isSavingLanguage, setIsSavingLanguage] = useState(false); const [languageError, setLanguageError] = useState(null); useEffect(() => { if (availableLanguages.length === 0) { refreshAvailableLanguages(); } }, [availableLanguages.length, refreshAvailableLanguages]); const handleThemeChange = (newTheme: 'light' | 'dark') => { setTheme(newTheme); localStorage.setItem('theme', newTheme); if (newTheme === 'dark') { document.documentElement.classList.add('dark-theme'); document.documentElement.classList.remove('light-theme'); } else { document.documentElement.classList.add('light-theme'); document.documentElement.classList.remove('dark-theme'); } document.documentElement.setAttribute('data-theme', newTheme); }; const handleLanguageChange = useCallback(async (newLanguage: string) => { if (!currentUser?.id || !currentUser?.username) return; setIsSavingLanguage(true); setLanguageError(null); try { await updateUser(currentUser.id, { id: currentUser.id, username: currentUser.username, email: currentUser.email, fullName: currentUser.fullName, language: newLanguage, enabled: currentUser.enabled ?? true, authenticationAuthority: currentUser.authenticationAuthority || 'local' }); const cachedUser = getUserDataCache(); if (cachedUser) setUserDataCache({ ...cachedUser, language: newLanguage }); setLanguage(newLanguage); window.dispatchEvent(new CustomEvent('userInfoUpdated')); } catch { setLanguageError('Sprache konnte nicht gespeichert werden'); } finally { setIsSavingLanguage(false); } }, [currentUser, updateUser, setLanguage]); const handleProfileSave = useCallback(async (formData: any) => { if (!currentUser?.id || !currentUser?.username) throw new Error('Nicht angemeldet'); const newLanguage = formData.language || currentUser.language || 'de'; const updatedUser = await updateUser(currentUser.id, { id: currentUser.id, username: currentUser.username, email: formData.email || currentUser.email, fullName: formData.fullName || currentUser.fullName, language: newLanguage, enabled: currentUser.enabled ?? true, authenticationAuthority: currentUser.authenticationAuthority || 'local' }); const cachedUser = getUserDataCache(); if (cachedUser) setUserDataCache({ ...cachedUser, fullName: updatedUser.fullName || cachedUser.fullName, email: updatedUser.email || cachedUser.email, language: newLanguage }); if (newLanguage !== currentLanguage) setLanguage(newLanguage); if (refetchUser) await refetchUser(); window.dispatchEvent(new CustomEvent('userInfoUpdated')); }, [currentUser, updateUser, refetchUser, currentLanguage, setLanguage]); return (

{t('settings.einstellungen')}

{t('settings.persoenlicheEinstellungenUndPraeferenzen')}

{activeTab === 'profile' && ( <>

{t('settings.konto')}

{t('settings.aendernSieIhrenNamenUnd')}

{currentUser && (
Benutzername{currentUser.username}
Name{currentUser.fullName || '-'}
E-Mail{currentUser.email || '-'}
)}

Ueber

Version2.0.0
Build2026.03.23
)} {activeTab === 'appearance' && (

{t('settings.darstellung')}

{t('settings.waehlenSieZwischenHellemUnd')}

{t('settings.spracheBeschreibung')}{languageError && {languageError}}

{isSavingLanguage && {t('settings.speichern')}}
)} {activeTab === 'voice' && } {activeTab === 'neutralization' && } {activeTab === 'privacy' && (

{t('settings.datenschutz')}

{t('settings.datenschutzBeschreibung')}

{t('settings.datenexportPortabilitaetUndKontoloeschung')}

{t('settings.gdprOeffnen')}
)}
setIsProfileModalOpen(false)} userData={currentUser} onSave={handleProfileSave} />
); }; export default SettingsPage;