diff --git a/packages/crm-service/src/graph/graph.service.ts b/packages/crm-service/src/graph/graph.service.ts index 13813fd..a5d1eca 100644 --- a/packages/crm-service/src/graph/graph.service.ts +++ b/packages/crm-service/src/graph/graph.service.ts @@ -70,6 +70,18 @@ export interface M365Contact { companyName: string | null; } +export interface M365UserProfile { + givenName: string | null; + surname: string | null; + displayName: string | null; + mobilePhone: string | null; + businessPhones: string[]; + city: string | null; + streetAddress: string | null; + postalCode: string | null; + jobTitle: string | null; +} + export interface M365MailFolder { id: string; displayName: string; @@ -597,6 +609,38 @@ export class GraphService { this.logger.debug(`Graph: Aufgabe ${taskId} als erledigt markiert`); } + /** Microsoft-365-Benutzerprofil laden (für Profilanreicherung) */ + async getM365Profile(userJwt: string): Promise { + const accessToken = await this.getM365Token(userJwt); + + const data = await this.graphGet<{ + givenName?: string | null; + surname?: string | null; + displayName?: string | null; + mobilePhone?: string | null; + businessPhones?: string[]; + city?: string | null; + streetAddress?: string | null; + postalCode?: string | null; + jobTitle?: string | null; + }>(accessToken, '/me', { + $select: + 'givenName,surname,displayName,mobilePhone,businessPhones,city,streetAddress,postalCode,jobTitle', + }); + + return { + givenName: data.givenName ?? null, + surname: data.surname ?? null, + displayName: data.displayName ?? null, + mobilePhone: data.mobilePhone ?? null, + businessPhones: data.businessPhones ?? [], + city: data.city ?? null, + streetAddress: data.streetAddress ?? null, + postalCode: data.postalCode ?? null, + jobTitle: data.jobTitle ?? null, + }; + } + /** E-Mails in einem bestimmten Ordner (mit optionalem Tages-Filter) */ async getMailsByFolder( userJwt: string, diff --git a/packages/crm-service/src/graph/office365.controller.ts b/packages/crm-service/src/graph/office365.controller.ts index 0dc96df..562c834 100644 --- a/packages/crm-service/src/graph/office365.controller.ts +++ b/packages/crm-service/src/graph/office365.controller.ts @@ -112,6 +112,14 @@ export class Office365Controller { return { success: true }; } + /** Microsoft-365-Benutzerprofil abrufen (für Profilanreicherung) */ + @Get('profile') + async getProfile(@Req() req: Request & { user: JwtUser }) { + const jwt = (req.headers.authorization ?? '').replace('Bearer ', ''); + const profile = await this.graphService.getM365Profile(jwt); + return { success: true, data: profile }; + } + @Get('folders') async getMailFolders(@Req() req: Request & { user: JwtUser }) { const jwt = (req.headers.authorization ?? '').replace('Bearer ', ''); diff --git a/packages/frontend/src/crm/api.ts b/packages/frontend/src/crm/api.ts index 551ecf5..34b7399 100644 --- a/packages/frontend/src/crm/api.ts +++ b/packages/frontend/src/crm/api.ts @@ -78,6 +78,7 @@ import type { CrmOpenTask, M365Contact, M365MailFolder, + M365UserProfile, } from './types'; // --- Contacts --- @@ -900,4 +901,9 @@ export const office365Api = { {}, ) .then((r) => r.data), + + getM365Profile: () => + api + .get<{ success: boolean; data: M365UserProfile }>('/crm/office365/profile') + .then((r) => r.data), }; diff --git a/packages/frontend/src/crm/types.ts b/packages/frontend/src/crm/types.ts index c86e12f..ee91711 100644 --- a/packages/frontend/src/crm/types.ts +++ b/packages/frontend/src/crm/types.ts @@ -1054,6 +1054,19 @@ export interface M365MailFolder { wellKnownName?: string | null; // optional — nicht von allen Exchange-Tenants unterstützt } +/** Microsoft-365-Benutzerprofil (für Profilanreicherung) */ +export interface M365UserProfile { + givenName: string | null; + surname: string | null; + displayName: string | null; + mobilePhone: string | null; + businessPhones: string[]; + city: string | null; + streetAddress: string | null; + postalCode: string | null; + jobTitle: string | null; +} + /** Minimaler CRM-Kontakt für E-Mail-Lookup */ export interface CrmContactLookup { id: string; diff --git a/packages/frontend/src/profile/ProfilePage.module.css b/packages/frontend/src/profile/ProfilePage.module.css index be3ae44..77ad410 100644 --- a/packages/frontend/src/profile/ProfilePage.module.css +++ b/packages/frontend/src/profile/ProfilePage.module.css @@ -398,3 +398,66 @@ white-space: nowrap; } } + +/* ── O365-Profilanreicherung Banner ── */ + +.enrichBanner { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.625rem; + background: rgba(59, 130, 246, 0.05); + border: 1px solid rgba(59, 130, 246, 0.2); + border-radius: var(--radius-md); + padding: 0.625rem 0.875rem; + margin-bottom: 1rem; +} + +.enrichInfo { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 0; +} + +.enrichIcon { + color: var(--color-primary); + flex-shrink: 0; + display: flex; +} + +.enrichLabel { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.enrichBtn { + padding: 0.3125rem 0.875rem; + background: var(--color-primary); + color: #fff; + border: none; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: opacity 0.15s; + flex-shrink: 0; +} + +.enrichBtn:hover:not(:disabled) { + opacity: 0.88; +} + +.enrichBtn:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.enrichFeedback { + width: 100%; + margin: 0; + padding: 0.375rem 0.625rem; + font-size: 0.8125rem; +} diff --git a/packages/frontend/src/profile/ProfilePage.tsx b/packages/frontend/src/profile/ProfilePage.tsx index ebd36cc..ce6b633 100644 --- a/packages/frontend/src/profile/ProfilePage.tsx +++ b/packages/frontend/src/profile/ProfilePage.tsx @@ -6,7 +6,7 @@ import { UserAvatar } from '../components/UserAvatar'; import { resizeImageToBase64 } from '../utils/imageUtils'; import { ExpertProfileTab } from './ExpertProfileTab'; import { useIntegrations, useDisconnectM365 } from '../crm/hooks'; -import { integrationsApi } from '../crm/api'; +import { integrationsApi, office365Api } from '../crm/api'; import styles from './ProfilePage.module.css'; type ProfileTab = 'personal' | 'expert' | 'password' | 'integrations'; @@ -33,6 +33,11 @@ export function ProfilePage() { 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(''); @@ -169,6 +174,61 @@ export function ProfilePage() { } }; + // === 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(); @@ -432,6 +492,34 @@ export function ProfilePage() { {/* --- Rechte Spalte: Formular --- */}
+ + {/* O365-Anreicherung (nur wenn verbunden) */} + {m365Integration?.connected && ( +
+
+ + + + + + + + Fehlende Felder aus Microsoft 365 übernehmen + +
+ + {enrichMsg &&
{enrichMsg}
} + {enrichError &&
{enrichError}
} +
+ )} +
{profileMsg &&
{profileMsg}
} {profileError &&
{profileError}
}