frontend_nyla/src/pages/Settings.tsx
2026-01-25 23:57:47 +01:00

371 lines
13 KiB
TypeScript

/**
* Settings Page
*
* Benutzer-Einstellungen (System-Level, ohne Instanz-Kontext).
*/
import React, { useState, useCallback } 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 styles from './Settings.module.css';
// =============================================================================
// PROFILE EDIT MODAL
// =============================================================================
interface ProfileEditModalProps {
isOpen: boolean;
onClose: () => void;
userData: any;
onSave: (data: any) => Promise<void>;
}
const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, userData, onSave }) => {
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// Define editable profile fields
const profileAttributes: AttributeDefinition[] = [
{
name: 'fullName',
type: 'string',
label: 'Vollständiger Name',
description: 'Ihr vollständiger Name',
required: false,
placeholder: 'Max Mustermann'
},
{
name: 'email',
type: 'email',
label: 'E-Mail-Adresse',
description: 'Ihre E-Mail-Adresse für Benachrichtigungen',
required: true,
placeholder: 'name@example.com'
},
{
name: 'language',
type: 'select',
label: 'Sprache',
description: 'Anzeigesprache der Anwendung',
required: true,
options: [
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' }
]
}
];
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 (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2>Profil bearbeiten</h2>
<button className={styles.closeButton} onClick={onClose}>&times;</button>
</div>
<div className={styles.modalBody}>
{error && <div className={styles.errorMessage}>{error}</div>}
<FormGeneratorForm
attributes={profileAttributes}
data={userData}
mode="edit"
onSubmit={handleSubmit}
onCancel={onClose}
submitButtonText={isSaving ? 'Speichern...' : 'Speichern'}
cancelButtonText="Abbrechen"
/>
</div>
</div>
</div>
);
};
// =============================================================================
// SETTINGS PAGE
// =============================================================================
export const SettingsPage: React.FC = () => {
const { currentLanguage, setLanguage } = useLanguage();
const { user: currentUser, refetch: refetchUser } = useCurrentUser();
const { updateUser } = useUser();
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<string | null>(null);
// Handle theme change
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);
};
// Handle language change - save to backend and update cache
const handleLanguageChange = useCallback(async (newLanguage: 'de' | 'en' | 'fr') => {
if (!currentUser?.id || !currentUser?.username) return;
setIsSavingLanguage(true);
setLanguageError(null);
try {
// 1. Build full user object for update (backend requires full User model)
const userUpdateData = {
id: currentUser.id,
username: currentUser.username,
email: currentUser.email,
fullName: currentUser.fullName,
language: newLanguage,
enabled: currentUser.enabled ?? true,
authenticationAuthority: currentUser.authenticationAuthority || 'local'
};
// 2. Save to backend
await updateUser(currentUser.id, userUpdateData);
// 3. Update sessionStorage cache
const cachedUser = getUserDataCache();
if (cachedUser) {
setUserDataCache({ ...cachedUser, language: newLanguage });
}
// 4. Update UI language context
setLanguage(newLanguage);
// 5. Dispatch event to notify other components
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
console.log('Language updated successfully to:', newLanguage);
} catch (err: any) {
console.error('Failed to update language:', err);
setLanguageError('Sprache konnte nicht gespeichert werden');
} finally {
setIsSavingLanguage(false);
}
}, [currentUser, updateUser, setLanguage]);
// Handle profile save
const handleProfileSave = useCallback(async (formData: any) => {
if (!currentUser?.id || !currentUser?.username) throw new Error('Nicht angemeldet');
// Get the new language (from form or current user)
const newLanguage = formData.language || currentUser.language || 'de';
// Build full user object for update (backend requires full User model)
const userUpdateData = {
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'
};
// Update user via API
const updatedUser = await updateUser(currentUser.id, userUpdateData);
// Update sessionStorage cache
const cachedUser = getUserDataCache();
if (cachedUser) {
setUserDataCache({
...cachedUser,
fullName: updatedUser.fullName || cachedUser.fullName,
email: updatedUser.email || cachedUser.email,
language: newLanguage
});
}
// Update UI language if changed
if (newLanguage !== currentLanguage) {
setLanguage(newLanguage as 'de' | 'en' | 'fr');
}
// Refetch user data
if (refetchUser) {
await refetchUser();
}
// Dispatch event to notify other components (e.g., sidebar)
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
}, [currentUser, updateUser, refetchUser, currentLanguage, setLanguage]);
return (
<div className={styles.settings}>
<header className={styles.header}>
<h1>Einstellungen</h1>
<p className={styles.subtitle}>Persönliche Einstellungen und Präferenzen</p>
</header>
<main className={styles.content}>
{/* Darstellung */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Darstellung</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>Theme</label>
<p className={styles.settingDescription}>
Wähle zwischen hellem und dunklem Design.
</p>
</div>
<div className={styles.settingControl}>
<div className={styles.themeToggle}>
<button
className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`}
onClick={() => handleThemeChange('light')}
>
Hell
</button>
<button
className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`}
onClick={() => handleThemeChange('dark')}
>
Dunkel
</button>
</div>
</div>
</div>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>Sprache</label>
<p className={styles.settingDescription}>
Wähle die Anzeigesprache der Anwendung.
{languageError && <span className={styles.errorText}> {languageError}</span>}
</p>
</div>
<div className={styles.settingControl}>
<select
className={styles.select}
value={currentLanguage}
onChange={(e) => handleLanguageChange(e.target.value as 'de' | 'en' | 'fr')}
disabled={isSavingLanguage}
>
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
{isSavingLanguage && <span className={styles.savingIndicator}>Speichern...</span>}
</div>
</div>
</section>
{/* Konto */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Konto</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>Profil bearbeiten</label>
<p className={styles.settingDescription}>
Ändere deinen Namen und E-Mail-Adresse.
</p>
</div>
<div className={styles.settingControl}>
<button
className={styles.button}
onClick={() => setIsProfileModalOpen(true)}
>
Profil öffnen
</button>
</div>
</div>
{/* Current user info display */}
{currentUser && (
<div className={styles.userInfoCard}>
<div className={styles.userInfoRow}>
<span className={styles.userInfoLabel}>Benutzername</span>
<span className={styles.userInfoValue}>{currentUser.username}</span>
</div>
<div className={styles.userInfoRow}>
<span className={styles.userInfoLabel}>Name</span>
<span className={styles.userInfoValue}>{currentUser.fullName || '-'}</span>
</div>
<div className={styles.userInfoRow}>
<span className={styles.userInfoLabel}>E-Mail</span>
<span className={styles.userInfoValue}>{currentUser.email || '-'}</span>
</div>
</div>
)}
</section>
{/* Datenschutz */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Datenschutz</h2>
<div className={styles.settingRow}>
<div className={styles.settingInfo}>
<label className={styles.settingLabel}>GDPR / Privacy</label>
<p className={styles.settingDescription}>
Data export, portability and account deletion.
</p>
</div>
<div className={styles.settingControl}>
<Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">
Open GDPR page
</Link>
</div>
</div>
</section>
{/* Info */}
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Über</h2>
<div className={styles.infoCard}>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Version</span>
<span className={styles.infoValue}>2.0.0</span>
</div>
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Build</span>
<span className={styles.infoValue}>2026.01.20</span>
</div>
</div>
</section>
</main>
{/* Profile Edit Modal */}
<ProfileEditModal
isOpen={isProfileModalOpen}
onClose={() => setIsProfileModalOpen(false)}
userData={currentUser}
onSave={handleProfileSave}
/>
</div>
);
};
export default SettingsPage;