From 138742d385ae30424b28fa64a51d29269035bfab Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Fri, 13 Mar 2026 12:50:25 +0100 Subject: [PATCH] feat: O365-Profil automatisch beim Login synchronisieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer Hook useO365ProfileSync: läuft einmalig pro Browser-Session, überschreibt INSIGHT-Profil-Felder mit O365-Daten (firstName, lastName, phone, mobile, city, street, postalCode) — kein UI-Feedback, kein Fehler bricht die UX auf. - AppLayout ruft useO365ProfileSync() auf, sodass die Synchronisation beim Laden der App (nach Login) automatisch startet. - ProfilePage: "↓ O365 übernehmen"-Button überschreibt jetzt alle Felder wo O365 Daten hat (nicht mehr nur leere Felder) — konsistent mit dem Auto-Sync-Verhalten. Co-Authored-By: Claude Sonnet 4.6 --- .../frontend/src/hooks/useO365ProfileSync.ts | 62 +++++++++++++++++++ packages/frontend/src/profile/ProfilePage.tsx | 41 ++++++------ packages/frontend/src/shell/AppLayout.tsx | 4 ++ 3 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 packages/frontend/src/hooks/useO365ProfileSync.ts diff --git a/packages/frontend/src/hooks/useO365ProfileSync.ts b/packages/frontend/src/hooks/useO365ProfileSync.ts new file mode 100644 index 0000000..5cc4877 --- /dev/null +++ b/packages/frontend/src/hooks/useO365ProfileSync.ts @@ -0,0 +1,62 @@ +import { useEffect } from 'react'; +import { useAuth } from '../auth/AuthContext'; +import { useIntegrations } from '../crm/hooks'; +import { office365Api } from '../crm/api'; +import api from '../api/client'; + +const SESSION_KEY = 'o365_profile_synced'; + +/** + * Syncs the INSIGHT user profile from Microsoft 365 once per browser session. + * Runs silently in the background — no UI feedback, no disruption on error. + * Uses sessionStorage to prevent duplicate syncs within the same session. + * + * Fields overwritten where O365 has a non-null value: + * firstName, lastName, phone, mobile, city, street, postalCode + */ +export function useO365ProfileSync(): void { + const { user, refreshUser } = useAuth(); + const { data: integrationsData } = useIntegrations(); + + const isM365Connected = + integrationsData?.data?.some( + (i) => i.provider === 'MICROSOFT_365' && i.connected, + ) ?? false; + + useEffect(() => { + // Prerequisites: authenticated user + M365 connected + not yet synced this session + if (!user || !isM365Connected) return; + if (sessionStorage.getItem(SESSION_KEY)) return; + + // Mark as synced immediately to prevent duplicate calls on concurrent re-renders + sessionStorage.setItem(SESSION_KEY, 'true'); + + void (async () => { + try { + const result = await office365Api.getM365Profile(); + const p = result.data; + + // Overwrite all fields where O365 has a non-null value + const patch: Record = {}; + if (p.givenName) patch.firstName = p.givenName; + if (p.surname) patch.lastName = p.surname; + if (p.businessPhones?.[0]) patch.phone = p.businessPhones[0]; + if (p.mobilePhone) patch.mobile = p.mobilePhone; + if (p.city) patch.city = p.city; + if (p.streetAddress) patch.street = p.streetAddress; + if (p.postalCode) patch.postalCode = p.postalCode; + + if (Object.keys(patch).length > 0) { + await api.patch('/users/me', patch); + await refreshUser(); + } + } catch { + // Silent failure — auto-sync must never disrupt the user experience. + // Remove the flag so a future navigation attempt can retry. + sessionStorage.removeItem(SESSION_KEY); + } + })(); + // refreshUser is stable from AuthContext; intentionally excluded from deps + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [user?.id, isM365Connected]); +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index ce6b633..5d67f83 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -174,7 +174,7 @@ export function ProfilePage() { } }; - // === Handler: Profil aus O365 anreichern === + // === Handler: Profil aus O365 synchronisieren (überschreibt bestehende Werte) === const handleEnrichFromO365 = async () => { setEnrichMsg(''); setEnrichError(''); @@ -184,43 +184,44 @@ export function ProfilePage() { const result = await office365Api.getM365Profile(); const p = result.data; - const filled: string[] = []; + const updated: string[] = []; - if (!firstName && p.givenName) { + // Always overwrite with O365 values where O365 has data + if (p.givenName) { setFirstName(p.givenName); - filled.push('Vorname'); + updated.push('Vorname'); } - if (!lastName && p.surname) { + if (p.surname) { setLastName(p.surname); - filled.push('Nachname'); + updated.push('Nachname'); } - if (!phone && p.businessPhones?.[0]) { + if (p.businessPhones?.[0]) { setPhone(p.businessPhones[0]); - filled.push('Telefon'); + updated.push('Telefon'); } - if (!mobile && p.mobilePhone) { + if (p.mobilePhone) { setMobile(p.mobilePhone); - filled.push('Mobil'); + updated.push('Mobil'); } - if (!city && p.city) { + if (p.city) { setCity(p.city); - filled.push('Ort'); + updated.push('Ort'); } - if (!street && p.streetAddress) { + if (p.streetAddress) { setStreet(p.streetAddress); - filled.push('Straße'); + updated.push('Straße'); } - if (!postalCode && p.postalCode) { + if (p.postalCode) { setPostalCode(p.postalCode); - filled.push('PLZ'); + updated.push('PLZ'); } - if (filled.length > 0) { + if (updated.length > 0) { setEnrichMsg( - `${filled.length} Felder übernommen: ${filled.join(', ')}. Bitte prüfen und speichern.`, + `${updated.length} Felder aktualisiert: ${updated.join(', ')}. Bitte prüfen und speichern.`, ); } else { - setEnrichMsg('Alle Felder sind bereits ausgefüllt — keine Änderungen.'); + setEnrichMsg('Keine Profildaten in Microsoft 365 gefunden.'); } } catch { setEnrichError('O365-Profil konnte nicht geladen werden.'); @@ -504,7 +505,7 @@ export function ProfilePage() { - Fehlende Felder aus Microsoft 365 übernehmen + Profil mit Microsoft 365 synchronisieren