From 90a0388b2201a680cb2b744c40341aa9dd532bcb Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Sat, 14 Mar 2026 10:59:04 +0100 Subject: [PATCH] =?UTF-8?q?feat(core+frontend):=20Anh=C3=A4nge=20im=20Expo?= =?UTF-8?q?rt=20+=20neues=203-Spalten-Layout=20im=20Profil-Reiter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PDF-Export: alle Anhänge als zusätzliche Seiten (Bilder als Vorschau, andere mit Hinweis) - DOCX-Export: Bild-Anhänge als zusätzliche Sections (je eine Seite pro Bild) - ExpertProfileTab: Skills/Sprachen/Erfahrungen nebeneinander (3 Spalten) - ExpertProfileTab: Zertifizierungen und Profilanlagen nebeneinander (2 Spalten) - threeColumnRow CSS-Klasse hinzugefügt (responsive auf 1 Spalte bei <900px) Co-Authored-By: Claude Sonnet 4.6 --- .../expert-profile/profile-export.service.ts | 97 +++++++++++++++++-- .../src/profile/ExpertProfileTab.module.css | 9 +- .../frontend/src/profile/ExpertProfileTab.tsx | 13 ++- 3 files changed, 103 insertions(+), 16 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 ce08b38..1512c7b 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 @@ -507,22 +507,29 @@ export class ProfileExportService { } } - // --- ANHÄNGE (Bild-Anhänge als zusätzliche Seiten) --- + // --- ANHÄNGE als zusätzliche Seiten --- const attachments = profile?.attachments ?? []; for (const att of attachments) { - if (!att.mimetype.startsWith('image/')) continue; doc.addPage(); let yAtt = 40; yAtt = this.pdfSectionTitle(doc, `ANLAGE: ${att.filename}`, 40, yAtt, pageWidth - 80, accentColor); - try { - const imgBuffer = this.base64ToBuffer(att.data); - const maxW = pageWidth - 80; - const maxH = 680; - doc.image(imgBuffer, 40, yAtt, { fit: [maxW, maxH], align: 'center' }); - } catch (err) { - this.logger.warn(`Anhang-Bild konnte nicht gerendert werden: ${att.filename}`, err); + if (att.mimetype.startsWith('image/')) { + try { + const imgBuffer = this.base64ToBuffer(att.data); + const maxW = pageWidth - 80; + const maxH = 680; + doc.image(imgBuffer, 40, yAtt, { fit: [maxW, maxH], align: 'center' }); + } catch (err) { + this.logger.warn(`Anhang-Bild konnte nicht gerendert werden: ${att.filename}`, err); + doc.font('Helvetica').fontSize(9).fillColor('#777777'); + doc.text(`(Bild konnte nicht dargestellt werden: ${att.filename})`, 40, yAtt, { width: pageWidth - 80 }); + } + } else { + // Nicht-Bild-Anhänge: Dateiname und Hinweis doc.font('Helvetica').fontSize(9).fillColor('#777777'); - doc.text(`(Bild konnte nicht dargestellt werden: ${att.filename})`, 40, yAtt, { width: pageWidth - 80 }); + doc.text(`Typ: ${att.mimetype}`, 40, yAtt, { width: pageWidth - 80 }); + doc.moveDown(0.4); + doc.text(`(Datei-Anhang – nicht als Vorschau darstellbar)`, 40, doc.y, { width: pageWidth - 80 }); } } @@ -944,6 +951,75 @@ export class ProfileExportService { ); } + // --- Anhänge als zusätzliche Sections (je Bild eine Seite) --- + type DocxSection = { + properties: { page: { margin: { top: number; bottom: number; left: number; right: number } } }; + footers?: { default: Footer }; + children: Paragraph[]; + }; + + const attachmentSections: DocxSection[] = []; + const attachments = profile?.attachments ?? []; + for (const att of attachments) { + if (!att.mimetype.startsWith('image/')) continue; + try { + const imgBuffer = this.base64ToBuffer(att.data); + const { data: resizedBuffer, info: imgInfo } = await sharp(imgBuffer) + .resize(600, 760, { fit: 'inside', withoutEnlargement: true }) + .png() + .toBuffer({ resolveWithObject: true }); + + const attSection: DocxSection = { + properties: { + page: { + margin: { top: 720, bottom: companyFooterText ? 800 : 720, left: 720, right: 720 }, + }, + }, + footers: companyFooterText + ? { default: new Footer({ children: footerChildren }) } + : undefined, + children: [ + this.docxSectionHeading(`ANLAGE: ${att.filename}`, accentHex), + new Paragraph({ + children: [ + new ImageRun({ + data: resizedBuffer, + transformation: { width: imgInfo.width, height: imgInfo.height }, + type: 'png', + }), + ], + spacing: { before: 200 }, + }), + ], + }; + attachmentSections.push(attSection); + } catch (err) { + this.logger.warn(`DOCX-Anhang konnte nicht eingebettet werden: ${att.filename}`, err); + attachmentSections.push({ + properties: { + page: { + margin: { top: 720, bottom: companyFooterText ? 800 : 720, left: 720, right: 720 }, + }, + }, + footers: companyFooterText + ? { default: new Footer({ children: footerChildren }) } + : undefined, + children: [ + this.docxSectionHeading(`ANLAGE: ${att.filename}`, accentHex), + new Paragraph({ + children: [ + new TextRun({ + text: `(Bild konnte nicht eingebettet werden: ${att.filename})`, + size: 18, + color: '999999', + }), + ], + }), + ], + }); + } + } + // --- Dokument zusammenstellen --- const document = new Document({ styles: { @@ -971,6 +1047,7 @@ export class ProfileExportService { : undefined, children: [layoutTable, ...sections], }, + ...attachmentSections, ], }); diff --git a/packages/frontend/src/profile/ExpertProfileTab.module.css b/packages/frontend/src/profile/ExpertProfileTab.module.css index f286aed..cb40999 100644 --- a/packages/frontend/src/profile/ExpertProfileTab.module.css +++ b/packages/frontend/src/profile/ExpertProfileTab.module.css @@ -16,8 +16,15 @@ gap: 1.5rem; } +.threeColumnRow { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 1.5rem; +} + @media (max-width: 900px) { - .twoColumnRow { + .twoColumnRow, + .threeColumnRow { grid-template-columns: 1fr; } } diff --git a/packages/frontend/src/profile/ExpertProfileTab.tsx b/packages/frontend/src/profile/ExpertProfileTab.tsx index e9e5e93..ba47de4 100644 --- a/packages/frontend/src/profile/ExpertProfileTab.tsx +++ b/packages/frontend/src/profile/ExpertProfileTab.tsx @@ -147,15 +147,18 @@ export function ExpertProfileTab({ apiBase = '/expert-profile/me' }: ExpertProfi - {/* Skills + Sprachen nebeneinander */} -
+ {/* Skills + Sprachen + Erfahrungen nebeneinander */} +
+
- - - + {/* Zertifizierungen + Profilanlagen nebeneinander */} +
+ + +
); }