mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:36:38 +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;
|
||||
}
|
||||
|
||||
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<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) */
|
||||
async getMailsByFolder(
|
||||
userJwt: string,
|
||||
|
|
|
|||
|
|
@ -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 ', '');
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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('');
|
||||
|
|
@ -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 --- */}
|
||||
<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>}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue