import { useState, useEffect, useRef, type FormEvent, type ChangeEvent } from 'react'; import { useLocation } from 'react-router-dom'; import { useAuth } from '../auth/AuthContext'; import api from '../api/client'; import { UserAvatar } from '../components/UserAvatar'; import { resizeImageToBase64 } from '../utils/imageUtils'; import { ExpertProfileTab } from './ExpertProfileTab'; import { useIntegrations, useDisconnectM365 } from '../crm/hooks'; import { integrationsApi, office365Api } from '../crm/api'; import styles from './ProfilePage.module.css'; type ProfileTab = 'personal' | 'expert' | 'password' | 'integrations'; export function ProfilePage() { const { user, refreshUser } = useAuth(); // --- Persönliche Informationen --- const [firstName, setFirstName] = useState(user?.firstName ?? ''); const [lastName, setLastName] = useState(user?.lastName ?? ''); const [phone, setPhone] = useState(user?.phone ?? ''); const [mobile, setMobile] = useState(user?.mobile ?? ''); const [street, setStreet] = useState(user?.street ?? ''); const [postalCode, setPostalCode] = useState(user?.postalCode ?? ''); const [city, setCity] = useState(user?.city ?? ''); const [profileMsg, setProfileMsg] = useState(''); const [profileError, setProfileError] = useState(''); const [profileLoading, setProfileLoading] = useState(false); // --- Profilbild --- const [avatar, setAvatar] = useState(user?.avatar ?? null); const [avatarMsg, setAvatarMsg] = useState(''); const [avatarError, setAvatarError] = useState(''); const [avatarLoading, setAvatarLoading] = useState(false); const fileInputRef = useRef(null); // --- O365-Profilanreicherung --- const [enrichLoading, setEnrichLoading] = useState(false); const [enrichMsg, setEnrichMsg] = useState(''); const [enrichError, setEnrichError] = useState(''); // --- Passwort ändern --- const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordMsg, setPasswordMsg] = useState(''); const [passwordError, setPasswordError] = useState(''); const [passwordLoading, setPasswordLoading] = useState(false); // --- 2FA --- const [twoFactorEnabled, setTwoFactorEnabled] = useState( user?.twoFactorEnabled ?? false, ); const [setupData, setSetupData] = useState<{ qrCode: string; secret: string; } | null>(null); const [totpCode, setTotpCode] = useState(''); const [disablePassword, setDisablePassword] = useState(''); const [showDisableConfirm, setShowDisableConfirm] = useState(false); const [tfaMsg, setTfaMsg] = useState(''); const [tfaError, setTfaError] = useState(''); const [tfaLoading, setTfaLoading] = useState(false); // 2FA-Status mit Context-User synchronisieren useEffect(() => { if (user?.twoFactorEnabled !== undefined) { setTwoFactorEnabled(user.twoFactorEnabled); } }, [user?.twoFactorEnabled]); // Avatar mit Context-User synchronisieren useEffect(() => { if (user?.avatar !== undefined) { setAvatar(user.avatar ?? null); } }, [user?.avatar]); // Kontaktdaten mit Context-User synchronisieren useEffect(() => { if (user) { setPhone(user.phone ?? ''); setMobile(user.mobile ?? ''); setStreet(user.street ?? ''); setPostalCode(user.postalCode ?? ''); setCity(user.city ?? ''); } }, [user?.phone, user?.mobile, user?.street, user?.postalCode, user?.city]); // === Handler: Profilbild hochladen === const handleAvatarChange = async (e: ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { setAvatarError('Bitte wählen Sie eine Bilddatei aus'); return; } if (file.size > 5 * 1024 * 1024) { setAvatarError('Bild darf maximal 5MB groß sein'); return; } setAvatarMsg(''); setAvatarError(''); setAvatarLoading(true); try { const base64 = await resizeImageToBase64(file, 200, 200); await api.patch('/users/me', { avatar: base64 }); setAvatar(base64); await refreshUser(); setAvatarMsg('Profilbild erfolgreich aktualisiert'); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setAvatarError( error.response?.data?.message ?? 'Fehler beim Hochladen des Profilbilds', ); } finally { setAvatarLoading(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; // === Handler: Profilbild entfernen === const handleAvatarRemove = async () => { setAvatarMsg(''); setAvatarError(''); setAvatarLoading(true); try { await api.patch('/users/me', { avatar: null }); setAvatar(null); await refreshUser(); setAvatarMsg('Profilbild entfernt'); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setAvatarError( error.response?.data?.message ?? 'Fehler beim Entfernen des Profilbilds', ); } finally { setAvatarLoading(false); } }; // === Handler: Profil aktualisieren === const handleProfileUpdate = async (e: FormEvent) => { e.preventDefault(); setProfileMsg(''); setProfileError(''); setProfileLoading(true); try { await api.patch('/users/me', { firstName, lastName, phone: phone || null, mobile: mobile || null, street: street || null, postalCode: postalCode || null, city: city || null, }); await refreshUser(); setProfileMsg('Profil erfolgreich aktualisiert'); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setProfileError( error.response?.data?.message ?? 'Fehler beim Aktualisieren', ); } finally { setProfileLoading(false); } }; // === Handler: Profil aus O365 anreichern === const handleEnrichFromO365 = async () => { setEnrichMsg(''); setEnrichError(''); setEnrichLoading(true); try { const result = await office365Api.getM365Profile(); const p = result.data; const filled: string[] = []; if (!firstName && p.givenName) { setFirstName(p.givenName); filled.push('Vorname'); } if (!lastName && p.surname) { setLastName(p.surname); filled.push('Nachname'); } if (!phone && p.businessPhones?.[0]) { setPhone(p.businessPhones[0]); filled.push('Telefon'); } if (!mobile && p.mobilePhone) { setMobile(p.mobilePhone); filled.push('Mobil'); } if (!city && p.city) { setCity(p.city); filled.push('Ort'); } if (!street && p.streetAddress) { setStreet(p.streetAddress); filled.push('Straße'); } if (!postalCode && p.postalCode) { setPostalCode(p.postalCode); filled.push('PLZ'); } if (filled.length > 0) { setEnrichMsg( `${filled.length} Felder übernommen: ${filled.join(', ')}. Bitte prüfen und speichern.`, ); } else { setEnrichMsg('Alle Felder sind bereits ausgefüllt — keine Änderungen.'); } } catch { setEnrichError('O365-Profil konnte nicht geladen werden.'); } finally { setEnrichLoading(false); } }; // === Handler: Passwort ändern === const handlePasswordChange = async (e: FormEvent) => { e.preventDefault(); setPasswordMsg(''); setPasswordError(''); if (newPassword !== confirmPassword) { setPasswordError('Passwörter stimmen nicht überein'); return; } if (newPassword.length < 8) { setPasswordError('Neues Passwort muss mindestens 8 Zeichen lang sein'); return; } setPasswordLoading(true); try { await api.post('/users/me/change-password', { currentPassword, newPassword, }); setPasswordMsg('Passwort erfolgreich geändert'); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setPasswordError( error.response?.data?.message ?? 'Fehler beim Ändern des Passworts', ); } finally { setPasswordLoading(false); } }; // === Handler: 2FA Setup starten === const handleSetup2fa = async () => { setTfaMsg(''); setTfaError(''); setTfaLoading(true); try { const { data } = await api.post<{ qrCode: string; secret: string }>( '/auth/2fa/setup', ); setSetupData(data); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( error.response?.data?.message ?? 'Fehler beim 2FA-Setup', ); } finally { setTfaLoading(false); } }; // === Handler: 2FA aktivieren (Code verifizieren) === const handleEnable2fa = async (e: FormEvent) => { e.preventDefault(); setTfaMsg(''); setTfaError(''); setTfaLoading(true); try { await api.post('/auth/2fa/enable', { totpCode }); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( error.response?.data?.message ?? 'Ungültiger Code', ); setTfaLoading(false); return; } // Erfolg — State aktualisieren setTwoFactorEnabled(true); setSetupData(null); setTotpCode(''); setTfaMsg('2FA wurde erfolgreich aktiviert'); setTfaLoading(false); // User-Context im Hintergrund aktualisieren refreshUser().catch(() => {}); }; // === Handler: 2FA deaktivieren === const handleDisable2fa = async (e: FormEvent) => { e.preventDefault(); setTfaMsg(''); setTfaError(''); setTfaLoading(true); try { await api.post('/auth/2fa/disable', { password: disablePassword }); } catch (err: unknown) { const error = err as { response?: { data?: { message?: string } } }; setTfaError( error.response?.data?.message ?? 'Fehler beim Deaktivieren', ); setTfaLoading(false); return; } // Erfolg — State aktualisieren setTwoFactorEnabled(false); setShowDisableConfirm(false); setDisablePassword(''); setTfaMsg('2FA wurde erfolgreich deaktiviert'); setTfaLoading(false); // User-Context im Hintergrund aktualisieren refreshUser().catch(() => {}); }; // --- Tab-Navigation --- const location = useLocation(); const searchParams = new URLSearchParams(location.search); const integrationParam = searchParams.get('integration'); const integrationStatus = searchParams.get('status'); const [activeTab, setActiveTab] = useState( integrationParam === 'microsoft-365' ? 'integrations' : 'personal', ); // --- Microsoft 365 Integration --- const { data: integrationsData, isLoading: integrationsLoading } = useIntegrations(); const disconnectM365 = useDisconnectM365(); const m365Integration = integrationsData?.data?.find((i) => i.provider === 'MICROSOFT_365'); const [m365Msg, setM365Msg] = useState( integrationParam === 'microsoft-365' && integrationStatus === 'success' ? 'Microsoft 365 wurde erfolgreich verbunden!' : integrationParam === 'microsoft-365' && integrationStatus === 'error' ? 'Verbindung mit Microsoft 365 fehlgeschlagen. Bitte versuchen Sie es erneut.' : '', ); const [m365IsError, setM365IsError] = useState( integrationParam === 'microsoft-365' && integrationStatus === 'error', ); return (

Mein Profil {!twoFactorEnabled && ( setActiveTab('password')} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setActiveTab('password'); }} title="Klicken, um 2FA zu aktivieren" > 2FA nicht aktiv )}

