diff --git a/lib/documents.js b/lib/documents.js index 95335f1..e503ab1 100644 --- a/lib/documents.js +++ b/lib/documents.js @@ -228,7 +228,13 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen `(keine anderen, keine zusätzlichen Felder, "zeitraum" immer als einzelner String):\n\n` + `${skeleton}\n\n` + `Vorgaben (deutscher Lebenslauf nach aktueller Norm/Best Practice):\n` + - `- headline: kurze Berufsbezeichnung des Bewerbers, ggf. auf die Zielstelle zugeschnitten.\n` + + `- headline: kurze, sachliche Berufsbezeichnung des Bewerbers, ggf. auf die Zielstelle zugeschnitten. ` + + `NIEMALS eine geschützte oder formal an eine Ausbildung/Prüfung/einen Studienabschluss gebundene ` + + `Berufsbezeichnung verwenden, die der Bewerber laut Unterlagen nicht nachweislich erworben hat ` + + `(z. B. "Fachinformatiker", "Ingenieur", "Techniker", "Meister", "Kaufmann/-frau", "Geselle", ` + + `"Facharzt", "Bachelor/Master", "Dipl.-..." u. Ä.). Im Zweifel eine neutrale, nicht geschützte ` + + `Tätigkeitsbezeichnung wählen (z. B. "Softwareentwickler", "IT-Fachkraft", "Anwendungsentwickler") ` + + `statt eines geschützten Titels.\n` + `- kontakt: E-Mail, Telefon, Ort, Webseite, Geburtsdatum NUR übernehmen, wenn in den ` + `Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden. Geburtsdatum als ` + `DD.MM.YYYY (z. B. "03.04.1990"). Keine anderen persönlichen Angaben (Führerschein, ` + @@ -900,18 +906,12 @@ function cvEducation(doc, S, dry, cur, e) { cur.y += 4 * S; } -// Big name + target role, closed by a short accent rule — the confident head -// the recruiter's eye lands on first. +// Big name + target role — the confident head the recruiter's eye lands on +// first. No divider rule here; the first section heading provides the structure. function composeMainHeader(doc, S, dry, cur, header, titel) { write(doc, S, dry, cur, header.name, { pt: 25, style: 'bold', color: RC.accent, x: MAIN_X, width: MAIN_W, factor: 1.06 }); if (titel) write(doc, S, dry, cur, titel, { pt: 12, color: RC.sub, x: MAIN_X, width: MAIN_W, factor: 1.36, charSpace: 0.3 }); - cur.y += 2.6 * S; - if (!dry) { - doc.setDrawColor(RC.accent[0], RC.accent[1], RC.accent[2]); - doc.setLineWidth(1.2 * S); - doc.line(MAIN_X, cur.y, MAIN_X + 20, cur.y); - } - cur.y += 2.2 * S; + cur.y += 2 * S; } function composeMain(doc, S, dry, { cv, header, titel }) { @@ -961,7 +961,7 @@ function renderCV(cv, header, titel, foto) { // 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 = { mx: 25, mr: 20, top: 24, bottom: 24 }; // DIN-ish margins (bottom reserves the footer strip) const LET_R = PAGE.w - LET.mr; // 190 const LET_W = LET_R - LET.mx; // 165 @@ -991,29 +991,22 @@ function composeLetter(doc, S, dry, cur, { letter, header, job, anlagen, signatu const betreff = letter.betreff || (job.stelle ? `Bewerbung als ${job.stelle}` : 'Bewerbung'); const stadt = cityName(header); // just the town for the date line ("Ort, den …") - // --- Letterhead: name (left) + contact (right), accented rule --- - const top = cur.y; - write(doc, S, dry, cur, header.name, { pt: 17, style: 'bold', color: LC.accent, x, width: width * 0.55, factor: 1.15 }); - if (header.headline) write(doc, S, dry, cur, header.headline, { pt: 10, color: LC.muted, x, width: width * 0.55, factor: 1.25, charSpace: 0.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 })); + // --- Letterhead: a big, confident name + role in tracked caps, closed by + // the shared hairline-with-accent motif. Full contact details live in the + // footer strip, which keeps the head clean and echoes the résumé header. --- + write(doc, S, dry, cur, header.name, { pt: 20, style: 'bold', color: LC.accent, x, width, factor: 1.12 }); + if (header.headline) write(doc, S, dry, cur, header.headline, { pt: 9.5, color: LC.muted, x, width, factor: 1.32, charSpace: 0.8, upper: true }); // Hairline across, with a short heavier accent segment at the start — the // same motif that anchors every section of the résumé. - cur.y = Math.max(leftBottom, rc.y) + 3 * S; + cur.y += 3.4 * S; if (!dry) { doc.setDrawColor(LC.hair[0], LC.hair[1], LC.hair[2]); doc.setLineWidth(0.3 * S); doc.line(x, cur.y, right, cur.y); doc.setDrawColor(LC.accent[0], LC.accent[1], LC.accent[2]); - doc.setLineWidth(1.0 * S); - doc.line(x, cur.y, x + 15 * S, cur.y); + doc.setLineWidth(1.1 * S); + doc.line(x, cur.y, x + 16 * S, cur.y); } cur.y += 13 * S; @@ -1031,9 +1024,9 @@ function composeLetter(doc, S, dry, cur, { letter, header, job, anlagen, signatu const dateLine = stadt ? `${stadt}, den ${today}` : today; write(doc, S, dry, cur, dateLine, { pt: 10, color: LC.ink, x, width, align: 'right', right, factor: 1.2 }); - // --- Subject (bold navy, no "Betreff:" label) — the letter's headline --- + // --- Subject (bold, no "Betreff:" label) — the letter's headline --- cur.y += 7 * S; - write(doc, S, dry, cur, betreff, { pt: 11, style: 'bold', color: LC.accent, x, width, factor: 1.3 }); + write(doc, S, dry, cur, betreff, { pt: 11.5, style: 'bold', color: LC.accent, x, width, factor: 1.3 }); // --- Salutation + body --- cur.y += 6 * S; @@ -1075,6 +1068,25 @@ function composeLetter(doc, S, dry, cur, { letter, header, job, anlagen, signatu write(doc, S, dry, cur, (anlagen.length > 1 ? 'Anlagen: ' : 'Anlage: ') + anlagen.join(', '), { pt: 9, color: LC.muted, x, width, factor: 1.2 }); } + + // --- Footer letterhead: a centred contact strip under a hairline, drawn in + // the reserved bottom margin so it never collides with the body. --- + if (!dry) { + const parts = []; + if (header.adresse) parts.push(String(header.adresse).replace(/\s*,\s*/g, ', ')); + if (header.telefon) parts.push(header.telefon); + if (header.email) parts.push(header.email); + if (parts.length) { + const fy = PAGE.h - 15; + doc.setDrawColor(LC.hair[0], LC.hair[1], LC.hair[2]); + doc.setLineWidth(0.3 * S); + doc.line(x, fy, right, fy); + doc.setFont('Lato', 'normal'); + doc.setFontSize(8 * S); + doc.setTextColor(LC.muted[0], LC.muted[1], LC.muted[2]); + doc.text(parts.join(' · '), (x + right) / 2, fy + 4 * S, { align: 'center' }); + } + } } function renderSingleColumn(compose) {