From 8fc894c74c9eed7f7ca421369e9d4b50a7c26cb0 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sat, 14 Mar 2026 09:43:29 +0100 Subject: [PATCH] =?UTF-8?q?fix(core):=20Word-Export=20=E2=80=94=20jobTitle?= =?UTF-8?q?,=20Logo,=20Akzentfarbe=20und=20Firmenfu=C3=9Fzeile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jobtitel aus Profil statt experiences[0].area unter dem Namen - Platform-Logo aus Redis über Avatar einfügen - Dominante Akzentfarbe dynamisch aus Logo extrahieren - Firmen-Fußzeile aus Redis-Settings als DOCX-Footer Co-Authored-By: Claude Sonnet 4.6 --- .../expert-profile/profile-export.service.ts | 104 +++++++++++++++++- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/core-service/src/core/expert-profile/profile-export.service.ts b/packages/core-service/src/core/expert-profile/profile-export.service.ts index 45aa944..a518ead 100644 --- a/packages/core-service/src/core/expert-profile/profile-export.service.ts +++ b/packages/core-service/src/core/expert-profile/profile-export.service.ts @@ -20,6 +20,7 @@ import { ImageRun, HeadingLevel, ShadingType, + Footer, } from 'docx'; // ============================================================ @@ -555,6 +556,48 @@ export class ProfileExportService { const data = await this.expertProfileService.getExportData(userId) as ExportData; const profile = data.expertProfile; const fullName = `${data.firstName} ${data.lastName}`; + + // Branding + Firmendaten aus Redis laden + const [brandingRaw, companyRaw] = await Promise.all([ + this.redis.get('platform_branding_logo'), + this.redis.get('platform_company_settings'), + ]); + + let platformLogo: Buffer | null = null; + if (brandingRaw) { + try { + const branding = JSON.parse(brandingRaw) as { logo?: string }; + if (branding.logo) { + const base64 = branding.logo.replace(/^data:image\/\w+;base64,/, ''); + platformLogo = Buffer.from(base64, 'base64'); + } + } catch { /* ignore */ } + } + + // Dominante Farbe aus Logo extrahieren + if (platformLogo) { + const extracted = await this.extractDominantColor(platformLogo); + if (extracted) accentColor = extracted; + } + + let companyFooterText: string | null = null; + if (companyRaw) { + try { + const c = JSON.parse(companyRaw) as { + name?: string | null; street?: string | null; postalCode?: string | null; + city?: string | null; phone?: string | null; email?: string | null; website?: string | null; + }; + const parts: string[] = []; + if (c.name) parts.push(c.name); + const addr = [c.street, [c.postalCode, c.city].filter(Boolean).join(' ')].filter(Boolean).join(', '); + if (addr) parts.push(addr); + if (c.phone) parts.push(`Tel: ${c.phone}`); + if (c.email) parts.push(c.email); + if (c.website) parts.push(c.website); + if (parts.length) companyFooterText = parts.join(' | '); + } catch { /* ignore */ } + } + const accentHex = accentColor.replace('#', ''); const lightAccent = lightenColor(accentColor, 0.85).replace('#', ''); @@ -563,6 +606,31 @@ export class ProfileExportService { // --- Kontakt-Infos für linke Spalte --- const leftParagraphs: Paragraph[] = []; + // Platform-Logo + if (platformLogo) { + try { + const logoBuffer = await sharp(platformLogo) + .resize(120, 40, { fit: 'inside' }) + .png() + .toBuffer(); + leftParagraphs.push( + new Paragraph({ + children: [ + new ImageRun({ + data: logoBuffer, + transformation: { width: 120, height: 40 }, + type: 'png', + }), + ], + alignment: AlignmentType.CENTER, + spacing: { after: 120 }, + }), + ); + } catch (err) { + this.logger.warn('Logo für DOCX konnte nicht geladen werden', err); + } + } + // Avatar (rund zugeschnitten) let avatarImageRun: ImageRun | null = null; if (data.avatar) { @@ -605,13 +673,13 @@ export class ProfileExportService { }), ); - // Rolle - if (profile && profile.experiences.length > 0) { + // Rolle / Jobtitel + if (data.jobTitle) { leftParagraphs.push( new Paragraph({ children: [ new TextRun({ - text: profile.experiences[0].area, + text: data.jobTitle, size: 20, color: accentHex, }), @@ -854,6 +922,27 @@ export class ProfileExportService { ); } + // --- Fußzeile --- + const footerChildren: Paragraph[] = []; + if (companyFooterText) { + footerChildren.push( + new Paragraph({ + children: [ + new TextRun({ + text: companyFooterText, + size: 14, + color: '999999', + }), + ], + alignment: AlignmentType.CENTER, + border: { + top: { style: BorderStyle.SINGLE, size: 3, color: 'cccccc', space: 4 }, + }, + spacing: { before: 80 }, + }), + ); + } + // --- Dokument zusammenstellen --- const document = new Document({ styles: { @@ -869,9 +958,16 @@ export class ProfileExportService { { properties: { page: { - margin: { top: 720, bottom: 720, left: 720, right: 720 }, + margin: { top: 720, bottom: companyFooterText ? 800 : 720, left: 720, right: 720 }, }, }, + footers: companyFooterText + ? { + default: new Footer({ + children: footerChildren, + }), + } + : undefined, children: [layoutTable, ...sections], }, ],