{/* === Tab-Leiste === */}
{/* === Tab: Profil === */} {activeTab === 'personal' && (
{/* --- Linke Spalte: Avatar --- */}
fileInputRef.current?.click()} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click(); }} >
Ändern
{avatar && ( )}
{avatarMsg &&
{avatarMsg}
} {avatarError &&
{avatarError}
} JPEG, PNG, GIF oder WebP. Max. 200x200px.
{/* --- Rechte Spalte: Formular --- */}
{/* O365-Anreicherung (nur wenn verbunden) */} {m365Integration?.connected && (
Fehlende Felder aus Microsoft 365 übernehmen
{enrichMsg &&
{enrichMsg}
} {enrichError &&
{enrichError}
}
)}
{profileMsg &&
{profileMsg}
} {profileError &&
{profileError}
} {/* Name */}
Name
setFirstName(e.target.value)} required maxLength={100} />
setLastName(e.target.value)} required maxLength={100} />
{/* E-Mail & Rolle */}
E-Mail kann nicht geändert werden
{/* Telefon */}
Kontakt
setPhone(e.target.value)} maxLength={30} placeholder="+49 123 456789" />
setMobile(e.target.value)} maxLength={30} placeholder="+49 170 1234567" />
{/* Adresse */}
Adresse
setStreet(e.target.value)} maxLength={200} placeholder="Musterstraße 42" />
setPostalCode(e.target.value)} maxLength={10} placeholder="12345" />
setCity(e.target.value)} maxLength={100} placeholder="Berlin" />
)} {/* === Tab: Experten Profil === */} {activeTab === 'expert' && } {/* === Tab: Passwort ändern + 2FA === */} {activeTab === 'password' && ( <>

Passwort ändern

{passwordMsg &&
{passwordMsg}
} {passwordError && (
{passwordError}
)}
setCurrentPassword(e.target.value)} required minLength={8} />
setNewPassword(e.target.value)} required minLength={8} />
setConfirmPassword(e.target.value)} required minLength={8} />
{/* === 2FA Sektion (innerhalb Passwort-Tab) === */}

Zwei-Faktor-Authentifizierung (2FA)

{tfaMsg &&
{tfaMsg}
} {tfaError &&
{tfaError}
}
{twoFactorEnabled ? '2FA ist aktiviert' : '2FA ist nicht aktiviert'}
{/* 2FA NICHT aktiviert: Setup starten */} {!twoFactorEnabled && !setupData && ( )} {/* QR-Code anzeigen + Verifizierung */} {!twoFactorEnabled && setupData && (

Scannen Sie den QR-Code mit Ihrer Authenticator-App (z.B. Google Authenticator, Authy):

QR-Code für 2FA
{setupData.secret}
setTotpCode(e.target.value)} placeholder="6-stelliger Code" maxLength={6} pattern="[0-9]{6}" required autoFocus /> Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein
)} {/* 2FA aktiviert: Deaktivieren-Option */} {twoFactorEnabled && !showDisableConfirm && ( )} {/* Deaktivierung mit Passwort bestätigen */} {twoFactorEnabled && showDisableConfirm && (

Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren:

setDisablePassword(e.target.value)} required minLength={8} autoFocus />
)}
)} {/* === Tab: Integrationen === */} {activeTab === 'integrations' && ( <>

Microsoft 365

{m365Msg && (
{m365Msg}
)} {integrationsLoading ? (

Laden…

) : m365Integration?.connected ? (

● Verbunden {m365Integration.msTenantId && ( Tenant: {m365Integration.msTenantId} )}

{m365Integration.expiresAt && (

Token gültig bis: {new Date(m365Integration.expiresAt).toLocaleString('de-DE')}

)}
) : (

Verbinden Sie Ihr Microsoft 365 Konto, um E-Mails, Kalendertermine und Aufgaben direkt in Kontaktprofilen zu sehen.

)}
)}
); }