mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
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>
This commit is contained in:
parent
403c581e57
commit
347a6ca418
6 changed files with 223 additions and 1 deletions
|
|
@ -70,6 +70,18 @@ export interface M365Contact {
|
||||||
companyName: string | null;
|
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 {
|
export interface M365MailFolder {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
@ -597,6 +609,38 @@ export class GraphService {
|
||||||
this.logger.debug(`Graph: Aufgabe ${taskId} als erledigt markiert`);
|
this.logger.debug(`Graph: Aufgabe ${taskId} als erledigt markiert`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Microsoft-365-Benutzerprofil laden (für Profilanreicherung) */
|
||||||
|
async getM365Profile(userJwt: string): Promise<M365UserProfile> {
|
||||||
|
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) */
|
/** E-Mails in einem bestimmten Ordner (mit optionalem Tages-Filter) */
|
||||||
async getMailsByFolder(
|
async getMailsByFolder(
|
||||||
userJwt: string,
|
userJwt: string,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,14 @@ export class Office365Controller {
|
||||||
return { success: true };
|
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')
|
@Get('folders')
|
||||||
async getMailFolders(@Req() req: Request & { user: JwtUser }) {
|
async getMailFolders(@Req() req: Request & { user: JwtUser }) {
|
||||||
const jwt = (req.headers.authorization ?? '').replace('Bearer ', '');
|
const jwt = (req.headers.authorization ?? '').replace('Bearer ', '');
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ import type {
|
||||||
CrmOpenTask,
|
CrmOpenTask,
|
||||||
M365Contact,
|
M365Contact,
|
||||||
M365MailFolder,
|
M365MailFolder,
|
||||||
|
M365UserProfile,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// --- Contacts ---
|
// --- Contacts ---
|
||||||
|
|
@ -900,4 +901,9 @@ export const office365Api = {
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getM365Profile: () =>
|
||||||
|
api
|
||||||
|
.get<{ success: boolean; data: M365UserProfile }>('/crm/office365/profile')
|
||||||
|
.then((r) => r.data),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1054,6 +1054,19 @@ export interface M365MailFolder {
|
||||||
wellKnownName?: string | null; // optional — nicht von allen Exchange-Tenants unterstützt
|
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 */
|
/** Minimaler CRM-Kontakt für E-Mail-Lookup */
|
||||||
export interface CrmContactLookup {
|
export interface CrmContactLookup {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -398,3 +398,66 @@
|
||||||
white-space: nowrap;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { UserAvatar } from '../components/UserAvatar';
|
||||||
import { resizeImageToBase64 } from '../utils/imageUtils';
|
import { resizeImageToBase64 } from '../utils/imageUtils';
|
||||||
import { ExpertProfileTab } from './ExpertProfileTab';
|
import { ExpertProfileTab } from './ExpertProfileTab';
|
||||||
import { useIntegrations, useDisconnectM365 } from '../crm/hooks';
|
import { useIntegrations, useDisconnectM365 } from '../crm/hooks';
|
||||||
import { integrationsApi } from '../crm/api';
|
import { integrationsApi, office365Api } from '../crm/api';
|
||||||
import styles from './ProfilePage.module.css';
|
import styles from './ProfilePage.module.css';
|
||||||
|
|
||||||
type ProfileTab = 'personal' | 'expert' | 'password' | 'integrations';
|
type ProfileTab = 'personal' | 'expert' | 'password' | 'integrations';
|
||||||
|
|
@ -33,6 +33,11 @@ export function ProfilePage() {
|
||||||
const [avatarLoading, setAvatarLoading] = useState(false);
|
const [avatarLoading, setAvatarLoading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// --- O365-Profilanreicherung ---
|
||||||
|
const [enrichLoading, setEnrichLoading] = useState(false);
|
||||||
|
const [enrichMsg, setEnrichMsg] = useState('');
|
||||||
|
const [enrichError, setEnrichError] = useState('');
|
||||||
|
|
||||||
// --- Passwort ändern ---
|
// --- Passwort ändern ---
|
||||||
const [currentPassword, setCurrentPassword] = useState('');
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = 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 ===
|
// === Handler: Passwort ändern ===
|
||||||
const handlePasswordChange = async (e: FormEvent) => {
|
const handlePasswordChange = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -432,6 +492,34 @@ export function ProfilePage() {
|
||||||
|
|
||||||
{/* --- Rechte Spalte: Formular --- */}
|
{/* --- Rechte Spalte: Formular --- */}
|
||||||
<div className={styles.formColumn}>
|
<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}>
|
<form onSubmit={handleProfileUpdate} className={styles.form}>
|
||||||
{profileMsg && <div className={styles.success}>{profileMsg}</div>}
|
{profileMsg && <div className={styles.success}>{profileMsg}</div>}
|
||||||
{profileError && <div className={styles.error}>{profileError}</div>}
|
{profileError && <div className={styles.error}>{profileError}</div>}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue