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 ?? ''); // --- Organisation --- const [jobTitle, setJobTitle] = useState(user?.jobTitle ?? ''); const [department, setDepartment] = useState(user?.department ?? ''); const [companyName, setCompanyName] = useState(user?.companyName ?? ''); const [officeLocation, setOfficeLocation] = useState(user?.officeLocation ?? ''); 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]); // Organisationsdaten mit Context-User synchronisieren useEffect(() => { if (user) { setJobTitle(user.jobTitle ?? ''); setDepartment(user.department ?? ''); setCompanyName(user.companyName ?? ''); setOfficeLocation(user.officeLocation ?? ''); } }, [user?.jobTitle, user?.department, user?.companyName, user?.officeLocation]); // === 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, jobTitle: jobTitle || null, department: department || null, companyName: companyName || null, officeLocation: officeLocation || 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 synchronisieren (überschreibt bestehende Werte) === const handleEnrichFromO365 = async () => { setEnrichMsg(''); setEnrichError(''); setEnrichLoading(true); try { // Profil + Foto parallel laden const [profileResult, photoResult] = await Promise.all([ office365Api.getM365Profile(), office365Api.getM365Photo(), ]); const p = profileResult.data; const updated: string[] = []; // Kontaktfelder — immer überschreiben wo O365 Daten hat if (p.givenName) { setFirstName(p.givenName); updated.push('Vorname'); } if (p.surname) { setLastName(p.surname); updated.push('Nachname'); } if (p.businessPhones?.[0]) { setPhone(p.businessPhones[0]); updated.push('Telefon'); } if (p.mobilePhone) { setMobile(p.mobilePhone); updated.push('Mobil'); } if (p.city) { setCity(p.city); updated.push('Ort'); } if (p.streetAddress) { setStreet(p.streetAddress); updated.push('Straße'); } if (p.postalCode) { setPostalCode(p.postalCode); updated.push('PLZ'); } // Organisationsfelder if (p.jobTitle) { setJobTitle(p.jobTitle); updated.push('Position'); } if (p.department) { setDepartment(p.department); updated.push('Abteilung'); } if (p.companyName) { setCompanyName(p.companyName); updated.push('Firma'); } if (p.officeLocation) { setOfficeLocation(p.officeLocation); updated.push('Standort'); } // Profilbild übernehmen (manuelle Sync überschreibt immer) if (photoResult.data.photoBase64) { await api.patch('/users/me', { avatar: photoResult.data.photoBase64 }); setAvatar(photoResult.data.photoBase64); await refreshUser(); updated.push('Profilbild'); } if (updated.length > 0) { setEnrichMsg( `${updated.length} Felder aktualisiert: ${updated.join(', ')}. Bitte prüfen und speichern.`, ); } else { setEnrichMsg('Keine Profildaten in Microsoft 365 gefunden.'); } } 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 && (
Profil mit Microsoft 365 synchronisieren
{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" />
{/* Organisation */}
Organisation
setJobTitle(e.target.value)} maxLength={100} placeholder="Senior Developer" />
setDepartment(e.target.value)} maxLength={100} placeholder="Engineering" />
setCompanyName(e.target.value)} maxLength={200} placeholder="Acme GmbH" />
setOfficeLocation(e.target.value)} maxLength={200} placeholder="Berlin Office" />
)} {/* === 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.

)}
)}
); }