INSIGHT-MVP/packages/frontend/src/profile/ProfilePage.tsx
Thomas Reitz 347a6ca418 feat: Profil mit Microsoft 365 Daten anreichern
- GraphService: getM365Profile() lädt givenName, surname, mobilePhone,
  businessPhones, city, streetAddress, postalCode, jobTitle via /me
- Office365Controller: GET /crm/office365/profile Endpunkt
- Frontend types: M365UserProfile Interface
- Frontend api: office365Api.getM365Profile()
- ProfilePage: O365 uebernehmen Button (nur wenn M365 verbunden)
  fuellt leere Felder: Vorname, Nachname, Telefon, Mobil, Ort, Strasse, PLZ
  Bestehende Werte werden NICHT ueberschrieben; Feedback zeigt welche
  Felder uebernommen wurden; User muss Speichern klicken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 12:38:43 +01:00

963 lines
33 KiB
TypeScript

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<string | null>(user?.avatar ?? null);
const [avatarMsg, setAvatarMsg] = useState('');
const [avatarError, setAvatarError] = useState('');
const [avatarLoading, setAvatarLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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<ProfileTab>(
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 (
<div>
<h1
style={{
fontSize: '1.5rem',
fontWeight: 600,
marginBottom: '1.5rem',
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
}}
>
Mein Profil
{!twoFactorEnabled && (
<span
className={styles.tfaWarning}
onClick={() => 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
</span>
)}
</h1>
{/* === Tab-Leiste === */}
<div className={styles.tabs}>
<button
type="button"
className={`${styles.tab} ${activeTab === 'personal' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('personal')}
>
Profil
</button>
<button
type="button"
className={`${styles.tab} ${activeTab === 'expert' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('expert')}
>
Experten Profil
</button>
<button
type="button"
className={`${styles.tab} ${activeTab === 'password' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('password')}
>
Passwort ändern
</button>
<button
type="button"
className={`${styles.tab} ${activeTab === 'integrations' ? styles.tabActive : ''}`}
onClick={() => setActiveTab('integrations')}
>
Integrationen
</button>
</div>
{/* === Tab: Profil === */}
{activeTab === 'personal' && (
<div className={styles.section}>
<div className={styles.profileLayout}>
{/* --- Linke Spalte: Avatar --- */}
<div className={styles.avatarColumn}>
<div
className={styles.avatarPreview}
onClick={() => fileInputRef.current?.click()}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click();
}}
>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={avatar}
size={96}
/>
<div className={styles.avatarOverlay}>
<span>Ändern</span>
</div>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={handleAvatarChange}
className={styles.hiddenInput}
/>
<div className={styles.avatarActions}>
<button
type="button"
className={styles.buttonSecondary}
onClick={() => fileInputRef.current?.click()}
disabled={avatarLoading}
>
{avatarLoading ? 'Laden...' : 'Hochladen'}
</button>
{avatar && (
<button
type="button"
className={styles.buttonDanger}
onClick={handleAvatarRemove}
disabled={avatarLoading}
>
Entfernen
</button>
)}
</div>
{avatarMsg && <div className={styles.success}>{avatarMsg}</div>}
{avatarError && <div className={styles.error}>{avatarError}</div>}
<small className={styles.avatarHint}>
JPEG, PNG, GIF oder WebP. Max. 200x200px.
</small>
</div>
{/* --- Rechte Spalte: Formular --- */}
<div className={styles.formColumn}>
{/* O365-Anreicherung (nur wenn verbunden) */}
{m365Integration?.connected && (
<div className={styles.enrichBanner}>
<div className={styles.enrichInfo}>
<span className={styles.enrichIcon}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/>
<path d="M15 9l-6 6M9 9l6 6"/>
</svg>
</span>
<span className={styles.enrichLabel}>
Fehlende Felder aus Microsoft 365 übernehmen
</span>
</div>
<button
type="button"
className={styles.enrichBtn}
onClick={handleEnrichFromO365}
disabled={enrichLoading}
>
{enrichLoading ? 'Lädt…' : '↓ O365 übernehmen'}
</button>
{enrichMsg && <div className={`${styles.success} ${styles.enrichFeedback}`}>{enrichMsg}</div>}
{enrichError && <div className={`${styles.error} ${styles.enrichFeedback}`}>{enrichError}</div>}
</div>
)}
<form onSubmit={handleProfileUpdate} className={styles.form}>
{profileMsg && <div className={styles.success}>{profileMsg}</div>}
{profileError && <div className={styles.error}>{profileError}</div>}
{/* Name */}
<fieldset className={styles.fieldGroup}>
<legend className={styles.fieldGroupLegend}>Name</legend>
<div className={styles.fieldRow}>
<div className={styles.field}>
<label htmlFor="firstName">Vorname</label>
<input
id="firstName"
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
maxLength={100}
/>
</div>
<div className={styles.field}>
<label htmlFor="lastName">Nachname</label>
<input
id="lastName"
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
maxLength={100}
/>
</div>
</div>
</fieldset>
{/* E-Mail & Rolle */}
<div className={styles.fieldRow}>
<div className={styles.field}>
<label htmlFor="email">E-Mail</label>
<input
id="email"
type="email"
value={user?.email ?? ''}
disabled
/>
<small>E-Mail kann nicht geändert werden</small>
</div>
<div className={styles.field}>
<label>Rolle</label>
<input type="text" value={user?.role ?? ''} disabled />
</div>
</div>
{/* Telefon */}
<fieldset className={styles.fieldGroup}>
<legend className={styles.fieldGroupLegend}>Kontakt</legend>
<div className={styles.fieldRow}>
<div className={styles.field}>
<label htmlFor="phone">Telefon</label>
<input
id="phone"
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
maxLength={30}
placeholder="+49 123 456789"
/>
</div>
<div className={styles.field}>
<label htmlFor="mobile">Mobil</label>
<input
id="mobile"
type="tel"
value={mobile}
onChange={(e) => setMobile(e.target.value)}
maxLength={30}
placeholder="+49 170 1234567"
/>
</div>
</div>
</fieldset>
{/* Adresse */}
<fieldset className={styles.fieldGroup}>
<legend className={styles.fieldGroupLegend}>Adresse</legend>
<div className={styles.field}>
<label htmlFor="street">Straße</label>
<input
id="street"
type="text"
value={street}
onChange={(e) => setStreet(e.target.value)}
maxLength={200}
placeholder="Musterstraße 42"
/>
</div>
<div className={styles.fieldRow}>
<div className={`${styles.field} ${styles.fieldSmall}`}>
<label htmlFor="postalCode">PLZ</label>
<input
id="postalCode"
type="text"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
maxLength={10}
placeholder="12345"
/>
</div>
<div className={styles.field}>
<label htmlFor="city">Ort</label>
<input
id="city"
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
maxLength={100}
placeholder="Berlin"
/>
</div>
</div>
</fieldset>
<button
type="submit"
className={styles.button}
disabled={profileLoading}
>
{profileLoading ? 'Speichern...' : 'Profil speichern'}
</button>
</form>
</div>
</div>
</div>
)}
{/* === Tab: Experten Profil === */}
{activeTab === 'expert' && <ExpertProfileTab />}
{/* === Tab: Passwort ändern + 2FA === */}
{activeTab === 'password' && (
<>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Passwort ändern</h2>
<form onSubmit={handlePasswordChange} className={styles.form}>
{passwordMsg && <div className={styles.success}>{passwordMsg}</div>}
{passwordError && (
<div className={styles.error}>{passwordError}</div>
)}
<div className={styles.field}>
<label htmlFor="currentPassword">Aktuelles Passwort</label>
<input
id="currentPassword"
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
minLength={8}
/>
</div>
<div className={styles.field}>
<label htmlFor="newPassword">Neues Passwort</label>
<input
id="newPassword"
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
</div>
<div className={styles.field}>
<label htmlFor="confirmPassword">Neues Passwort bestätigen</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
</div>
<button
type="submit"
className={styles.button}
disabled={passwordLoading}
>
{passwordLoading ? 'Ändern...' : 'Passwort ändern'}
</button>
</form>
</div>
{/* === 2FA Sektion (innerhalb Passwort-Tab) === */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
Zwei-Faktor-Authentifizierung (2FA)
</h2>
{tfaMsg && <div className={styles.success}>{tfaMsg}</div>}
{tfaError && <div className={styles.error}>{tfaError}</div>}
<div className={styles.tfaStatus}>
<span
style={{
display: 'inline-block',
width: 8,
height: 8,
borderRadius: '50%',
background: twoFactorEnabled
? 'var(--color-success)'
: 'var(--color-error)',
marginRight: '0.5rem',
}}
/>
<span>
{twoFactorEnabled
? '2FA ist aktiviert'
: '2FA ist nicht aktiviert'}
</span>
</div>
{/* 2FA NICHT aktiviert: Setup starten */}
{!twoFactorEnabled && !setupData && (
<button
type="button"
className={styles.button}
onClick={handleSetup2fa}
disabled={tfaLoading}
>
{tfaLoading ? 'Laden...' : '2FA aktivieren'}
</button>
)}
{/* QR-Code anzeigen + Verifizierung */}
{!twoFactorEnabled && setupData && (
<div className={styles.tfaSetup}>
<p className={styles.tfaInstructions}>
Scannen Sie den QR-Code mit Ihrer Authenticator-App (z.B. Google
Authenticator, Authy):
</p>
<div className={styles.qrContainer}>
<img src={setupData.qrCode} alt="QR-Code für 2FA" />
</div>
<div className={styles.manualSecret}>
<label>Manueller Schlüssel:</label>
<code>{setupData.secret}</code>
</div>
<form onSubmit={handleEnable2fa} className={styles.form}>
<div className={styles.field}>
<label htmlFor="totpCode">Bestätigungscode</label>
<input
id="totpCode"
type="text"
value={totpCode}
onChange={(e) => setTotpCode(e.target.value)}
placeholder="6-stelliger Code"
maxLength={6}
pattern="[0-9]{6}"
required
autoFocus
/>
<small>
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein
</small>
</div>
<div className={styles.buttonRow}>
<button
type="submit"
className={styles.button}
disabled={tfaLoading}
>
{tfaLoading
? 'Verifizieren...'
: 'Code verifizieren und aktivieren'}
</button>
<button
type="button"
className={styles.buttonSecondary}
onClick={() => {
setSetupData(null);
setTotpCode('');
}}
>
Abbrechen
</button>
</div>
</form>
</div>
)}
{/* 2FA aktiviert: Deaktivieren-Option */}
{twoFactorEnabled && !showDisableConfirm && (
<button
type="button"
className={styles.buttonDanger}
onClick={() => setShowDisableConfirm(true)}
>
2FA deaktivieren
</button>
)}
{/* Deaktivierung mit Passwort bestätigen */}
{twoFactorEnabled && showDisableConfirm && (
<form onSubmit={handleDisable2fa} className={styles.form}>
<p
style={{
color: 'var(--color-text-secondary)',
fontSize: '0.875rem',
marginBottom: '0.75rem',
}}
>
Geben Sie Ihr Passwort ein, um 2FA zu deaktivieren:
</p>
<div className={styles.field}>
<label htmlFor="disablePassword">Passwort</label>
<input
id="disablePassword"
type="password"
value={disablePassword}
onChange={(e) => setDisablePassword(e.target.value)}
required
minLength={8}
autoFocus
/>
</div>
<div className={styles.buttonRow}>
<button
type="submit"
className={styles.buttonDanger}
disabled={tfaLoading}
>
{tfaLoading ? 'Deaktivieren...' : '2FA deaktivieren'}
</button>
<button
type="button"
className={styles.buttonSecondary}
onClick={() => {
setShowDisableConfirm(false);
setDisablePassword('');
}}
>
Abbrechen
</button>
</div>
</form>
)}
</div>
</>
)}
{/* === Tab: Integrationen === */}
{activeTab === 'integrations' && (
<>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>Microsoft 365</h2>
{m365Msg && (
<div className={m365IsError ? styles.error : styles.success} style={{ marginBottom: '1rem' }}>
{m365Msg}
</div>
)}
{integrationsLoading ? (
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>Laden</p>
) : m365Integration?.connected ? (
<div>
<p style={{ fontSize: '0.9375rem', color: 'var(--color-text)', marginBottom: '0.5rem' }}>
<span style={{ color: 'var(--color-success, #16a34a)', fontWeight: 600 }}> Verbunden</span>
{m365Integration.msTenantId && (
<span style={{ marginLeft: '0.75rem', fontSize: '0.8125rem', color: 'var(--color-text-muted)' }}>
Tenant: {m365Integration.msTenantId}
</span>
)}
</p>
{m365Integration.expiresAt && (
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginBottom: '1rem' }}>
Token gültig bis: {new Date(m365Integration.expiresAt).toLocaleString('de-DE')}
</p>
)}
<button
type="button"
className={styles.buttonDanger}
disabled={disconnectM365.isPending}
onClick={() => {
setM365Msg('');
disconnectM365.mutate(undefined, {
onSuccess: () => {
setM365Msg('Microsoft 365 wurde getrennt.');
setM365IsError(false);
},
onError: () => {
setM365Msg('Fehler beim Trennen der Verbindung.');
setM365IsError(true);
},
});
}}
>
{disconnectM365.isPending ? 'Trennen…' : 'Verbindung trennen'}
</button>
</div>
) : (
<div>
<p style={{ fontSize: '0.9375rem', color: 'var(--color-text-secondary)', marginBottom: '1rem' }}>
Verbinden Sie Ihr Microsoft 365 Konto, um E-Mails, Kalendertermine und Aufgaben
direkt in Kontaktprofilen zu sehen.
</p>
<button
type="button"
onClick={() => integrationsApi.connectM365()}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1.25rem',
background: 'var(--color-primary)',
color: 'white',
border: 'none',
borderRadius: 'var(--radius-sm)',
fontSize: '0.9375rem',
fontWeight: 600,
cursor: 'pointer',
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 0h7.5v7.5H0zm8.5 0H16v7.5H8.5zM0 8.5h7.5V16H0zm8.5 0H16V16H8.5z" />
</svg>
Microsoft 365 verbinden
</button>
</div>
)}
</div>
</>
)}
</div>
);
}