mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat: Erweiterte Profilfelder (analog O365) + Profilbild-Sync aus Microsoft 365
Neue Felder im Benutzerprofil (analog Microsoft 365 /me): - Stellenbezeichnung (jobTitle), Abteilung (department) - Firma (companyName), Standort (officeLocation) Changes: - Core: Prisma-Migration + neue Felder in User-Model, UpdateUserDto, findById/update/updateProfile - CRM: M365UserProfile-Interface + getM365Profile um neue Felder erweitert; neue Methode getM365Photo() lädt 96x96 JPEG als Base64 Data-URL; neuer Endpoint GET /crm/office365/photo - Frontend: AuthContext User-Interface, M365UserProfile-Typ, office365Api.getM365Photo() ProfilePage: Neues Formular-Fieldset "Organisation" mit 4 Feldern; manueller Sync-Button übernimmt auch Profilbild (immer überschreiben); useO365ProfileSync: Auto-Sync lädt Foto nur wenn noch kein INSIGHT-Avatar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
138742d385
commit
2348602fb0
11 changed files with 248 additions and 34 deletions
|
|
@ -34,6 +34,12 @@ model User {
|
||||||
postalCode String? @map("postal_code") @db.VarChar(10)
|
postalCode String? @map("postal_code") @db.VarChar(10)
|
||||||
city String? @map("city") @db.VarChar(100)
|
city String? @map("city") @db.VarChar(100)
|
||||||
|
|
||||||
|
// Organisation (analog Microsoft 365 /me Profil)
|
||||||
|
jobTitle String? @map("job_title") @db.VarChar(100)
|
||||||
|
department String? @map("department") @db.VarChar(100)
|
||||||
|
companyName String? @map("company_name") @db.VarChar(200)
|
||||||
|
officeLocation String? @map("office_location") @db.VarChar(200)
|
||||||
|
|
||||||
role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER
|
role String @default("USER") @db.VarChar(50) // PLATFORM_ADMIN, TENANT_ADMIN, USER
|
||||||
isActive Boolean @default(true) @map("is_active")
|
isActive Boolean @default(true) @map("is_active")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable: Organisations-Felder für Benutzerprofil (analog Microsoft 365 /me)
|
||||||
|
ALTER TABLE "users" ADD COLUMN "job_title" VARCHAR(100);
|
||||||
|
ALTER TABLE "users" ADD COLUMN "department" VARCHAR(100);
|
||||||
|
ALTER TABLE "users" ADD COLUMN "company_name" VARCHAR(200);
|
||||||
|
ALTER TABLE "users" ADD COLUMN "office_location" VARCHAR(200);
|
||||||
|
|
@ -77,4 +77,33 @@ export class UpdateUserDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
city?: string | null;
|
city?: string | null;
|
||||||
|
|
||||||
|
// --- Organisation (analog Microsoft 365 /me) ---
|
||||||
|
@ApiProperty({ example: 'Senior Developer', required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o: UpdateUserDto) => o.jobTitle !== null)
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
jobTitle?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Engineering', required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o: UpdateUserDto) => o.department !== null)
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
department?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Acme GmbH', required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o: UpdateUserDto) => o.companyName !== null)
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
companyName?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Berlin Office', required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateIf((o: UpdateUserDto) => o.officeLocation !== null)
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
officeLocation?: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,10 @@ export class UsersService {
|
||||||
street: user.street,
|
street: user.street,
|
||||||
postalCode: user.postalCode,
|
postalCode: user.postalCode,
|
||||||
city: user.city,
|
city: user.city,
|
||||||
|
jobTitle: user.jobTitle,
|
||||||
|
department: user.department,
|
||||||
|
companyName: user.companyName,
|
||||||
|
officeLocation: user.officeLocation,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
twoFactorEnabled: user.twoFactorEnabled,
|
twoFactorEnabled: user.twoFactorEnabled,
|
||||||
|
|
@ -136,6 +140,10 @@ export class UsersService {
|
||||||
...(dto.street !== undefined && { street: dto.street }),
|
...(dto.street !== undefined && { street: dto.street }),
|
||||||
...(dto.postalCode !== undefined && { postalCode: dto.postalCode }),
|
...(dto.postalCode !== undefined && { postalCode: dto.postalCode }),
|
||||||
...(dto.city !== undefined && { city: dto.city }),
|
...(dto.city !== undefined && { city: dto.city }),
|
||||||
|
...(dto.jobTitle !== undefined && { jobTitle: dto.jobTitle }),
|
||||||
|
...(dto.department !== undefined && { department: dto.department }),
|
||||||
|
...(dto.companyName !== undefined && { companyName: dto.companyName }),
|
||||||
|
...(dto.officeLocation !== undefined && { officeLocation: dto.officeLocation }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -150,6 +158,10 @@ export class UsersService {
|
||||||
street: updated.street,
|
street: updated.street,
|
||||||
postalCode: updated.postalCode,
|
postalCode: updated.postalCode,
|
||||||
city: updated.city,
|
city: updated.city,
|
||||||
|
jobTitle: updated.jobTitle,
|
||||||
|
department: updated.department,
|
||||||
|
companyName: updated.companyName,
|
||||||
|
officeLocation: updated.officeLocation,
|
||||||
role: updated.role,
|
role: updated.role,
|
||||||
isActive: updated.isActive,
|
isActive: updated.isActive,
|
||||||
};
|
};
|
||||||
|
|
@ -175,6 +187,10 @@ export class UsersService {
|
||||||
...(dto.street !== undefined && { street: dto.street }),
|
...(dto.street !== undefined && { street: dto.street }),
|
||||||
...(dto.postalCode !== undefined && { postalCode: dto.postalCode }),
|
...(dto.postalCode !== undefined && { postalCode: dto.postalCode }),
|
||||||
...(dto.city !== undefined && { city: dto.city }),
|
...(dto.city !== undefined && { city: dto.city }),
|
||||||
|
...(dto.jobTitle !== undefined && { jobTitle: dto.jobTitle }),
|
||||||
|
...(dto.department !== undefined && { department: dto.department }),
|
||||||
|
...(dto.companyName !== undefined && { companyName: dto.companyName }),
|
||||||
|
...(dto.officeLocation !== undefined && { officeLocation: dto.officeLocation }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -189,6 +205,10 @@ export class UsersService {
|
||||||
street: updated.street,
|
street: updated.street,
|
||||||
postalCode: updated.postalCode,
|
postalCode: updated.postalCode,
|
||||||
city: updated.city,
|
city: updated.city,
|
||||||
|
jobTitle: updated.jobTitle,
|
||||||
|
department: updated.department,
|
||||||
|
companyName: updated.companyName,
|
||||||
|
officeLocation: updated.officeLocation,
|
||||||
role: updated.role,
|
role: updated.role,
|
||||||
isActive: updated.isActive,
|
isActive: updated.isActive,
|
||||||
twoFactorEnabled: updated.twoFactorEnabled,
|
twoFactorEnabled: updated.twoFactorEnabled,
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,9 @@ export interface M365UserProfile {
|
||||||
streetAddress: string | null;
|
streetAddress: string | null;
|
||||||
postalCode: string | null;
|
postalCode: string | null;
|
||||||
jobTitle: string | null;
|
jobTitle: string | null;
|
||||||
|
department: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
officeLocation: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface M365MailFolder {
|
export interface M365MailFolder {
|
||||||
|
|
@ -623,9 +626,12 @@ export class GraphService {
|
||||||
streetAddress?: string | null;
|
streetAddress?: string | null;
|
||||||
postalCode?: string | null;
|
postalCode?: string | null;
|
||||||
jobTitle?: string | null;
|
jobTitle?: string | null;
|
||||||
|
department?: string | null;
|
||||||
|
companyName?: string | null;
|
||||||
|
officeLocation?: string | null;
|
||||||
}>(accessToken, '/me', {
|
}>(accessToken, '/me', {
|
||||||
$select:
|
$select:
|
||||||
'givenName,surname,displayName,mobilePhone,businessPhones,city,streetAddress,postalCode,jobTitle',
|
'givenName,surname,displayName,mobilePhone,businessPhones,city,streetAddress,postalCode,jobTitle,department,companyName,officeLocation',
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -638,9 +644,48 @@ export class GraphService {
|
||||||
streetAddress: data.streetAddress ?? null,
|
streetAddress: data.streetAddress ?? null,
|
||||||
postalCode: data.postalCode ?? null,
|
postalCode: data.postalCode ?? null,
|
||||||
jobTitle: data.jobTitle ?? null,
|
jobTitle: data.jobTitle ?? null,
|
||||||
|
department: data.department ?? null,
|
||||||
|
companyName: data.companyName ?? null,
|
||||||
|
officeLocation: data.officeLocation ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft-365-Profilbild laden (96x96 JPEG).
|
||||||
|
* Gibt Base64 Data-URL zurück, oder null wenn kein Foto vorhanden (404).
|
||||||
|
*/
|
||||||
|
async getM365Photo(userJwt: string): Promise<string | null> {
|
||||||
|
const accessToken = await this.getM365Token(userJwt);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(
|
||||||
|
`${GRAPH_BASE}/me/photos/96x96/$value`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp.status === 404 || resp.status === 400) {
|
||||||
|
this.logger.debug('Graph: Kein M365-Profilbild vorhanden (404/400)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.logger.warn(`Graph: Profilbild-Fehler ${resp.status} — wird ignoriert`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await resp.arrayBuffer();
|
||||||
|
const base64 = Buffer.from(arrayBuffer).toString('base64');
|
||||||
|
return `data:image/jpeg;base64,${base64}`;
|
||||||
|
} catch (err) {
|
||||||
|
// Foto ist optional — Fehler niemals an den User propagieren
|
||||||
|
this.logger.warn(`Graph: getM365Photo Fehler: ${(err as Error).message}`);
|
||||||
|
return 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,
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,14 @@ export class Office365Controller {
|
||||||
return { success: true, data: profile };
|
return { success: true, data: profile };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Microsoft-365-Profilbild abrufen (96x96 JPEG als Base64 Data-URL) */
|
||||||
|
@Get('photo')
|
||||||
|
async getPhoto(@Req() req: Request & { user: JwtUser }) {
|
||||||
|
const jwt = (req.headers.authorization ?? '').replace('Bearer ', '');
|
||||||
|
const photoBase64 = await this.graphService.getM365Photo(jwt);
|
||||||
|
return { success: true, data: { photoBase64 } };
|
||||||
|
}
|
||||||
|
|
||||||
@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 ', '');
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ interface User {
|
||||||
street?: string | null;
|
street?: string | null;
|
||||||
postalCode?: string | null;
|
postalCode?: string | null;
|
||||||
city?: string | null;
|
city?: string | null;
|
||||||
|
// Organisation (analog Microsoft 365 /me)
|
||||||
|
jobTitle?: string | null;
|
||||||
|
department?: string | null;
|
||||||
|
companyName?: string | null;
|
||||||
|
officeLocation?: string | null;
|
||||||
role: string;
|
role: string;
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -906,4 +906,9 @@ export const office365Api = {
|
||||||
api
|
api
|
||||||
.get<{ success: boolean; data: M365UserProfile }>('/crm/office365/profile')
|
.get<{ success: boolean; data: M365UserProfile }>('/crm/office365/profile')
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
getM365Photo: () =>
|
||||||
|
api
|
||||||
|
.get<{ success: boolean; data: { photoBase64: string | null } }>('/crm/office365/photo')
|
||||||
|
.then((r) => r.data),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1065,6 +1065,9 @@ export interface M365UserProfile {
|
||||||
streetAddress: string | null;
|
streetAddress: string | null;
|
||||||
postalCode: string | null;
|
postalCode: string | null;
|
||||||
jobTitle: string | null;
|
jobTitle: string | null;
|
||||||
|
department: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
officeLocation: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Minimaler CRM-Kontakt für E-Mail-Lookup */
|
/** Minimaler CRM-Kontakt für E-Mail-Lookup */
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useIntegrations } from '../crm/hooks';
|
||||||
import { office365Api } from '../crm/api';
|
import { office365Api } from '../crm/api';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
|
|
||||||
|
|
||||||
const SESSION_KEY = 'o365_profile_synced';
|
const SESSION_KEY = 'o365_profile_synced';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,11 +34,17 @@ export function useO365ProfileSync(): void {
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const result = await office365Api.getM365Profile();
|
// Fetch profile + photo in parallel
|
||||||
const p = result.data;
|
const [profileResult, photoResult] = await Promise.all([
|
||||||
|
office365Api.getM365Profile(),
|
||||||
|
office365Api.getM365Photo(),
|
||||||
|
]);
|
||||||
|
const p = profileResult.data;
|
||||||
|
|
||||||
// Overwrite all fields where O365 has a non-null value
|
// Build patch with all fields where O365 has a non-null value
|
||||||
const patch: Record<string, string | null> = {};
|
const patch: Record<string, string | null> = {};
|
||||||
|
|
||||||
|
// Kontaktfelder
|
||||||
if (p.givenName) patch.firstName = p.givenName;
|
if (p.givenName) patch.firstName = p.givenName;
|
||||||
if (p.surname) patch.lastName = p.surname;
|
if (p.surname) patch.lastName = p.surname;
|
||||||
if (p.businessPhones?.[0]) patch.phone = p.businessPhones[0];
|
if (p.businessPhones?.[0]) patch.phone = p.businessPhones[0];
|
||||||
|
|
@ -46,6 +53,17 @@ export function useO365ProfileSync(): void {
|
||||||
if (p.streetAddress) patch.street = p.streetAddress;
|
if (p.streetAddress) patch.street = p.streetAddress;
|
||||||
if (p.postalCode) patch.postalCode = p.postalCode;
|
if (p.postalCode) patch.postalCode = p.postalCode;
|
||||||
|
|
||||||
|
// Organisationsfelder
|
||||||
|
if (p.jobTitle) patch.jobTitle = p.jobTitle;
|
||||||
|
if (p.department) patch.department = p.department;
|
||||||
|
if (p.companyName) patch.companyName = p.companyName;
|
||||||
|
if (p.officeLocation) patch.officeLocation = p.officeLocation;
|
||||||
|
|
||||||
|
// Profilbild — nur wenn noch kein INSIGHT-Avatar gesetzt
|
||||||
|
if (!user.avatar && photoResult.data.photoBase64) {
|
||||||
|
patch.avatar = photoResult.data.photoBase64;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(patch).length > 0) {
|
if (Object.keys(patch).length > 0) {
|
||||||
await api.patch('/users/me', patch);
|
await api.patch('/users/me', patch);
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ export function ProfilePage() {
|
||||||
const [street, setStreet] = useState(user?.street ?? '');
|
const [street, setStreet] = useState(user?.street ?? '');
|
||||||
const [postalCode, setPostalCode] = useState(user?.postalCode ?? '');
|
const [postalCode, setPostalCode] = useState(user?.postalCode ?? '');
|
||||||
const [city, setCity] = useState(user?.city ?? '');
|
const [city, setCity] = useState(user?.city ?? '');
|
||||||
|
|
||||||
|
// --- Organisation ---
|
||||||
|
const [jobTitle, setJobTitle] = useState(user?.jobTitle ?? '');
|
||||||
|
const [department, setDepartment] = useState(user?.department ?? '');
|
||||||
|
const [companyName, setCompanyName] = useState(user?.companyName ?? '');
|
||||||
|
const [officeLocation, setOfficeLocation] = useState(user?.officeLocation ?? '');
|
||||||
const [profileMsg, setProfileMsg] = useState('');
|
const [profileMsg, setProfileMsg] = useState('');
|
||||||
const [profileError, setProfileError] = useState('');
|
const [profileError, setProfileError] = useState('');
|
||||||
const [profileLoading, setProfileLoading] = useState(false);
|
const [profileLoading, setProfileLoading] = useState(false);
|
||||||
|
|
@ -86,6 +92,16 @@ export function ProfilePage() {
|
||||||
}
|
}
|
||||||
}, [user?.phone, user?.mobile, user?.street, user?.postalCode, user?.city]);
|
}, [user?.phone, user?.mobile, user?.street, user?.postalCode, user?.city]);
|
||||||
|
|
||||||
|
// Organisationsdaten mit Context-User synchronisieren
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
setJobTitle(user.jobTitle ?? '');
|
||||||
|
setDepartment(user.department ?? '');
|
||||||
|
setCompanyName(user.companyName ?? '');
|
||||||
|
setOfficeLocation(user.officeLocation ?? '');
|
||||||
|
}
|
||||||
|
}, [user?.jobTitle, user?.department, user?.companyName, user?.officeLocation]);
|
||||||
|
|
||||||
// === Handler: Profilbild hochladen ===
|
// === Handler: Profilbild hochladen ===
|
||||||
const handleAvatarChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleAvatarChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
|
|
@ -161,6 +177,10 @@ export function ProfilePage() {
|
||||||
street: street || null,
|
street: street || null,
|
||||||
postalCode: postalCode || null,
|
postalCode: postalCode || null,
|
||||||
city: city || null,
|
city: city || null,
|
||||||
|
jobTitle: jobTitle || null,
|
||||||
|
department: department || null,
|
||||||
|
companyName: companyName || null,
|
||||||
|
officeLocation: officeLocation || null,
|
||||||
});
|
});
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
setProfileMsg('Profil erfolgreich aktualisiert');
|
setProfileMsg('Profil erfolgreich aktualisiert');
|
||||||
|
|
@ -181,39 +201,36 @@ export function ProfilePage() {
|
||||||
setEnrichLoading(true);
|
setEnrichLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await office365Api.getM365Profile();
|
// Profil + Foto parallel laden
|
||||||
const p = result.data;
|
const [profileResult, photoResult] = await Promise.all([
|
||||||
|
office365Api.getM365Profile(),
|
||||||
|
office365Api.getM365Photo(),
|
||||||
|
]);
|
||||||
|
const p = profileResult.data;
|
||||||
|
|
||||||
const updated: string[] = [];
|
const updated: string[] = [];
|
||||||
|
|
||||||
// Always overwrite with O365 values where O365 has data
|
// Kontaktfelder — immer überschreiben wo O365 Daten hat
|
||||||
if (p.givenName) {
|
if (p.givenName) { setFirstName(p.givenName); updated.push('Vorname'); }
|
||||||
setFirstName(p.givenName);
|
if (p.surname) { setLastName(p.surname); updated.push('Nachname'); }
|
||||||
updated.push('Vorname');
|
if (p.businessPhones?.[0]) { setPhone(p.businessPhones[0]); updated.push('Telefon'); }
|
||||||
}
|
if (p.mobilePhone) { setMobile(p.mobilePhone); updated.push('Mobil'); }
|
||||||
if (p.surname) {
|
if (p.city) { setCity(p.city); updated.push('Ort'); }
|
||||||
setLastName(p.surname);
|
if (p.streetAddress) { setStreet(p.streetAddress); updated.push('Straße'); }
|
||||||
updated.push('Nachname');
|
if (p.postalCode) { setPostalCode(p.postalCode); updated.push('PLZ'); }
|
||||||
}
|
|
||||||
if (p.businessPhones?.[0]) {
|
// Organisationsfelder
|
||||||
setPhone(p.businessPhones[0]);
|
if (p.jobTitle) { setJobTitle(p.jobTitle); updated.push('Position'); }
|
||||||
updated.push('Telefon');
|
if (p.department) { setDepartment(p.department); updated.push('Abteilung'); }
|
||||||
}
|
if (p.companyName) { setCompanyName(p.companyName); updated.push('Firma'); }
|
||||||
if (p.mobilePhone) {
|
if (p.officeLocation) { setOfficeLocation(p.officeLocation); updated.push('Standort'); }
|
||||||
setMobile(p.mobilePhone);
|
|
||||||
updated.push('Mobil');
|
// Profilbild übernehmen (manuelle Sync überschreibt immer)
|
||||||
}
|
if (photoResult.data.photoBase64) {
|
||||||
if (p.city) {
|
await api.patch('/users/me', { avatar: photoResult.data.photoBase64 });
|
||||||
setCity(p.city);
|
setAvatar(photoResult.data.photoBase64);
|
||||||
updated.push('Ort');
|
await refreshUser();
|
||||||
}
|
updated.push('Profilbild');
|
||||||
if (p.streetAddress) {
|
|
||||||
setStreet(p.streetAddress);
|
|
||||||
updated.push('Straße');
|
|
||||||
}
|
|
||||||
if (p.postalCode) {
|
|
||||||
setPostalCode(p.postalCode);
|
|
||||||
updated.push('PLZ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated.length > 0) {
|
if (updated.length > 0) {
|
||||||
|
|
@ -641,6 +658,59 @@ export function ProfilePage() {
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{/* Organisation */}
|
||||||
|
<fieldset className={styles.fieldGroup}>
|
||||||
|
<legend className={styles.fieldGroupLegend}>Organisation</legend>
|
||||||
|
<div className={styles.fieldRow}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="jobTitle">Stellenbezeichnung</label>
|
||||||
|
<input
|
||||||
|
id="jobTitle"
|
||||||
|
type="text"
|
||||||
|
value={jobTitle}
|
||||||
|
onChange={(e) => setJobTitle(e.target.value)}
|
||||||
|
maxLength={100}
|
||||||
|
placeholder="Senior Developer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="department">Abteilung</label>
|
||||||
|
<input
|
||||||
|
id="department"
|
||||||
|
type="text"
|
||||||
|
value={department}
|
||||||
|
onChange={(e) => setDepartment(e.target.value)}
|
||||||
|
maxLength={100}
|
||||||
|
placeholder="Engineering"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.fieldRow}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="companyName">Firma</label>
|
||||||
|
<input
|
||||||
|
id="companyName"
|
||||||
|
type="text"
|
||||||
|
value={companyName}
|
||||||
|
onChange={(e) => setCompanyName(e.target.value)}
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="Acme GmbH"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="officeLocation">Standort</label>
|
||||||
|
<input
|
||||||
|
id="officeLocation"
|
||||||
|
type="text"
|
||||||
|
value={officeLocation}
|
||||||
|
onChange={(e) => setOfficeLocation(e.target.value)}
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="Berlin Office"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue