From f5d83dc1c33e377aff03b31d221d6be8efc01e17 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sat, 14 Mar 2026 09:20:25 +0100 Subject: [PATCH] =?UTF-8?q?fix(core):=20PDF-Export=20=E2=80=94=20Footer-Le?= =?UTF-8?q?erseite=20behoben=20und=20Logo=20ueber=20Profilfoto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Footer: doc.page.margins.bottom temporaer auf 0 gesetzt beim Footer-Zeichnen verhindert PDFKit Auto-Pagination (footerTextY > maxY wuerde sonst eine leere zweite Seite erzeugen) - Logo: Platform-Branding-Logo (aus Redis platform_branding_logo) wird oben in der linken Spalte ueber dem Profilfoto gerendert (fit 180x50px) - Firmendaten + Branding parallel via Promise.all aus Redis geladen Co-Authored-By: Claude Sonnet 4.6 --- .../expert-profile/profile-export.service.ts | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 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 c74f544..8e3584c 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 @@ -192,12 +192,16 @@ export class ProfileExportService { const data = await this.expertProfileService.getExportData(userId) as ExportData; const profile = data.expertProfile; - // Firmendaten für PDF-Footer laden + // Firmendaten + Branding-Logo parallel laden let companyFooterText = ''; + let platformLogo: Buffer | null = null; try { - const raw = await this.redis.get('platform_company_settings'); - if (raw) { - const c = JSON.parse(raw) as Record; + const [companyRaw, brandingRaw] = await Promise.all([ + this.redis.get('platform_company_settings'), + this.redis.get('platform_branding_logo'), + ]); + if (companyRaw) { + const c = JSON.parse(companyRaw) as Record; const address = [c['street'], [c['postalCode'], c['city']].filter(Boolean).join(' ')].filter(Boolean).join(', '); companyFooterText = [ c['name'], @@ -207,7 +211,13 @@ export class ProfileExportService { c['website'], ].filter(Boolean).join(' | '); } - } catch { /* Firmendaten nicht verfügbar — kein Footer */ } + if (brandingRaw) { + const b = JSON.parse(brandingRaw) as Record; + if (typeof b['logo'] === 'string' && b['logo']) { + platformLogo = this.base64ToBuffer(b['logo']); + } + } + } catch { /* Einstellungen nicht verfügbar */ } const fullName = `${data.firstName} ${data.lastName}`; const { firstName, lastName } = data; @@ -236,6 +246,19 @@ export class ProfileExportService { // --- SEITE 1: Linke Spalte --- + // Platform-Logo (über dem Profilfoto) + if (platformLogo) { + try { + doc.image(platformLogo, leftColX, yLeft, { + fit: [leftColWidth, 50], + align: 'center', + }); + yLeft += 58; + } catch (err) { + this.logger.warn('Platform-Logo konnte nicht gerendert werden', err); + } + } + // Avatar (rundes Bild) if (data.avatar) { try { @@ -497,16 +520,20 @@ export class ProfileExportService { // --- FOOTER: Firmendaten auf jeder Seite --- if (companyFooterText) { - const pageHeight = doc.page.height; - const footerLineY = pageHeight - 32; - const footerTextY = pageHeight - 26; const range = doc.bufferedPageRange(); for (let p = range.start; p < range.start + range.count; p++) { doc.switchToPage(p); + // Bottom-Margin temporär auf 0, damit PDFKit keine Leerseite erzeugt + const origBottom = doc.page.margins.bottom; + doc.page.margins.bottom = 0; + const pageH = doc.page.height; + const footerLineY = pageH - 32; + const footerTextY = pageH - 26; doc.moveTo(40, footerLineY).lineTo(pageWidth - 40, footerLineY) .strokeColor('#cccccc').lineWidth(0.5).stroke(); doc.font('Helvetica').fontSize(7).fillColor('#999999'); doc.text(companyFooterText, 40, footerTextY, { width: pageWidth - 80, align: 'center', lineBreak: false }); + doc.page.margins.bottom = origBottom; } }