fix(core): PDF-Export — Footer-Leerseite behoben und Logo ueber Profilfoto

- 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 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-14 09:20:25 +01:00
parent 46ced98bf4
commit f5d83dc1c3

View file

@ -192,12 +192,16 @@ export class ProfileExportService {
const data = await this.expertProfileService.getExportData(userId) as ExportData; const data = await this.expertProfileService.getExportData(userId) as ExportData;
const profile = data.expertProfile; const profile = data.expertProfile;
// Firmendaten für PDF-Footer laden // Firmendaten + Branding-Logo parallel laden
let companyFooterText = ''; let companyFooterText = '';
let platformLogo: Buffer | null = null;
try { try {
const raw = await this.redis.get('platform_company_settings'); const [companyRaw, brandingRaw] = await Promise.all([
if (raw) { this.redis.get('platform_company_settings'),
const c = JSON.parse(raw) as Record<string, unknown>; this.redis.get('platform_branding_logo'),
]);
if (companyRaw) {
const c = JSON.parse(companyRaw) as Record<string, unknown>;
const address = [c['street'], [c['postalCode'], c['city']].filter(Boolean).join(' ')].filter(Boolean).join(', '); const address = [c['street'], [c['postalCode'], c['city']].filter(Boolean).join(' ')].filter(Boolean).join(', ');
companyFooterText = [ companyFooterText = [
c['name'], c['name'],
@ -207,7 +211,13 @@ export class ProfileExportService {
c['website'], c['website'],
].filter(Boolean).join(' | '); ].filter(Boolean).join(' | ');
} }
} catch { /* Firmendaten nicht verfügbar — kein Footer */ } if (brandingRaw) {
const b = JSON.parse(brandingRaw) as Record<string, unknown>;
if (typeof b['logo'] === 'string' && b['logo']) {
platformLogo = this.base64ToBuffer(b['logo']);
}
}
} catch { /* Einstellungen nicht verfügbar */ }
const fullName = `${data.firstName} ${data.lastName}`; const fullName = `${data.firstName} ${data.lastName}`;
const { firstName, lastName } = data; const { firstName, lastName } = data;
@ -236,6 +246,19 @@ export class ProfileExportService {
// --- SEITE 1: Linke Spalte --- // --- 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) // Avatar (rundes Bild)
if (data.avatar) { if (data.avatar) {
try { try {
@ -497,16 +520,20 @@ export class ProfileExportService {
// --- FOOTER: Firmendaten auf jeder Seite --- // --- FOOTER: Firmendaten auf jeder Seite ---
if (companyFooterText) { if (companyFooterText) {
const pageHeight = doc.page.height;
const footerLineY = pageHeight - 32;
const footerTextY = pageHeight - 26;
const range = doc.bufferedPageRange(); const range = doc.bufferedPageRange();
for (let p = range.start; p < range.start + range.count; p++) { for (let p = range.start; p < range.start + range.count; p++) {
doc.switchToPage(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) doc.moveTo(40, footerLineY).lineTo(pageWidth - 40, footerLineY)
.strokeColor('#cccccc').lineWidth(0.5).stroke(); .strokeColor('#cccccc').lineWidth(0.5).stroke();
doc.font('Helvetica').fontSize(7).fillColor('#999999'); doc.font('Helvetica').fontSize(7).fillColor('#999999');
doc.text(companyFooterText, 40, footerTextY, { width: pageWidth - 80, align: 'center', lineBreak: false }); doc.text(companyFooterText, 40, footerTextY, { width: pageWidth - 80, align: 'center', lineBreak: false });
doc.page.margins.bottom = origBottom;
} }
} }