mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:36:38 +02:00
feat: O365-Profil automatisch beim Login synchronisieren
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
347a6ca418
commit
138742d385
3 changed files with 87 additions and 20 deletions
62
packages/frontend/src/hooks/useO365ProfileSync.ts
Normal file
62
packages/frontend/src/hooks/useO365ProfileSync.ts
Normal file
|
|
@ -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<string, string | null> = {};
|
||||
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]);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
</svg>
|
||||
</span>
|
||||
<span className={styles.enrichLabel}>
|
||||
Fehlende Felder aus Microsoft 365 übernehmen
|
||||
Profil mit Microsoft 365 synchronisieren
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { UserAvatar } from '../components/UserAvatar';
|
|||
import api from '../api/client';
|
||||
import { useTheme } from '../theme/ThemeContext';
|
||||
import { useCrmSettings } from '../crm/settings/CrmSettingsContext';
|
||||
import { useO365ProfileSync } from '../hooks/useO365ProfileSync';
|
||||
import styles from './AppLayout.module.css';
|
||||
|
||||
interface ExternalLink {
|
||||
|
|
@ -114,6 +115,9 @@ export function AppLayout() {
|
|||
const navigate = useNavigate();
|
||||
const { mode, setMode } = useTheme();
|
||||
const { isModuleEnabled } = useCrmSettings();
|
||||
|
||||
// Silently sync INSIGHT profile from O365 once per browser session
|
||||
useO365ProfileSync();
|
||||
const isAdmin = user?.role === 'PLATFORM_ADMIN' || user?.role === 'TENANT_ADMIN';
|
||||
const anyCrmModuleEnabled =
|
||||
isModuleEnabled('contacts') ||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue