fix(core): PDF-Export — Timeline-Linien korrekt und Faehigkeiten in linke Spalte

- BERUFSERFAHRUNG: Timeline-Linie wird jetzt NACH dem Content gezeichnet
  (entryStartY gespeichert, Linie von entryStartY+8 bis yRight-4)
  Damit stimmt die Laenge exakt mit der tatsaechlichen Eintraghoehe ueberein
- Seitenumbruch-Flag (pageBreakOccurred): Linie wird nicht gezeichnet wenn
  der Content ueber eine Seite hinausgeht
- FAEHIGKEITEN: aus dem full-width Bereich am Seitenende entfernt und in die
  linke Spalte nach ERFAHRUNG verschoben (kleinere Chips: 7pt, 16px, 5px Pad)
- Alte full-width FAEHIGKEITEN-Sektion entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-13 22:08:18 +01:00
parent 41d944312c
commit ed24c061c4

View file

@ -313,6 +313,34 @@ export class ProfileExportService {
} }
} }
// --- FÄHIGKEITEN (linke Spalte, nach Erfahrungen) ---
if (profile && profile.skills.length > 0) {
yLeft += 6;
yLeft = this.pdfSectionTitle(doc, 'FÄHIGKEITEN', leftColX, yLeft, leftColWidth, accentColor);
const skillStartX = leftColX;
const skillMaxX = leftColX + leftColWidth;
let skillX = skillStartX;
let skillY = yLeft;
const skillChipH = 16;
const skillChipPad = 5;
const skillChipGap = 4;
const skillBg = lightenColor(accentColor, 0.85);
for (const skill of profile.skills) {
doc.font('Helvetica').fontSize(7);
const tw = doc.widthOfString(skill);
const cw = tw + skillChipPad * 2;
if (skillX + cw > skillMaxX) {
skillX = skillStartX;
skillY += skillChipH + skillChipGap;
}
doc.roundedRect(skillX, skillY, cw, skillChipH, 7).fill(skillBg);
doc.font('Helvetica').fontSize(7).fillColor(accentColor);
doc.text(skill, skillX + skillChipPad, skillY + 4, { width: tw, lineBreak: false });
skillX += cw + skillChipGap;
}
yLeft = skillY + skillChipH + 4;
}
// --- SEITE 1: Rechte Spalte — BERUFSERFAHRUNG --- // --- SEITE 1: Rechte Spalte — BERUFSERFAHRUNG ---
if (profile && profile.projects.length > 0) { if (profile && profile.projects.length > 0) {
yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG', rightColX, yRight, rightColWidth, accentColor); yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG', rightColX, yRight, rightColWidth, accentColor);
@ -331,14 +359,11 @@ export class ProfileExportService {
yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG (Forts.)', rightColX, yRight, rightColWidth, accentColor); yRight = this.pdfSectionTitle(doc, 'BERUFSERFAHRUNG (Forts.)', rightColX, yRight, rightColWidth, accentColor);
} }
// Timeline-Punkt let pageBreakOccurred = false;
doc.circle(timelineX, yRight + 4, 3.5).fill(accentColor); const entryStartY = yRight;
// Timeline-Linie (bis zum nächsten Eintrag) // Timeline-Punkt
if (i < profile.projects.length - 1) { doc.circle(timelineX, entryStartY + 4, 3.5).fill(accentColor);
doc.moveTo(timelineX, yRight + 8).lineTo(timelineX, yRight + 70)
.strokeColor(accentColor).lineWidth(1).stroke();
}
// Zeitraum (fett, größer) // Zeitraum (fett, größer)
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);
@ -367,6 +392,7 @@ export class ProfileExportService {
if (yRight > pageBottom) { if (yRight > pageBottom) {
doc.addPage(); doc.addPage();
yRight = 40; yRight = 40;
pageBreakOccurred = true;
} }
const raw = task.trim(); const raw = task.trim();
const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw); const hasBullet = /^[•\u2022]\s/.test(raw) || /^\d+\.\s/.test(raw);
@ -380,7 +406,11 @@ export class ProfileExportService {
yRight += 12; yRight += 12;
// Aktualisiere Timeline-Linie (Länge basiert auf tatsächlicher Position) // Timeline-Linie (korrekte Länge basiert auf tatsächlicher Höhe)
if (i < profile.projects.length - 1 && !pageBreakOccurred) {
doc.moveTo(timelineX, entryStartY + 8).lineTo(timelineX, yRight - 4)
.strokeColor(accentColor).lineWidth(1).stroke();
}
} }
} }
@ -433,51 +463,6 @@ export class ProfileExportService {
} }
} }
// --- FÄHIGKEITEN (Skills als Chips) ---
if (profile && profile.skills.length > 0) {
let y = Math.max(yLeft, yRight);
if (y > pageBottom - 60) {
doc.addPage();
y = 40;
} else {
y += 10;
}
y = this.pdfSectionTitle(doc, 'FÄHIGKEITEN', 40, y, pageWidth - 80, accentColor);
const chipStartX = 40;
const maxX = pageWidth - 40;
let chipX = chipStartX;
const chipHeight = 20;
const chipPadding = 10;
const chipGap = 6;
const lightBg = lightenColor(accentColor, 0.85);
for (const skill of profile.skills) {
doc.font('Helvetica').fontSize(8);
const textWidth = doc.widthOfString(skill);
const chipWidth = textWidth + chipPadding * 2;
if (chipX + chipWidth > maxX) {
chipX = chipStartX;
y += chipHeight + chipGap;
if (y > pageBottom) {
doc.addPage();
y = 40;
}
}
// Chip-Hintergrund (abgerundetes Rechteck)
doc.roundedRect(chipX, y, chipWidth, chipHeight, 10).fill(lightBg);
// Chip-Text
doc.font('Helvetica').fontSize(8).fillColor(accentColor);
doc.text(skill, chipX + chipPadding, y + 5.5, { width: textWidth, lineBreak: false });
chipX += chipWidth + chipGap;
}
}
// --- ANHÄNGE (Bild-Anhänge als zusätzliche Seiten) --- // --- ANHÄNGE (Bild-Anhänge als zusätzliche Seiten) ---
const attachments = profile?.attachments ?? []; const attachments = profile?.attachments ?? [];
for (const att of attachments) { for (const att of attachments) {