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 */} -