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:
+177
-71
@@ -16,6 +16,18 @@ const OLLAMA_TIMEOUT_MS = Number(process.env.OLLAMA_TIMEOUT_MS || 180000);
|
||||
// 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 = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -58,19 +70,10 @@ const OUTPUT_SCHEMA = {
|
||||
required: ['zeitraum', 'titel', 'firma', 'beschreibung'],
|
||||
},
|
||||
},
|
||||
ausbildung: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
zeitraum: { type: 'string' },
|
||||
abschluss: { type: 'string' },
|
||||
institution: { type: 'string' },
|
||||
zusatz: { type: 'string' },
|
||||
},
|
||||
required: ['zeitraum', 'abschluss', 'institution', 'zusatz'],
|
||||
},
|
||||
},
|
||||
studium: { type: 'array', items: EDU_ITEM },
|
||||
berufsausbildung: { type: 'array', items: EDU_ITEM },
|
||||
schulbildung: { type: 'array', items: EDU_ITEM },
|
||||
weiterbildungen: { type: 'array', items: EDU_ITEM },
|
||||
kenntnisse: { type: 'array', items: { type: 'string' } },
|
||||
sprachen: {
|
||||
type: 'array',
|
||||
@@ -82,7 +85,7 @@ const OUTPUT_SCHEMA = {
|
||||
},
|
||||
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'],
|
||||
@@ -146,9 +149,16 @@ async function generateTailoredTexts({ job, basisDokumente, settings }) {
|
||||
` "berufserfahrung": [\n` +
|
||||
` { "zeitraum": "02.2025 – heute", "titel": "Jobtitel", "firma": "Arbeitgeber, Ort", "beschreibung": "1 kurzer Satz (optional)" }\n` +
|
||||
` ],\n` +
|
||||
` "ausbildung": [\n` +
|
||||
` { "zeitraum": "2012 – 2015", "abschluss": "Abschluss", "institution": "Schule/Hochschule", "zusatz": "" }\n` +
|
||||
` "studium": [\n` +
|
||||
` { "zeitraum": "10.2010 – 03.2017", "abschluss": "Studiengang", "institution": "Hochschule", "zusatz": "" }\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` +
|
||||
` "sprachen": [ { "sprache": "Deutsch", "niveau": "Muttersprache" } ],\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 ` +
|
||||
`(keine anderen, keine zusätzlichen Felder, "zeitraum" immer als einzelner String):\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` +
|
||||
`- kontakt: E-Mail, Telefon, Ort, Webseite, Geburtsdatum, Führerschein NUR übernehmen, ` +
|
||||
`wenn in den Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden.\n` +
|
||||
`- anschreiben.absaetze: 3–4 kurze, überzeugende Absätze (kein Adressblock, kein Datum, ` +
|
||||
`keine Grußformel hier). Keine Platzhalter — echte Angaben nutzen.\n` +
|
||||
`- berufserfahrung: relevanteste Stationen (neueste zuerst). "firma" = "Arbeitgeber, Ort". ` +
|
||||
`"beschreibung" = optional EIN kurzer Satz (sonst leer).\n` +
|
||||
`- ausbildung: relevante Abschlüsse/Stationen. "zusatz" optional (z. B. "Abschluss: …" oder "nicht beendet").\n` +
|
||||
`keine Grußformel hier). Sachlich und positiv, in Aktiv-Sätzen, ohne Floskeln und ` +
|
||||
`Weichspüler ("Ich würde gerne …"). KEINE Gehaltsvorstellungen, KEINE Kündigungsgründe, ` +
|
||||
`KEINE Erwähnung von Lücken/Arbeitslosigkeit, keine Selbstzweifel. Keine Platzhalter — echte Angaben nutzen.\n` +
|
||||
`- berufserfahrung: ALLE Stationen aus den Unterlagen, neueste zuerst. "firma" = "Arbeitgeber, Ort". ` +
|
||||
`Nur die 2–3 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` +
|
||||
`- 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 ` +
|
||||
`JSON-Objekt, ohne Markdown, ohne Code-Fences, ohne weiteren Text.`;
|
||||
|
||||
@@ -259,11 +280,36 @@ function toZeitraum(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) {
|
||||
const k = parsed.kontakt || parsed.contact || {};
|
||||
const a = parsed.anschreiben || parsed.cover_letter || {};
|
||||
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 {
|
||||
headline: str(pick(parsed, ['headline', 'berufsbezeichnung', 'titel', 'position', 'beruf', 'rolle'])),
|
||||
kontakt: {
|
||||
@@ -286,17 +332,13 @@ function normalizeResult(parsed) {
|
||||
zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period']) || e.zeitraum),
|
||||
titel: str(pick(e, ['titel', 'position', 'rolle', 'taetigkeit', 'jobtitel', 'bezeichnung', 'job'])),
|
||||
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),
|
||||
ausbildung: pickArray(l, ['ausbildung', 'bildung', 'education', 'qualifikationen'])
|
||||
.map((e) => ({
|
||||
zeitraum: toZeitraum(pick(e, ['zeitraum', 'zeit', 'dauer', 'period', 'jahr']) || e.zeitraum),
|
||||
abschluss: str(pick(e, ['abschluss', 'titel', 'grad', 'degree', 'qualifikation', 'name'])),
|
||||
institution: str(pick(e, ['institution', 'schule', 'hochschule', 'einrichtung', 'organisation'])),
|
||||
zusatz: str(pick(e, ['zusatz', 'note', 'hinweis', 'details'])),
|
||||
}))
|
||||
.filter((e) => e.abschluss || e.institution),
|
||||
studium,
|
||||
berufsausbildung,
|
||||
schulbildung,
|
||||
weiterbildungen,
|
||||
kenntnisse: pickArray(l, ['kenntnisse', 'skills', 'faehigkeiten', 'fähigkeiten', 'kompetenzen'])
|
||||
.map((s) => (typeof s === 'object' ? str(pick(s, ['name', 'skill', 'bezeichnung'])) : str(s)))
|
||||
.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,
|
||||
}));
|
||||
}
|
||||
if (cv.ausbildung.length) {
|
||||
mainSection(doc, S, dry, cur, 'Ausbildung');
|
||||
cv.ausbildung.forEach((e) => mainEntry(doc, S, dry, cur, {
|
||||
zeitraum: e.zeitraum, title: e.abschluss, sub: e.institution, note: e.zusatz,
|
||||
// Education, split into norm-compliant German sections (reverse-chronological).
|
||||
const eduSections = [
|
||||
['Studium', cv.studium],
|
||||
['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) -------------------
|
||||
@@ -542,6 +564,91 @@ function renderTwoColumn(composeSide, composeMain) {
|
||||
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) {
|
||||
return {
|
||||
name: (settings && settings.name) || '',
|
||||
@@ -558,11 +665,8 @@ function renderLebenslaufPdf(cv, header) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderAnschreibenPdf(letter, header, job) {
|
||||
return renderTwoColumn(
|
||||
(doc, S, dry, cur) => composeSidebar(doc, S, dry, cur, { header, cv: null }),
|
||||
(doc, S, dry, cur) => composeMainLetter(doc, S, dry, cur, { letter, header, job })
|
||||
);
|
||||
function renderAnschreibenPdf(letter, header, job, anlagen) {
|
||||
return renderSingleColumn((doc, S, dry, cur) => composeLetter(doc, S, dry, cur, { letter, header, job, anlagen }));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -571,7 +675,8 @@ function renderAnschreibenPdf(letter, header, job) {
|
||||
|
||||
function hasAnschreiben(a) { return a && a.absaetze && a.absaetze.length > 0; }
|
||||
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 }) {
|
||||
@@ -584,17 +689,18 @@ async function generateApplicationDocuments({ job, basisDokumente, settings }) {
|
||||
const label = job.stelle || job.firma || '';
|
||||
|
||||
const documents = [];
|
||||
const cvPresent = hasLebenslauf(data.lebenslauf);
|
||||
|
||||
if (hasAnschreiben(data.anschreiben)) {
|
||||
documents.push({
|
||||
name: `Anschreiben – ${label}`.trim(),
|
||||
filename: `Anschreiben_${suffix}.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({
|
||||
name: `Lebenslauf – ${label}`.trim(),
|
||||
filename: `Lebenslauf_${suffix}.pdf`,
|
||||
|
||||
Reference in New Issue
Block a user