diff --git a/Summarize.md b/Summarize.md index 04a63bf..055ccb8 100644 --- a/Summarize.md +++ b/Summarize.md @@ -6,6 +6,27 @@ --- +### Aenderungen 2026-03-13 (18): Experten-Profil PDF-Export — 3 weitere Korrekturen + +#### Backend (Core Service) +- `expert-profile/expert-profile.service.ts` — `getExportData()`: Anhänge jetzt inkludiert (`attachments: { orderBy: { createdAt: 'asc' } }`) +- `expert-profile/profile-export.service.ts` — PDF-Export: + - Neues `ExportAttachment` Interface + `attachments[]` in `ExportData` + - Tasks: Bullet `•` wird nur noch hinzugefügt, wenn Original-Zeile bereits ein Aufzählungszeichen (`•`, `\u2022`) oder Nummerierung (`1.`) enthält — kein spurious Bullet für Plaintext-Zeilen + - Zertifizierungen: Schriftgröße reduziert (Titel 10pt→9pt, Aussteller 9pt→8pt, Abstände angepasst) — passt jetzt zur linken Spalte + - Zertifizierungen: Timeline-Linienlänge 40→32px + - Anhänge: Bild-Anhänge (image/jpeg, image/png, etc.) werden als zusätzliche Seiten ans PDF angehängt (Überschrift "ANLAGE: dateiname" + Bild; PDFs + andere Formate werden übersprungen) +- `expert-profile/profile-export.service.ts` — DOCX-Export: + - Tasks: Gleiche Bullet-Fix-Logik (nur Bullet wenn Original-Zeile Bullet/Nummer hat) + +#### TypeScript +- `npx tsc --noEmit` in packages/core-service: 0 Fehler + +#### Deployment-Hinweis (Schritt 18) +- Rebuild + Restart: nur core-service + +--- + ### Aenderungen 2026-03-13 (17): Experten-Profil PDF-Export — 5 Korrekturen #### Backend (Core Service) diff --git a/packages/core-service/src/core/expert-profile/expert-profile.service.ts b/packages/core-service/src/core/expert-profile/expert-profile.service.ts index a5a361b..91d223a 100644 --- a/packages/core-service/src/core/expert-profile/expert-profile.service.ts +++ b/packages/core-service/src/core/expert-profile/expert-profile.service.ts @@ -316,6 +316,7 @@ export class ExpertProfileService { languages: { orderBy: { language: 'asc' } }, projects: { orderBy: [{ fromYear: 'desc' }, { fromMonth: 'desc' }] }, certifications: { orderBy: { issueYear: 'desc' } }, + attachments: { orderBy: { createdAt: 'asc' } }, }, }, }, 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 76b1cf8..7de60c5 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 @@ -55,6 +55,14 @@ interface ExportLanguage { level: string; } +interface ExportAttachment { + id: string; + filename: string; + mimetype: string; + size: number; + data: string; +} + interface ExportData { firstName: string; lastName: string; @@ -71,6 +79,7 @@ interface ExportData { languages: ExportLanguage[]; projects: ExportProject[]; certifications: ExportCertification[]; + attachments: ExportAttachment[]; } | null; } @@ -350,7 +359,7 @@ export class ProfileExportService { yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2; } - // Aufgaben (Markdown-Marker entfernen) + // Aufgaben (Bullet nur wenn Original-Zeile ein Aufzählungszeichen hat) if (proj.tasks) { const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim()); doc.font('Helvetica').fontSize(8).fillColor('#444444'); @@ -359,11 +368,13 @@ export class ProfileExportService { doc.addPage(); yRight = 40; } - const normalized = this.normalizeTaskLine(task); - if (!normalized) continue; - const bulletText = `\u2022 ${normalized}`; - doc.text(bulletText, contentX + 4, yRight, { width: contentWidth - 8 }); - yRight += doc.heightOfString(bulletText, { width: contentWidth - 8 }) + 1; + const raw = task.trim(); + const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); + const cleaned = this.normalizeTaskLine(raw); + if (!cleaned) continue; + const displayText = hasBullet ? `\u2022 ${cleaned}` : cleaned; + doc.text(displayText, contentX + 4, yRight, { width: contentWidth - 8 }); + yRight += doc.heightOfString(displayText, { width: contentWidth - 8 }) + 1; } } @@ -399,26 +410,26 @@ export class ProfileExportService { // Timeline-Punkt doc.circle(certTimelineX, yRight + 4, 3.5).fill(accentColor); if (i < profile.certifications.length - 1) { - doc.moveTo(certTimelineX, yRight + 8).lineTo(certTimelineX, yRight + 40) + doc.moveTo(certTimelineX, yRight + 8).lineTo(certTimelineX, yRight + 32) .strokeColor(accentColor).lineWidth(1).stroke(); } // Jahr (fett) doc.font('Helvetica-Bold').fontSize(8).fillColor('#555555'); doc.text(String(cert.issueYear), certContentX, yRight, { width: certContentWidth }); - yRight += 12; + yRight += 11; // Titel - doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor); + doc.font('Helvetica-Bold').fontSize(9).fillColor(accentColor); doc.text(cert.title, certContentX, yRight, { width: certContentWidth }); yRight += doc.heightOfString(cert.title, { width: certContentWidth }) + 2; // Zertifizierungsstelle - doc.font('Helvetica').fontSize(9).fillColor('#555555'); + doc.font('Helvetica').fontSize(8).fillColor('#555555'); doc.text(cert.issuingBody, certContentX, yRight, { width: certContentWidth }); - yRight += 14; + yRight += 11; - yRight += 6; + yRight += 4; } } @@ -467,6 +478,25 @@ export class ProfileExportService { } } + // --- ANHÄNGE (Bild-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); + doc.font('Helvetica').fontSize(9).fillColor('#777777'); + doc.text(`(Bild konnte nicht dargestellt werden: ${att.filename})`, 40, yAtt, { width: pageWidth - 80 }); + } + } + doc.end(); }); } @@ -669,12 +699,15 @@ export class ProfileExportService { if (proj.tasks) { const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim()); for (const task of taskLines) { - const normalized = this.normalizeTaskLine(task); - if (!normalized) continue; + const raw = task.trim(); + const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); + const cleaned = this.normalizeTaskLine(raw); + if (!cleaned) continue; + const displayText = hasBullet ? `\u2022 ${cleaned}` : cleaned; rightParagraphs.push( new Paragraph({ children: [ - new TextRun({ text: `\u2022 ${normalized}`, size: 16, color: '444444' }), + new TextRun({ text: displayText, size: 16, color: '444444' }), ], spacing: { after: 20 }, }),