Norm-compliant CV education sections + redesigned monochrome cover letter

CV:
- Split education into proper German sections (Studium, Berufsausbildung,
  Schulbildung, Weiterbildungen) instead of one lumped "Ausbildung".
- Keep the timeline gap-free (lückenlos) and strip HR red-flag phrases
  ("nicht beendet", "abgebrochen", "arbeitslos", …) defensively.

Cover letter:
- Full refactor to a clean, single-column DIN-5008 business letter: letterhead
  (name left, contact right, hairline), recipient block, right-aligned date,
  bold subject, body, signature space, enclosure note ("Anlage: Lebenslauf").
- Monochrome (grayscale only), no accent colours; own single-page auto-fit.
- Prompt hardened against red flags (no salary, no reasons for leaving, no gaps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 10:36:33 +02:00
co-authored by Claude Opus 4.8
parent cd66e2ef4c
commit 5f02413410
+177 -71
View File
@@ -16,6 +16,18 @@ const OLLAMA_TIMEOUT_MS = Number(process.env.OLLAMA_TIMEOUT_MS || 180000);
// 1. LLM call — produce tailored TEXT for the template // 1. LLM call — produce tailored TEXT for the template
// =========================================================================== // ===========================================================================
// Reused shape for every education-type section (Studium, Berufsausbildung, …).
const EDU_ITEM = {
type: 'object',
properties: {
zeitraum: { type: 'string' },
abschluss: { type: 'string' },
institution: { type: 'string' },
zusatz: { type: 'string' },
},
required: ['zeitraum', 'abschluss', 'institution', 'zusatz'],
};
const OUTPUT_SCHEMA = { const OUTPUT_SCHEMA = {
type: 'object', type: 'object',
properties: { properties: {
@@ -58,19 +70,10 @@ const OUTPUT_SCHEMA = {
required: ['zeitraum', 'titel', 'firma', 'beschreibung'], required: ['zeitraum', 'titel', 'firma', 'beschreibung'],
}, },
}, },
ausbildung: { studium: { type: 'array', items: EDU_ITEM },
type: 'array', berufsausbildung: { type: 'array', items: EDU_ITEM },
items: { schulbildung: { type: 'array', items: EDU_ITEM },
type: 'object', weiterbildungen: { type: 'array', items: EDU_ITEM },
properties: {
zeitraum: { type: 'string' },
abschluss: { type: 'string' },
institution: { type: 'string' },
zusatz: { type: 'string' },
},
required: ['zeitraum', 'abschluss', 'institution', 'zusatz'],
},
},
kenntnisse: { type: 'array', items: { type: 'string' } }, kenntnisse: { type: 'array', items: { type: 'string' } },
sprachen: { sprachen: {
type: 'array', type: 'array',
@@ -82,7 +85,7 @@ const OUTPUT_SCHEMA = {
}, },
hobbys: { type: 'array', items: { type: 'string' } }, hobbys: { type: 'array', items: { type: 'string' } },
}, },
required: ['berufserfahrung', 'ausbildung', 'kenntnisse', 'sprachen', 'hobbys'], required: ['berufserfahrung', 'studium', 'berufsausbildung', 'schulbildung', 'weiterbildungen', 'kenntnisse', 'sprachen', 'hobbys'],
}, },
}, },
required: ['headline', 'kontakt', 'anschreiben', 'lebenslauf'], required: ['headline', 'kontakt', 'anschreiben', 'lebenslauf'],
@@ -146,9 +149,16 @@ async function generateTailoredTexts({ job, basisDokumente, settings }) {
` "berufserfahrung": [\n` + ` "berufserfahrung": [\n` +
` { "zeitraum": "02.2025 heute", "titel": "Jobtitel", "firma": "Arbeitgeber, Ort", "beschreibung": "1 kurzer Satz (optional)" }\n` + ` { "zeitraum": "02.2025 heute", "titel": "Jobtitel", "firma": "Arbeitgeber, Ort", "beschreibung": "1 kurzer Satz (optional)" }\n` +
` ],\n` + ` ],\n` +
` "ausbildung": [\n` + ` "studium": [\n` +
` { "zeitraum": "2012 2015", "abschluss": "Abschluss", "institution": "Schule/Hochschule", "zusatz": "" }\n` + ` { "zeitraum": "10.2010 03.2017", "abschluss": "Studiengang", "institution": "Hochschule", "zusatz": "" }\n` +
` ],\n` + ` ],\n` +
` "berufsausbildung": [\n` +
` { "zeitraum": "08.2006 07.2009", "abschluss": "Ausbildungsberuf / Abschluss", "institution": "Betrieb/Berufskolleg", "zusatz": "" }\n` +
` ],\n` +
` "schulbildung": [\n` +
` { "zeitraum": "08.2000 07.2006", "abschluss": "Schulabschluss (z. B. Fachoberschulreife)", "institution": "Schule, Ort", "zusatz": "" }\n` +
` ],\n` +
` "weiterbildungen": [],\n` +
` "kenntnisse": ["Fähigkeit 1", "Fähigkeit 2"],\n` + ` "kenntnisse": ["Fähigkeit 1", "Fähigkeit 2"],\n` +
` "sprachen": [ { "sprache": "Deutsch", "niveau": "Muttersprache" } ],\n` + ` "sprachen": [ { "sprache": "Deutsch", "niveau": "Muttersprache" } ],\n` +
` "hobbys": ["Hobby 1", "Hobby 2"]\n` + ` "hobbys": ["Hobby 1", "Hobby 2"]\n` +
@@ -166,17 +176,28 @@ async function generateTailoredTexts({ job, basisDokumente, settings }) {
`Verwende EXAKT diese JSON-Struktur und exakt diese Schlüsselnamen ` + `Verwende EXAKT diese JSON-Struktur und exakt diese Schlüsselnamen ` +
`(keine anderen, keine zusätzlichen Felder, "zeitraum" immer als einzelner String):\n\n` + `(keine anderen, keine zusätzlichen Felder, "zeitraum" immer als einzelner String):\n\n` +
`${skeleton}\n\n` + `${skeleton}\n\n` +
`Vorgaben:\n` + `Vorgaben (deutscher Lebenslauf nach aktueller Norm/Best Practice):\n` +
`- headline: kurze Berufsbezeichnung des Bewerbers, ggf. auf die Zielstelle zugeschnitten.\n` + `- headline: kurze Berufsbezeichnung des Bewerbers, ggf. auf die Zielstelle zugeschnitten.\n` +
`- kontakt: E-Mail, Telefon, Ort, Webseite, Geburtsdatum, Führerschein NUR übernehmen, ` + `- kontakt: E-Mail, Telefon, Ort, Webseite, Geburtsdatum, Führerschein NUR übernehmen, ` +
`wenn in den Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden.\n` + `wenn in den Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden.\n` +
`- anschreiben.absaetze: 34 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` + `- anschreiben.absaetze: 34 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` +
`keine Grußformel hier). Keine Platzhalter — echte Angaben nutzen.\n` + `keine Grußformel hier). Sachlich und positiv, in Aktiv-Sätzen, ohne Floskeln und ` +
`- berufserfahrung: relevanteste Stationen (neueste zuerst). "firma" = "Arbeitgeber, Ort". ` + `Weichspüler ("Ich würde gerne …"). KEINE Gehaltsvorstellungen, KEINE Kündigungsgründe, ` +
`"beschreibung" = optional EIN kurzer Satz (sonst leer).\n` + `KEINE Erwähnung von Lücken/Arbeitslosigkeit, keine Selbstzweifel. Keine Platzhalter — echte Angaben nutzen.\n` +
`- ausbildung: relevante Abschlüsse/Stationen. "zusatz" optional (z. B. "Abschluss: …" oder "nicht beendet").\n` + `- berufserfahrung: ALLE Stationen aus den Unterlagen, neueste zuerst. "firma" = "Arbeitgeber, Ort". ` +
`Nur die 23 jüngsten/relevantesten erhalten EINEN kurzen "beschreibung"-Satz, ältere lässt du leer.\n` +
`- Bildung korrekt einordnen: Hochschulstudium → studium; Berufsausbildung/Berufskolleg-Abschluss → ` +
`berufsausbildung; schulische Abschlüsse (Realschule, Gymnasium, Fachoberschulreife/Abitur) → schulbildung; ` +
`Zertifikate/Fortbildungen → weiterbildungen. Grundschule weglassen.\n` +
`- kenntnisse: max. 12 prägnante Stichworte, relevanteste zuerst.\n` + `- kenntnisse: max. 12 prägnante Stichworte, relevanteste zuerst.\n` +
`- sprachen & hobbys: nur falls in den Unterlagen vorhanden.\n` + `- sprachen, hobbys, weiterbildungen: nur falls in den Unterlagen vorhanden.\n\n` +
`WICHTIG — keine HR-Red-Flags:\n` +
`- Lückenloser Zeitverlauf: übernimm ALLE Zeiträume aus den Unterlagen, sodass keine unerklärten ` +
`Lücken entstehen. Lasse keine Stationen weg, die sonst eine Lücke hinterlassen (Überschneidungen sind ok).\n` +
`- Keine negativen/entwertenden Vermerke: NIEMALS "nicht beendet", "abgebrochen", "ohne Abschluss", ` +
`"arbeitslos", "arbeitssuchend", "gekündigt" o. Ä. Lasse "zusatz" in solchen Fällen leer und nenne nur ` +
`Zeitraum, Bezeichnung und Institution neutral.\n` +
`- Sachlich, positiv, konsistente Datumsformate (MM.JJJJ).\n\n` +
`Leere Felder als leerer String bzw. leeres Array. Antworte AUSSCHLIESSLICH mit dem ` + `Leere Felder als leerer String bzw. leeres Array. Antworte AUSSCHLIESSLICH mit dem ` +
`JSON-Objekt, ohne Markdown, ohne Code-Fences, ohne weiteren Text.`; `JSON-Objekt, ohne Markdown, ohne Code-Fences, ohne weiteren Text.`;
@@ -259,11 +280,36 @@ function toZeitraum(v) {
return str(v); return str(v);
} }
// Defensive: never let an HR red-flag phrase reach the PDF, even if the model
// ignores the instruction.
const RED_FLAG = /(nicht\s*beendet|abgebrochen|abbruch|ohne\s*abschluss|arbeits(los|suchend)|gek(ü|ue)ndigt|entlassen)/i;
const safeNote = (s) => (RED_FLAG.test(s) ? '' : s);
function mapEdu(e) {
return {
zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period', 'jahr']) || e.zeitraum),
abschluss: str(pick(e, ['abschluss', 'titel', 'grad', 'degree', 'qualifikation', 'studiengang', 'fach', 'name'])),
institution: str(pick(e, ['institution', 'schule', 'hochschule', 'universitaet', 'universität', 'einrichtung', 'betrieb', 'organisation', 'ort'])),
zusatz: safeNote(str(pick(e, ['zusatz', 'note', 'hinweis', 'schwerpunkt', 'details']))),
};
}
const eduList = (obj, keys) => pickArray(obj, keys).map(mapEdu).filter((e) => e.abschluss || e.institution);
function normalizeResult(parsed) { function normalizeResult(parsed) {
const k = parsed.kontakt || parsed.contact || {}; const k = parsed.kontakt || parsed.contact || {};
const a = parsed.anschreiben || parsed.cover_letter || {}; const a = parsed.anschreiben || parsed.cover_letter || {};
const l = parsed.lebenslauf || parsed.cv || parsed.resume || {}; const l = parsed.lebenslauf || parsed.cv || parsed.resume || {};
// Typed education sections; fall back to a lumped "ausbildung"/"bildung" list.
let studium = eduList(l, ['studium', 'hochschulstudium', 'studies']);
let berufsausbildung = eduList(l, ['berufsausbildung', 'ausbildung', 'berufsabschluss', 'apprenticeship']);
let schulbildung = eduList(l, ['schulbildung', 'schulausbildung', 'schule', 'schooling']);
const weiterbildungen = eduList(l, ['weiterbildungen', 'weiterbildung', 'zertifikate', 'certifications', 'fortbildungen']);
if (!studium.length && !berufsausbildung.length && !schulbildung.length) {
const lumped = eduList(l, ['ausbildung', 'bildung', 'education', 'qualifikationen']);
berufsausbildung = lumped;
}
return { return {
headline: str(pick(parsed, ['headline', 'berufsbezeichnung', 'titel', 'position', 'beruf', 'rolle'])), headline: str(pick(parsed, ['headline', 'berufsbezeichnung', 'titel', 'position', 'beruf', 'rolle'])),
kontakt: { kontakt: {
@@ -286,17 +332,13 @@ function normalizeResult(parsed) {
zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period']) || e.zeitraum), zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period']) || e.zeitraum),
titel: str(pick(e, ['titel', 'position', 'rolle', 'taetigkeit', 'jobtitel', 'bezeichnung', 'job'])), titel: str(pick(e, ['titel', 'position', 'rolle', 'taetigkeit', 'jobtitel', 'bezeichnung', 'job'])),
firma: str(pick(e, ['firma', 'arbeitgeber', 'unternehmen', 'company', 'organisation'])), firma: str(pick(e, ['firma', 'arbeitgeber', 'unternehmen', 'company', 'organisation'])),
beschreibung: str(pick(e, ['beschreibung', 'description', 'details', 'text', 'zusatz'])), beschreibung: safeNote(str(pick(e, ['beschreibung', 'description', 'details', 'text']))),
})) }))
.filter((e) => e.titel || e.firma), .filter((e) => e.titel || e.firma),
ausbildung: pickArray(l, ['ausbildung', 'bildung', 'education', 'qualifikationen']) studium,
.map((e) => ({ berufsausbildung,
zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period', 'jahr']) || e.zeitraum), schulbildung,
abschluss: str(pick(e, ['abschluss', 'titel', 'grad', 'degree', 'qualifikation', 'name'])), weiterbildungen,
institution: str(pick(e, ['institution', 'schule', 'hochschule', 'einrichtung', 'organisation'])),
zusatz: str(pick(e, ['zusatz', 'note', 'hinweis', 'details'])),
}))
.filter((e) => e.abschluss || e.institution),
kenntnisse: pickArray(l, ['kenntnisse', 'skills', 'faehigkeiten', 'fähigkeiten', 'kompetenzen']) kenntnisse: pickArray(l, ['kenntnisse', 'skills', 'faehigkeiten', 'fähigkeiten', 'kompetenzen'])
.map((s) => (typeof s === 'object' ? str(pick(s, ['name', 'skill', 'bezeichnung'])) : str(s))) .map((s) => (typeof s === 'object' ? str(pick(s, ['name', 'skill', 'bezeichnung'])) : str(s)))
.filter(Boolean), .filter(Boolean),
@@ -484,40 +526,20 @@ function composeMainCV(doc, S, dry, cur, { cv }) {
zeitraum: e.zeitraum, title: e.titel, sub: e.firma, desc: e.beschreibung, zeitraum: e.zeitraum, title: e.titel, sub: e.firma, desc: e.beschreibung,
})); }));
} }
if (cv.ausbildung.length) { // Education, split into norm-compliant German sections (reverse-chronological).
mainSection(doc, S, dry, cur, 'Ausbildung'); const eduSections = [
cv.ausbildung.forEach((e) => mainEntry(doc, S, dry, cur, { ['Studium', cv.studium],
zeitraum: e.zeitraum, title: e.abschluss, sub: e.institution, note: e.zusatz, ['Berufsausbildung', cv.berufsausbildung],
['Schulbildung', cv.schulbildung],
['Weiterbildungen', cv.weiterbildungen],
];
eduSections.forEach(([title, entries]) => {
if (!entries || !entries.length) return;
mainSection(doc, S, dry, cur, title);
entries.forEach((e) => mainEntry(doc, S, dry, cur, {
zeitraum: e.zeitraum, title: e.abschluss, sub: e.institution, note: safeNote(e.zusatz),
})); }));
}
}
function composeMainLetter(doc, S, dry, cur, { letter, header, job }) {
const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
const betreff = letter.betreff || (job.stelle ? `Bewerbung als ${job.stelle}` : 'Bewerbung');
cur.y += 4 * S;
if (job.firma) write(doc, S, dry, cur, job.firma, { pt: 10.5, style: 'bold', color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.25 });
if (job.ort) write(doc, S, dry, cur, job.ort, { pt: 10, color: C.gray, x: MAIN_X, width: MAIN_W, factor: 1.25 });
cur.y += 4 * S;
const dateLine = header.ort ? `${header.ort}, den ${today}` : today;
write(doc, S, dry, cur, dateLine, { pt: 10, color: C.gray, x: MAIN_X, width: MAIN_W, align: 'right', right: MAIN_R, factor: 1.2 });
cur.y += 4 * S;
write(doc, S, dry, cur, betreff, { pt: 12, style: 'bold', color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.25 });
cur.y += 3.5 * S;
if (letter.anrede) { write(doc, S, dry, cur, letter.anrede, { pt: 10.5, color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.35 }); cur.y += 2 * S; }
letter.absaetze.forEach((p, i) => {
if (i > 0) cur.y += 2.6 * S;
write(doc, S, dry, cur, p, { pt: 10.5, color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.45 });
}); });
cur.y += 4.5 * S;
if (letter.gruss) write(doc, S, dry, cur, letter.gruss, { pt: 10.5, color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.3 });
cur.y += 8 * S;
write(doc, S, dry, cur, header.name, { pt: 10.5, style: 'bold', color: C.ink, x: MAIN_X, width: MAIN_W, factor: 1.2 });
} }
// ----- two-column page assembly (measure → scale → draw) ------------------- // ----- two-column page assembly (measure → scale → draw) -------------------
@@ -542,6 +564,91 @@ function renderTwoColumn(composeSide, composeMain) {
return Buffer.from(doc.output('arraybuffer')); return Buffer.from(doc.output('arraybuffer'));
} }
// ===========================================================================
// Cover letter — clean, single-column DIN-5008 business letter (monochrome)
// ===========================================================================
const LET = { mx: 25, mr: 20, top: 24, bottom: 18 }; // DIN-ish margins
const LET_R = PAGE.w - LET.mr; // 190
const LET_W = LET_R - LET.mx; // 165
// Restrained grayscale palette — no accent colours.
const LC = {
ink: [26, 26, 26],
muted: [92, 98, 106],
hair: [206, 208, 212],
};
function composeLetter(doc, S, dry, cur, { letter, header, job, anlagen }) {
const x = LET.mx;
const right = LET_R;
const width = LET_W;
const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
const betreff = letter.betreff || (job.stelle ? `Bewerbung als ${job.stelle}` : 'Bewerbung');
// --- Letterhead: name (left) + contact (right), thin rule ---
const top = cur.y;
write(doc, S, dry, cur, header.name, { pt: 16, style: 'bold', color: LC.ink, x, width: width * 0.55, factor: 1.15 });
if (header.headline) write(doc, S, dry, cur, header.headline, { pt: 9.5, color: LC.muted, x, width: width * 0.55, factor: 1.2 });
const leftBottom = cur.y;
const contactLines = [];
if (header.adresse) String(header.adresse).split(',').forEach((s) => { if (s.trim()) contactLines.push(s.trim()); });
if (header.telefon) contactLines.push(header.telefon);
if (header.email) contactLines.push(header.email);
const rc = { y: top };
contactLines.forEach((line) => write(doc, S, dry, rc, line, { pt: 9, color: LC.muted, x, width, align: 'right', right, factor: 1.32 }));
cur.y = Math.max(leftBottom, rc.y) + 3 * S;
rule(doc, S, dry, cur.y, x, right, LC.hair, 0.3);
cur.y += 13 * S;
// --- Recipient (Anschriftfeld) ---
if (job.firma) write(doc, S, dry, cur, job.firma, { pt: 10.5, color: LC.ink, x, width, factor: 1.32 });
if (job.ort) write(doc, S, dry, cur, job.ort, { pt: 10.5, color: LC.ink, x, width, factor: 1.32 });
// --- Date (right-aligned) ---
cur.y += 7 * S;
const dateLine = header.ort ? `${header.ort}, den ${today}` : today;
write(doc, S, dry, cur, dateLine, { pt: 10, color: LC.ink, x, width, align: 'right', right, factor: 1.2 });
// --- Subject (bold, no "Betreff:" label) ---
cur.y += 7 * S;
write(doc, S, dry, cur, betreff, { pt: 11, style: 'bold', color: LC.ink, x, width, factor: 1.3 });
// --- Salutation + body ---
cur.y += 6 * S;
if (letter.anrede) { write(doc, S, dry, cur, letter.anrede, { pt: 10.5, color: LC.ink, x, width, factor: 1.4 }); cur.y += 3 * S; }
letter.absaetze.forEach((p, i) => {
if (i > 0) cur.y += 3.2 * S;
write(doc, S, dry, cur, p, { pt: 10.5, color: LC.ink, x, width, factor: 1.52 });
});
// --- Closing + signature space + name ---
cur.y += 6 * S;
if (letter.gruss) write(doc, S, dry, cur, letter.gruss, { pt: 10.5, color: LC.ink, x, width, factor: 1.3 });
cur.y += 13 * S; // room for a signature
write(doc, S, dry, cur, header.name, { pt: 10.5, style: 'bold', color: LC.ink, x, width, factor: 1.2 });
// --- Enclosures ---
if (anlagen && anlagen.length) {
cur.y += 8 * S;
write(doc, S, dry, cur, (anlagen.length > 1 ? 'Anlagen: ' : 'Anlage: ') + anlagen.join(', '),
{ pt: 9, color: LC.muted, x, width, factor: 1.2 });
}
}
function renderSingleColumn(compose) {
const doc = new jsPDF({ unit: 'mm', format: 'a4' });
const m = { y: LET.top }; compose(doc, 1, true, m);
const need = m.y - LET.top;
const avail = PAGE.h - LET.top - LET.bottom;
let S = 1;
if (need > avail) S = Math.max(MIN_SCALE, (avail / need) * 0.99);
compose(doc, S, false, { y: LET.top });
return Buffer.from(doc.output('arraybuffer'));
}
function buildHeader(settings, kontakt, headline) { function buildHeader(settings, kontakt, headline) {
return { return {
name: (settings && settings.name) || '', name: (settings && settings.name) || '',
@@ -558,11 +665,8 @@ function renderLebenslaufPdf(cv, header) {
); );
} }
function renderAnschreibenPdf(letter, header, job) { function renderAnschreibenPdf(letter, header, job, anlagen) {
return renderTwoColumn( return renderSingleColumn((doc, S, dry, cur) => composeLetter(doc, S, dry, cur, { letter, header, job, anlagen }));
(doc, S, dry, cur) => composeSidebar(doc, S, dry, cur, { header, cv: null }),
(doc, S, dry, cur) => composeMainLetter(doc, S, dry, cur, { letter, header, job })
);
} }
// =========================================================================== // ===========================================================================
@@ -571,7 +675,8 @@ function renderAnschreibenPdf(letter, header, job) {
function hasAnschreiben(a) { return a && a.absaetze && a.absaetze.length > 0; } function hasAnschreiben(a) { return a && a.absaetze && a.absaetze.length > 0; }
function hasLebenslauf(l) { function hasLebenslauf(l) {
return l && (l.berufserfahrung.length || l.ausbildung.length || l.kenntnisse.length); return l && (l.berufserfahrung.length || l.studium.length || l.berufsausbildung.length
|| l.schulbildung.length || l.weiterbildungen.length || l.kenntnisse.length);
} }
async function generateApplicationDocuments({ job, basisDokumente, settings }) { async function generateApplicationDocuments({ job, basisDokumente, settings }) {
@@ -584,17 +689,18 @@ async function generateApplicationDocuments({ job, basisDokumente, settings }) {
const label = job.stelle || job.firma || ''; const label = job.stelle || job.firma || '';
const documents = []; const documents = [];
const cvPresent = hasLebenslauf(data.lebenslauf);
if (hasAnschreiben(data.anschreiben)) { if (hasAnschreiben(data.anschreiben)) {
documents.push({ documents.push({
name: `Anschreiben ${label}`.trim(), name: `Anschreiben ${label}`.trim(),
filename: `Anschreiben_${suffix}.pdf`, filename: `Anschreiben_${suffix}.pdf`,
mime: 'application/pdf', mime: 'application/pdf',
buffer: renderAnschreibenPdf(data.anschreiben, header, job), buffer: renderAnschreibenPdf(data.anschreiben, header, job, cvPresent ? ['Lebenslauf'] : []),
}); });
} }
if (hasLebenslauf(data.lebenslauf)) { if (cvPresent) {
documents.push({ documents.push({
name: `Lebenslauf ${label}`.trim(), name: `Lebenslauf ${label}`.trim(),
filename: `Lebenslauf_${suffix}.pdf`, filename: `Lebenslauf_${suffix}.pdf`,