mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 01:56:39 +02:00
fix: PDF-Export — Dateiname, Fettschrift, Zeichen, Abstaende, Zertifizierungen rechts
5 Korrekturen am Experten-Profil PDF-Export: 1. Dateiname: Vorname_Nachname_CV.pdf/.docx (RFC 5987 Umlaut-sicher) 2. Fettschrift: Zeitraum, Firmenname und Branche in Berufserfahrung 3. Zeichen-Darstellung: Markdown-Marker (**bold**/*italic*/__u__) werden aus Tasks-Text entfernt, doppelte Bullet-Praefix-Normalisierung 4. Abstaende: Sprachen 14->11px, Erfahrung 14->11px, Gap 8->4px 5. Zertifizierungen in rechte Spalte verschoben (nach Berufserfahrung) statt als Vollbreite-Sektion am Ende Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c333cbfa4b
commit
a37942b37d
3 changed files with 131 additions and 79 deletions
25
Summarize.md
25
Summarize.md
|
|
@ -6,6 +6,31 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (17): Experten-Profil PDF-Export — 5 Korrekturen
|
||||||
|
|
||||||
|
#### Backend (Core Service)
|
||||||
|
- `expert-profile/profile-export.service.ts` — PDF-Export verbessert:
|
||||||
|
- Neue Helper-Methoden `stripMarkdown()` und `normalizeTaskLine()`
|
||||||
|
- Zeitraum fett (Helvetica-Bold, #555555)
|
||||||
|
- Firmenname + Branche fett (Helvetica-Bold, #333333)
|
||||||
|
- Tasks: Markdown-Marker (**bold**, *italic*, __u__) werden vor Ausgabe entfernt
|
||||||
|
- Sprachen/Erfahrung: Abstand 14px → 11px, Sektion-Gap 8px → 4px
|
||||||
|
- Zertifizierungen in rechte Spalte verschoben (nach Berufserfahrung)
|
||||||
|
- Return-Typ: `Promise<{ buffer, firstName, lastName }>` (war: `Promise<Buffer>`)
|
||||||
|
- `expert-profile/profile-export.service.ts` — DOCX analog angepasst:
|
||||||
|
- Zeitraum + Firma fett; normalizeTaskLine() fuer Tasks; Zertifizierungen in rightParagraphs
|
||||||
|
- `expert-profile/expert-profile.controller.ts`:
|
||||||
|
- PDF: `Vorname_Nachname_CV.pdf`, DOCX: `Vorname_Nachname_CV.docx`
|
||||||
|
- RFC-5987 `filename*=UTF-8''...` fuer Umlaut-Dateinamen
|
||||||
|
|
||||||
|
#### TypeScript
|
||||||
|
- `npx tsc --noEmit` in packages/core-service: 0 Fehler
|
||||||
|
|
||||||
|
#### Deployment-Hinweis (Schritt 17)
|
||||||
|
- Rebuild + Restart: nur core-service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Aenderungen 2026-03-13 (16): Global Admin — Login-Screen-Branding (Hintergrund + Logo)
|
### Aenderungen 2026-03-13 (16): Global Admin — Login-Screen-Branding (Hintergrund + Logo)
|
||||||
|
|
||||||
#### Backend (Core Service)
|
#### Backend (Core Service)
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,12 @@ export class ExpertProfileController {
|
||||||
@CurrentUser('sub') userId: string,
|
@CurrentUser('sub') userId: string,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
const buffer = await this.profileExportService.generatePdf(userId);
|
const { buffer, firstName, lastName } = await this.profileExportService.generatePdf(userId);
|
||||||
|
const baseName = `${firstName}_${lastName}_CV`.replace(/\s+/g, '_');
|
||||||
|
const encodedName = encodeURIComponent(`${baseName}.pdf`);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
'Content-Disposition': 'attachment; filename="Profil.pdf"',
|
'Content-Disposition': `attachment; filename="${baseName}.pdf"; filename*=UTF-8''${encodedName}`,
|
||||||
'Content-Length': String(buffer.length),
|
'Content-Length': String(buffer.length),
|
||||||
});
|
});
|
||||||
res.end(buffer);
|
res.end(buffer);
|
||||||
|
|
@ -187,10 +189,12 @@ export class ExpertProfileController {
|
||||||
@CurrentUser('sub') userId: string,
|
@CurrentUser('sub') userId: string,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
const buffer = await this.profileExportService.generateDocx(userId);
|
const { buffer, firstName, lastName } = await this.profileExportService.generateDocx(userId);
|
||||||
|
const baseName = `${firstName}_${lastName}_CV`.replace(/\s+/g, '_');
|
||||||
|
const encodedName = encodeURIComponent(`${baseName}.docx`);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
'Content-Disposition': 'attachment; filename="Profil.docx"',
|
'Content-Disposition': `attachment; filename="${baseName}.docx"; filename*=UTF-8''${encodedName}`,
|
||||||
'Content-Length': String(buffer.length),
|
'Content-Length': String(buffer.length),
|
||||||
});
|
});
|
||||||
res.end(buffer);
|
res.end(buffer);
|
||||||
|
|
|
||||||
|
|
@ -152,15 +152,36 @@ export class ProfileExportService {
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Hilfsfunktionen: Text-Bereinigung
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** Entfernt Markdown-Marker (**bold**, *italic*, __underline__) aus Text */
|
||||||
|
private stripMarkdown(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/\*\*([^*\n]+)\*\*/g, '$1') // **fett** → Text
|
||||||
|
.replace(/\*([^*\n]+)\*/g, '$1') // *kursiv* → Text
|
||||||
|
.replace(/__([^_\n]+)__/g, '$1'); // __unterstrichen__ → Text
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Normalisiert eine Aufgaben-Zeile: entfernt bestehende Bullet/Nummer-Präfixe */
|
||||||
|
private normalizeTaskLine(line: string): string {
|
||||||
|
return this.stripMarkdown(line.trim())
|
||||||
|
.replace(/^[•\u2022]\s*/, '') // vorhandenes Bullet entfernen
|
||||||
|
.replace(/^\d+\.\s+/, '') // vorhandene Nummerierung entfernen
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PDF Export
|
// PDF Export
|
||||||
// ============================================================
|
// ============================================================
|
||||||
async generatePdf(userId: string, accentColor = '#009688'): Promise<Buffer> {
|
async generatePdf(userId: string, accentColor = '#009688'): Promise<{ buffer: Buffer; firstName: string; lastName: string }> {
|
||||||
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;
|
||||||
const fullName = `${data.firstName} ${data.lastName}`;
|
const fullName = `${data.firstName} ${data.lastName}`;
|
||||||
|
const { firstName, lastName } = data;
|
||||||
|
|
||||||
return new Promise<Buffer>((resolve, reject) => {
|
return new Promise<{ buffer: Buffer; firstName: string; lastName: string }>((resolve, reject) => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
const doc = new PDFDocument({
|
const doc = new PDFDocument({
|
||||||
size: 'A4',
|
size: 'A4',
|
||||||
|
|
@ -169,7 +190,7 @@ export class ProfileExportService {
|
||||||
});
|
});
|
||||||
|
|
||||||
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
|
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
doc.on('end', () => resolve({ buffer: Buffer.concat(chunks), firstName, lastName }));
|
||||||
doc.on('error', reject);
|
doc.on('error', reject);
|
||||||
|
|
||||||
// --- Konstanten ---
|
// --- Konstanten ---
|
||||||
|
|
@ -265,9 +286,9 @@ export class ProfileExportService {
|
||||||
doc.text(lang.language, leftColX, yLeft, { width: leftColWidth * 0.55, continued: false });
|
doc.text(lang.language, leftColX, yLeft, { width: leftColWidth * 0.55, continued: false });
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#777777');
|
doc.font('Helvetica').fontSize(8).fillColor('#777777');
|
||||||
doc.text(lang.level, leftColX + leftColWidth * 0.6, yLeft, { width: leftColWidth * 0.4 });
|
doc.text(lang.level, leftColX + leftColWidth * 0.6, yLeft, { width: leftColWidth * 0.4 });
|
||||||
yLeft += 14;
|
yLeft += 11;
|
||||||
}
|
}
|
||||||
yLeft += 8;
|
yLeft += 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ERFAHRUNG (Expertise-Bereiche) ---
|
// --- ERFAHRUNG (Expertise-Bereiche) ---
|
||||||
|
|
@ -279,7 +300,7 @@ export class ProfileExportService {
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#777777');
|
doc.font('Helvetica').fontSize(8).fillColor('#777777');
|
||||||
const expDetail = `${exp.years}J.${exp.level ? ' · ' + exp.level : ''}`;
|
const expDetail = `${exp.years}J.${exp.level ? ' · ' + exp.level : ''}`;
|
||||||
doc.text(expDetail, leftColX + leftColWidth * 0.65, yLeft, { width: leftColWidth * 0.35 });
|
doc.text(expDetail, leftColX + leftColWidth * 0.65, yLeft, { width: leftColWidth * 0.35 });
|
||||||
yLeft += 14;
|
yLeft += 11;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,9 +331,9 @@ export class ProfileExportService {
|
||||||
.strokeColor(accentColor).lineWidth(1).stroke();
|
.strokeColor(accentColor).lineWidth(1).stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zeitraum
|
// Zeitraum (fett)
|
||||||
const dateRange = this.formatDateRange(proj.fromMonth, proj.fromYear, proj.toMonth, proj.toYear, proj.isCurrent);
|
const dateRange = this.formatDateRange(proj.fromMonth, proj.fromYear, proj.toMonth, proj.toYear, proj.isCurrent);
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#888888');
|
doc.font('Helvetica-Bold').fontSize(8).fillColor('#555555');
|
||||||
doc.text(dateRange, contentX, yRight, { width: contentWidth });
|
doc.text(dateRange, contentX, yRight, { width: contentWidth });
|
||||||
yRight += 12;
|
yRight += 12;
|
||||||
|
|
||||||
|
|
@ -321,15 +342,15 @@ export class ProfileExportService {
|
||||||
doc.text(proj.role, contentX, yRight, { width: contentWidth });
|
doc.text(proj.role, contentX, yRight, { width: contentWidth });
|
||||||
yRight += doc.heightOfString(proj.role, { width: contentWidth }) + 2;
|
yRight += doc.heightOfString(proj.role, { width: contentWidth }) + 2;
|
||||||
|
|
||||||
// Firma + Branche
|
// Firma + Branche (fett)
|
||||||
if (proj.company) {
|
if (proj.company) {
|
||||||
const companyLine = [proj.company, proj.industry].filter(Boolean).join(' · ');
|
const companyLine = [proj.company, proj.industry].filter(Boolean).join(' · ');
|
||||||
doc.font('Helvetica').fontSize(9).fillColor('#555555');
|
doc.font('Helvetica-Bold').fontSize(9).fillColor('#333333');
|
||||||
doc.text(companyLine, contentX, yRight, { width: contentWidth });
|
doc.text(companyLine, contentX, yRight, { width: contentWidth });
|
||||||
yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2;
|
yRight += doc.heightOfString(companyLine, { width: contentWidth }) + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aufgaben
|
// Aufgaben (Markdown-Marker entfernen)
|
||||||
if (proj.tasks) {
|
if (proj.tasks) {
|
||||||
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#444444');
|
doc.font('Helvetica').fontSize(8).fillColor('#444444');
|
||||||
|
|
@ -338,7 +359,9 @@ export class ProfileExportService {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
yRight = 40;
|
yRight = 40;
|
||||||
}
|
}
|
||||||
const bulletText = `\u2022 ${task.trim()}`;
|
const normalized = this.normalizeTaskLine(task);
|
||||||
|
if (!normalized) continue;
|
||||||
|
const bulletText = `\u2022 ${normalized}`;
|
||||||
doc.text(bulletText, contentX + 4, yRight, { width: contentWidth - 8 });
|
doc.text(bulletText, contentX + 4, yRight, { width: contentWidth - 8 });
|
||||||
yRight += doc.heightOfString(bulletText, { width: contentWidth - 8 }) + 1;
|
yRight += doc.heightOfString(bulletText, { width: contentWidth - 8 }) + 1;
|
||||||
}
|
}
|
||||||
|
|
@ -350,55 +373,53 @@ export class ProfileExportService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FOLGESEITEN: ZERTIFIZIERUNGEN ---
|
// --- ZERTIFIZIERUNGEN (rechte Spalte) ---
|
||||||
if (profile && profile.certifications.length > 0) {
|
if (profile && profile.certifications.length > 0) {
|
||||||
const certY = Math.max(yLeft, yRight);
|
if (yRight > pageBottom - 80) {
|
||||||
let y = certY > pageBottom - 80 ? 40 : certY + 20;
|
|
||||||
if (certY > pageBottom - 80) {
|
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
|
yRight = 40;
|
||||||
|
} else {
|
||||||
|
yRight += 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
y = this.pdfSectionTitle(doc, 'ZERTIFIZIERUNGEN', 40, y, pageWidth - 80, accentColor);
|
yRight = this.pdfSectionTitle(doc, 'ZERTIFIZIERUNGEN', rightColX, yRight, rightColWidth, accentColor);
|
||||||
|
|
||||||
const timelineX = 46;
|
const certTimelineX = rightColX + 6;
|
||||||
const contentX = 58;
|
const certContentX = rightColX + 18;
|
||||||
const contentWidth = pageWidth - 58 - 40;
|
const certContentWidth = rightColWidth - 18;
|
||||||
|
|
||||||
for (let i = 0; i < profile.certifications.length; i++) {
|
for (let i = 0; i < profile.certifications.length; i++) {
|
||||||
const cert = profile.certifications[i];
|
const cert = profile.certifications[i];
|
||||||
|
|
||||||
if (y > pageBottom) {
|
if (yRight > pageBottom) {
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
y = 40;
|
yRight = 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline-Punkt
|
// Timeline-Punkt
|
||||||
doc.circle(timelineX, y + 4, 3.5).fill(accentColor);
|
doc.circle(certTimelineX, yRight + 4, 3.5).fill(accentColor);
|
||||||
if (i < profile.certifications.length - 1) {
|
if (i < profile.certifications.length - 1) {
|
||||||
doc.moveTo(timelineX, y + 8).lineTo(timelineX, y + 40)
|
doc.moveTo(certTimelineX, yRight + 8).lineTo(certTimelineX, yRight + 40)
|
||||||
.strokeColor(accentColor).lineWidth(1).stroke();
|
.strokeColor(accentColor).lineWidth(1).stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jahr
|
// Jahr (fett)
|
||||||
doc.font('Helvetica').fontSize(8).fillColor('#888888');
|
doc.font('Helvetica-Bold').fontSize(8).fillColor('#555555');
|
||||||
doc.text(String(cert.issueYear), contentX, y, { width: contentWidth });
|
doc.text(String(cert.issueYear), certContentX, yRight, { width: certContentWidth });
|
||||||
y += 12;
|
yRight += 12;
|
||||||
|
|
||||||
// Titel
|
// Titel
|
||||||
doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor);
|
doc.font('Helvetica-Bold').fontSize(10).fillColor(accentColor);
|
||||||
doc.text(cert.title, contentX, y, { width: contentWidth });
|
doc.text(cert.title, certContentX, yRight, { width: certContentWidth });
|
||||||
y += doc.heightOfString(cert.title, { width: contentWidth }) + 2;
|
yRight += doc.heightOfString(cert.title, { width: certContentWidth }) + 2;
|
||||||
|
|
||||||
// Zertifizierungsstelle
|
// Zertifizierungsstelle
|
||||||
doc.font('Helvetica').fontSize(9).fillColor('#555555');
|
doc.font('Helvetica').fontSize(9).fillColor('#555555');
|
||||||
doc.text(cert.issuingBody, contentX, y, { width: contentWidth });
|
doc.text(cert.issuingBody, certContentX, yRight, { width: certContentWidth });
|
||||||
y += 14;
|
yRight += 14;
|
||||||
|
|
||||||
y += 8;
|
yRight += 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
yRight = y;
|
|
||||||
yLeft = y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FÄHIGKEITEN (Skills als Chips) ---
|
// --- FÄHIGKEITEN (Skills als Chips) ---
|
||||||
|
|
@ -453,7 +474,7 @@ export class ProfileExportService {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// DOCX Export
|
// DOCX Export
|
||||||
// ============================================================
|
// ============================================================
|
||||||
async generateDocx(userId: string, accentColor = '#009688'): Promise<Buffer> {
|
async generateDocx(userId: string, accentColor = '#009688'): Promise<{ buffer: Buffer; firstName: string; lastName: string }> {
|
||||||
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;
|
||||||
const fullName = `${data.firstName} ${data.lastName}`;
|
const fullName = `${data.firstName} ${data.lastName}`;
|
||||||
|
|
@ -618,7 +639,7 @@ export class ProfileExportService {
|
||||||
rightParagraphs.push(
|
rightParagraphs.push(
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
children: [
|
children: [
|
||||||
new TextRun({ text: dateRange, size: 16, color: '888888', italics: true }),
|
new TextRun({ text: dateRange, size: 16, color: '555555', bold: true }),
|
||||||
],
|
],
|
||||||
spacing: { before: 160, after: 40 },
|
spacing: { before: 160, after: 40 },
|
||||||
}),
|
}),
|
||||||
|
|
@ -638,7 +659,7 @@ export class ProfileExportService {
|
||||||
rightParagraphs.push(
|
rightParagraphs.push(
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
children: [
|
children: [
|
||||||
new TextRun({ text: companyLine, size: 18, color: '555555' }),
|
new TextRun({ text: companyLine, size: 18, color: '333333', bold: true }),
|
||||||
],
|
],
|
||||||
spacing: { after: 40 },
|
spacing: { after: 40 },
|
||||||
}),
|
}),
|
||||||
|
|
@ -648,10 +669,12 @@ export class ProfileExportService {
|
||||||
if (proj.tasks) {
|
if (proj.tasks) {
|
||||||
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
const taskLines = proj.tasks.split('\n').filter((l: string) => l.trim());
|
||||||
for (const task of taskLines) {
|
for (const task of taskLines) {
|
||||||
|
const normalized = this.normalizeTaskLine(task);
|
||||||
|
if (!normalized) continue;
|
||||||
rightParagraphs.push(
|
rightParagraphs.push(
|
||||||
new Paragraph({
|
new Paragraph({
|
||||||
children: [
|
children: [
|
||||||
new TextRun({ text: `\u2022 ${task.trim()}`, size: 16, color: '444444' }),
|
new TextRun({ text: `\u2022 ${normalized}`, size: 16, color: '444444' }),
|
||||||
],
|
],
|
||||||
spacing: { after: 20 },
|
spacing: { after: 20 },
|
||||||
}),
|
}),
|
||||||
|
|
@ -661,6 +684,40 @@ export class ProfileExportService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Zertifizierungen (rechte Spalte) ---
|
||||||
|
if (profile && profile.certifications.length > 0) {
|
||||||
|
rightParagraphs.push(this.docxSectionHeading('ZERTIFIZIERUNGEN', accentHex));
|
||||||
|
|
||||||
|
for (const cert of profile.certifications) {
|
||||||
|
rightParagraphs.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({ text: String(cert.issueYear), size: 16, color: '555555', bold: true }),
|
||||||
|
],
|
||||||
|
spacing: { before: 120, after: 30 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
rightParagraphs.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({ text: cert.title, bold: true, size: 20, color: accentHex }),
|
||||||
|
],
|
||||||
|
spacing: { after: 20 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
rightParagraphs.push(
|
||||||
|
new Paragraph({
|
||||||
|
children: [
|
||||||
|
new TextRun({ text: cert.issuingBody, size: 18, color: '555555' }),
|
||||||
|
],
|
||||||
|
spacing: { after: 60 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Tabelle (Zwei-Spalten-Layout) ---
|
// --- Tabelle (Zwei-Spalten-Layout) ---
|
||||||
const noBorders = {
|
const noBorders = {
|
||||||
top: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
|
top: { style: BorderStyle.NONE, size: 0, color: 'FFFFFF' },
|
||||||
|
|
@ -689,40 +746,6 @@ export class ProfileExportService {
|
||||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Volle Breite: Zertifizierungen ---
|
|
||||||
if (profile && profile.certifications.length > 0) {
|
|
||||||
sections.push(this.docxSectionHeading('ZERTIFIZIERUNGEN', accentHex));
|
|
||||||
|
|
||||||
for (const cert of profile.certifications) {
|
|
||||||
sections.push(
|
|
||||||
new Paragraph({
|
|
||||||
children: [
|
|
||||||
new TextRun({ text: String(cert.issueYear), size: 16, color: '888888', italics: true }),
|
|
||||||
],
|
|
||||||
spacing: { before: 120, after: 30 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
sections.push(
|
|
||||||
new Paragraph({
|
|
||||||
children: [
|
|
||||||
new TextRun({ text: cert.title, bold: true, size: 20, color: accentHex }),
|
|
||||||
],
|
|
||||||
spacing: { after: 20 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
sections.push(
|
|
||||||
new Paragraph({
|
|
||||||
children: [
|
|
||||||
new TextRun({ text: cert.issuingBody, size: 18, color: '555555' }),
|
|
||||||
],
|
|
||||||
spacing: { after: 60 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Volle Breite: Fähigkeiten (Skills als Chips) ---
|
// --- Volle Breite: Fähigkeiten (Skills als Chips) ---
|
||||||
if (profile && profile.skills.length > 0) {
|
if (profile && profile.skills.length > 0) {
|
||||||
sections.push(this.docxSectionHeading('FÄHIGKEITEN', accentHex));
|
sections.push(this.docxSectionHeading('FÄHIGKEITEN', accentHex));
|
||||||
|
|
@ -776,7 +799,7 @@ export class ProfileExportService {
|
||||||
|
|
||||||
const buffer = await Packer.toBuffer(document);
|
const buffer = await Packer.toBuffer(document);
|
||||||
this.logger.log(`DOCX generiert für User ${userId}: ${buffer.length} Bytes`);
|
this.logger.log(`DOCX generiert für User ${userId}: ${buffer.length} Bytes`);
|
||||||
return buffer;
|
return { buffer, firstName: data.firstName, lastName: data.lastName };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue