CV: single-column layout, drop personal fields (Geburtsdatum/Führerschein/Schulbildung)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+105
-196
@@ -39,10 +39,8 @@ const OUTPUT_SCHEMA = {
|
|||||||
telefon: { type: 'string' },
|
telefon: { type: 'string' },
|
||||||
ort: { type: 'string' },
|
ort: { type: 'string' },
|
||||||
webseite: { type: 'string' },
|
webseite: { type: 'string' },
|
||||||
geburtsdatum: { type: 'string' },
|
|
||||||
fuehrerschein: { type: 'string' },
|
|
||||||
},
|
},
|
||||||
required: ['email', 'telefon', 'ort', 'webseite', 'geburtsdatum', 'fuehrerschein'],
|
required: ['email', 'telefon', 'ort', 'webseite'],
|
||||||
},
|
},
|
||||||
anschreiben: {
|
anschreiben: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -83,7 +81,6 @@ const OUTPUT_SCHEMA = {
|
|||||||
},
|
},
|
||||||
studium: { type: 'array', items: EDU_ITEM },
|
studium: { type: 'array', items: EDU_ITEM },
|
||||||
berufsausbildung: { type: 'array', items: EDU_ITEM },
|
berufsausbildung: { type: 'array', items: EDU_ITEM },
|
||||||
schulbildung: { type: 'array', items: EDU_ITEM },
|
|
||||||
weiterbildungen: { type: 'array', items: EDU_ITEM },
|
weiterbildungen: { type: 'array', items: EDU_ITEM },
|
||||||
kenntnisse: { type: 'array', items: { type: 'string' } },
|
kenntnisse: { type: 'array', items: { type: 'string' } },
|
||||||
sprachen: {
|
sprachen: {
|
||||||
@@ -96,7 +93,7 @@ const OUTPUT_SCHEMA = {
|
|||||||
},
|
},
|
||||||
hobbys: { type: 'array', items: { type: 'string' } },
|
hobbys: { type: 'array', items: { type: 'string' } },
|
||||||
},
|
},
|
||||||
required: ['profil', 'berufserfahrung', 'studium', 'berufsausbildung', 'schulbildung', 'weiterbildungen', 'kenntnisse', 'sprachen', 'hobbys'],
|
required: ['profil', 'berufserfahrung', 'studium', 'berufsausbildung', 'weiterbildungen', 'kenntnisse', 'sprachen', 'hobbys'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['headline', 'kontakt', 'anschreiben', 'lebenslauf'],
|
required: ['headline', 'kontakt', 'anschreiben', 'lebenslauf'],
|
||||||
@@ -149,7 +146,7 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
const skeleton =
|
const skeleton =
|
||||||
`{\n` +
|
`{\n` +
|
||||||
` "headline": "Kurze Berufsbezeichnung (max. 5 Wörter)",\n` +
|
` "headline": "Kurze Berufsbezeichnung (max. 5 Wörter)",\n` +
|
||||||
` "kontakt": { "email": "", "telefon": "", "ort": "", "webseite": "", "geburtsdatum": "", "fuehrerschein": "" },\n` +
|
` "kontakt": { "email": "", "telefon": "", "ort": "", "webseite": "" },\n` +
|
||||||
` "anschreiben": {\n` +
|
` "anschreiben": {\n` +
|
||||||
` "empfaenger": { "firma": "Unternehmen", "adresse": "Straße Hausnr. (falls bekannt, sonst leer)", "ort": "PLZ Ort", "ansprechpartner": "" },\n` +
|
` "empfaenger": { "firma": "Unternehmen", "adresse": "Straße Hausnr. (falls bekannt, sonst leer)", "ort": "PLZ Ort", "ansprechpartner": "" },\n` +
|
||||||
` "betreff": "Bewerbung als …",\n` +
|
` "betreff": "Bewerbung als …",\n` +
|
||||||
@@ -168,9 +165,6 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
` "berufsausbildung": [\n` +
|
` "berufsausbildung": [\n` +
|
||||||
` { "zeitraum": "08.2006 – 07.2009", "abschluss": "Ausbildungsberuf / Abschluss", "institution": "Betrieb/Berufskolleg", "zusatz": "" }\n` +
|
` { "zeitraum": "08.2006 – 07.2009", "abschluss": "Ausbildungsberuf / Abschluss", "institution": "Betrieb/Berufskolleg", "zusatz": "" }\n` +
|
||||||
` ],\n` +
|
` ],\n` +
|
||||||
` "schulbildung": [\n` +
|
|
||||||
` { "zeitraum": "08.2000 – 07.2006", "abschluss": "Schulabschluss (z. B. Fachoberschulreife)", "institution": "Schule, Ort", "zusatz": "" }\n` +
|
|
||||||
` ],\n` +
|
|
||||||
` "weiterbildungen": [],\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` +
|
||||||
@@ -198,8 +192,9 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
`${skeleton}\n\n` +
|
`${skeleton}\n\n` +
|
||||||
`Vorgaben (deutscher Lebenslauf nach aktueller Norm/Best Practice):\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 NUR übernehmen, wenn in den Basis-Unterlagen ` +
|
||||||
`wenn in den Basis-Unterlagen vorhanden; sonst leerer String. Nichts erfinden.\n` +
|
`vorhanden; sonst leerer String. Nichts erfinden. Keine persönlichen Angaben (Geburtsdatum, ` +
|
||||||
|
`Führerschein, Familienstand, Nationalität o. Ä.) — diese Felder bleiben leer.\n` +
|
||||||
`- anschreiben.empfaenger: Empfängerblock. firma = Unternehmen; adresse = Straße + Hausnr. ` +
|
`- anschreiben.empfaenger: Empfängerblock. firma = Unternehmen; adresse = Straße + Hausnr. ` +
|
||||||
`(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` +
|
`(nur falls aus Stellenanzeige oder Notizen bekannt, sonst leer); ort = "PLZ Ort"; ` +
|
||||||
`ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` +
|
`ansprechpartner = konkrete Kontaktperson (nur falls bekannt, sonst leer). Nutze eine in den ` +
|
||||||
@@ -213,16 +208,21 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
`fortgesetzt), z. B. "mit …" oder "als …" — außer das erste Wort ist ein Substantiv, ein Eigenname oder die ` +
|
`fortgesetzt), z. B. "mit …" oder "als …" — außer das erste Wort ist ein Substantiv, ein Eigenname oder die ` +
|
||||||
`Höflichkeitsform "Ihre/Ihr/Ihnen". Alle WEITEREN Absätze sind neue Sätze und beginnen normal mit GROSSbuchstaben.\n` +
|
`Höflichkeitsform "Ihre/Ihr/Ihnen". Alle WEITEREN Absätze sind neue Sätze und beginnen normal mit GROSSbuchstaben.\n` +
|
||||||
`- profil: 2–3 Sätze Kurzprofil, konkret auf die Zielstelle zugeschnitten (der individuelle Text zur Stelle).\n` +
|
`- profil: 2–3 Sätze Kurzprofil, konkret auf die Zielstelle zugeschnitten (der individuelle Text zur Stelle).\n` +
|
||||||
`- berufserfahrung: ALLE Stationen aus den Unterlagen, neueste zuerst. "firma" = "Arbeitgeber, Ort". ` +
|
`- berufserfahrung: ALLE Stationen aus den Unterlagen, neueste zuerst — lückenloser Verlauf, ` +
|
||||||
`Nur die 2–3 jüngsten/relevantesten erhalten je 2–3 knappe Stichpunkte ("punkte"), ältere bekommen ein leeres Array.\n` +
|
`keine Station weglassen, die sonst eine Lücke hinterlässt (Überschneidungen sind ok). ` +
|
||||||
`- Bildung korrekt einordnen: Hochschulstudium → studium; Berufsausbildung/Ausbildungsberuf → ` +
|
`"firma" = "Arbeitgeber, Ort". Je jünger/relevanter eine Station, desto mehr Stichpunkte: die ` +
|
||||||
`berufsausbildung; schulische Abschlüsse (Mittlere Reife/Fachoberschulreife, Fachhochschulreife, Abitur) → schulbildung; ` +
|
`2–3 jüngsten erhalten je 2–3 knappe "punkte", mittelalte 0–1 Stichpunkt, ältere ein leeres ` +
|
||||||
`Zertifikate/Fortbildungen → weiterbildungen. Grundschule weglassen. ` +
|
`Array (nur noch Zeile mit Zeitraum, Titel, Firma). Irrelevante alte Stationen dürfen als ` +
|
||||||
`WICHTIG: Wenn ein schulischer Abschluss (z. B. Fachhochschulreife) an einem Berufskolleg gemeinsam mit einer ` +
|
`letzte knappe Zeile stehenbleiben, wenn sie für einen lückenlosen Verlauf nötig sind — nicht ausschmücken.\n` +
|
||||||
`Berufsausbildung erworben wurde, führe diesen schulischen Abschluss ZUSÄTZLICH als eigenen Eintrag unter ` +
|
`- Bildung: Hochschulstudium → studium; Berufsausbildung/Ausbildungsberuf → berufsausbildung; ` +
|
||||||
`schulbildung auf (nicht nur als Zusatz der Berufsausbildung).\n` +
|
`Zertifikate/Fortbildungen → weiterbildungen. Schulabschlüsse (Mittlere Reife, Fachoberschulreife, ` +
|
||||||
`- kenntnisse: max. 12 prägnante Stichworte, relevanteste zuerst.\n` +
|
`Fachhochschulreife, Abitur u. ä.) NICHT in den Lebenslauf aufnehmen — komplett weglassen. ` +
|
||||||
`- sprachen, hobbys, weiterbildungen: nur falls in den Unterlagen vorhanden.\n\n` +
|
`Grundschule ebenfalls weglassen.\n` +
|
||||||
|
`- Nur auf die Stelle bezogene Inhalte: alles weglassen, was nicht zur Zielstelle passt. ` +
|
||||||
|
`kenntnisse: max. 12 prägnante, zur Stelle passende Stichworte, relevanteste zuerst.\n` +
|
||||||
|
`- sprachen, weiterbildungen: nur falls in den Unterlagen vorhanden UND für die Stelle relevant.\n` +
|
||||||
|
`- hobbys: NUR aufnehmen, wenn sie direkt für den Beruf nützlich sind (z. B. Open-Source-Engagement, ` +
|
||||||
|
`vereinsinterne IT/Verwaltung, ehrenamtliche Projektarbeit mit Bezug zur Stelle); sonst komplett weglassen.\n\n` +
|
||||||
`WICHTIG — keine HR-Red-Flags:\n` +
|
`WICHTIG — keine HR-Red-Flags:\n` +
|
||||||
`- Lückenloser Zeitverlauf: übernimm ALLE Zeiträume aus den Unterlagen, sodass keine unerklärten ` +
|
`- 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` +
|
`Lücken entstehen. Lasse keine Stationen weg, die sonst eine Lücke hinterlassen (Überschneidungen sind ok).\n` +
|
||||||
@@ -477,50 +477,58 @@ function rule(doc, S, dry, y, x1, x2, color, weight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Resume (Lebenslauf) — white page: centered header + two columns below
|
// Resume (Lebenslauf) — clean single-column layout
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
const RV = { mx: 18, top: 15, bottom: 13 };
|
const RV = { mx: 20, top: 16, bottom: 14 };
|
||||||
const RV_FULL = PAGE.w - RV.mx * 2; // 174
|
const RV_W = PAGE.w - RV.mx * 2; // 170 content width
|
||||||
const RV_CENTER = PAGE.w / 2; // 105
|
const RV_R = PAGE.w - RV.mx; // 190 right edge (right-aligned dates)
|
||||||
const RV_LEFT_X = RV.mx; // 18
|
const RV_DATE_W = 34; // reserved right zone for the date column
|
||||||
const RV_LEFT_W = 104;
|
|
||||||
const RV_GAP = 8;
|
|
||||||
const RV_RIGHT_X = RV.mx + RV_LEFT_W + RV_GAP; // 130
|
|
||||||
const RV_RIGHT_W = (PAGE.w - RV.mx) - RV_RIGHT_X; // 62
|
|
||||||
|
|
||||||
const RC = {
|
const RC = {
|
||||||
ink: [26, 26, 28],
|
ink: [26, 26, 26],
|
||||||
sub: [92, 98, 106],
|
sub: [92, 98, 106],
|
||||||
line: [30, 30, 32], // dark separators (header / profile)
|
hair: [206, 208, 212],
|
||||||
hair: [208, 210, 214], // light section underlines
|
|
||||||
highlight: [200, 226, 200], // soft green behind the title
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function centerText(doc, S, dry, cur, text, { pt, style = 'normal', color = RC.ink, factor = 1.2 }) {
|
// Section heading: bold uppercase tracked label with a thin hairline beneath.
|
||||||
if (!text) return;
|
function cvHeading(doc, S, dry, cur, title) {
|
||||||
doc.setFont('helvetica', style);
|
const pt = 11.5;
|
||||||
|
cur.y += 2.5 * S;
|
||||||
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(pt * S);
|
doc.setFontSize(pt * S);
|
||||||
const lines = doc.splitTextToSize(String(text), RV_FULL);
|
if (!dry) {
|
||||||
for (const ln of lines) {
|
doc.setCharSpace(0.4 * S);
|
||||||
if (!dry) { doc.setTextColor(color[0], color[1], color[2]); doc.text(ln, RV_CENTER, cur.y + pt * S * PT * 0.76, { align: 'center' }); }
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
cur.y += pt * S * PT * factor;
|
doc.text(String(title).toUpperCase(), RV.mx, cur.y + pt * S * PT * 0.76);
|
||||||
|
doc.setCharSpace(0);
|
||||||
|
doc.setDrawColor(RC.hair[0], RC.hair[1], RC.hair[2]);
|
||||||
|
doc.setLineWidth(0.3 * S);
|
||||||
|
doc.line(RV.mx, cur.y + pt * S * PT * 0.98, RV_R, cur.y + pt * S * PT * 0.98);
|
||||||
}
|
}
|
||||||
|
cur.y += pt * S * PT + 3.2 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Column section heading: bold uppercase, tracked, with a thin underline.
|
// One row: bold title (left) + muted date (right-aligned) on a single baseline.
|
||||||
function cvHeading(doc, S, dry, cur, title, x, w) {
|
function cvRow(doc, S, dry, cur, leftText, rightText, leftPt) {
|
||||||
cur.y += 3 * S;
|
leftPt = leftPt || 11;
|
||||||
|
const rightPt = 9.5;
|
||||||
|
const leftW = RV_W - RV_DATE_W;
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.setFontSize(11.5 * S);
|
doc.setFontSize(leftPt * S);
|
||||||
|
const lines = doc.splitTextToSize(String(leftText || ''), leftW);
|
||||||
|
const lineH = leftPt * S * PT * 1.18;
|
||||||
if (!dry) {
|
if (!dry) {
|
||||||
doc.setCharSpace(0.5 * S);
|
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
||||||
doc.text(String(title).toUpperCase(), x, cur.y + 11.5 * S * PT * 0.76);
|
lines.forEach((ln, i) => doc.text(ln, RV.mx, cur.y + i * lineH + leftPt * S * PT * 0.76));
|
||||||
doc.setCharSpace(0);
|
if (rightText) {
|
||||||
|
doc.setFont('helvetica', 'normal');
|
||||||
|
doc.setFontSize(rightPt * S);
|
||||||
|
doc.setTextColor(RC.sub[0], RC.sub[1], RC.sub[2]);
|
||||||
|
doc.text(String(rightText), RV_R, cur.y + rightPt * S * PT * 0.76, { align: 'right' });
|
||||||
}
|
}
|
||||||
cur.y += 11.5 * S * PT + 1.8 * S;
|
}
|
||||||
cur.y += 2.4 * S;
|
cur.y += lineH * Math.max(1, lines.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
||||||
@@ -542,164 +550,65 @@ function cvBullets(doc, S, dry, cur, items, x, w, pt = 9.3) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvExperience(doc, S, dry, cur, e, x, w) {
|
function cvExperience(doc, S, dry, cur, e) {
|
||||||
const meta = [e.firma, e.zeitraum].filter(Boolean).join(' | ');
|
cvRow(doc, S, dry, cur, e.titel, e.zeitraum, 11);
|
||||||
write(doc, S, dry, cur, meta, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.1 });
|
if (e.firma) write(doc, S, dry, cur, e.firma, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
||||||
write(doc, S, dry, cur, e.titel, { pt: 11.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.15 });
|
if (e.punkte && e.punkte.length) { cur.y += 0.8 * S; cvBullets(doc, S, dry, cur, e.punkte, RV.mx, RV_W); }
|
||||||
if (e.punkte && e.punkte.length) { cur.y += 0.6 * S; cvBullets(doc, S, dry, cur, e.punkte, x, w); }
|
cur.y += 2.6 * S;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cvEducation(doc, S, dry, cur, e) {
|
||||||
|
cvRow(doc, S, dry, cur, e.abschluss, e.zeitraum, 11);
|
||||||
|
if (e.institution) write(doc, S, dry, cur, e.institution, { pt: 9.7, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.18 });
|
||||||
|
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9.5, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
||||||
cur.y += 2.4 * S;
|
cur.y += 2.4 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cvEducation(doc, S, dry, cur, e, x, w) {
|
// Header: name, position subtitle, single muted contact line, thin rule.
|
||||||
const meta = [e.institution, e.zeitraum].filter(Boolean).join(' | ');
|
|
||||||
write(doc, S, dry, cur, meta, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.1 });
|
|
||||||
write(doc, S, dry, cur, e.abschluss, { pt: 11.5, style: 'bold', color: RC.ink, x, width: w, factor: 1.15 });
|
|
||||||
if (e.zusatz) write(doc, S, dry, cur, e.zusatz, { pt: 9.5, color: RC.ink, x, width: w, factor: 1.2 });
|
|
||||||
cur.y += 2.2 * S;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small vector icons drawn in front of the contact rows (phone, mail, pin, web).
|
|
||||||
function drawContactIcon(doc, kind, x, baselineY, S) {
|
|
||||||
const ink = RC.ink;
|
|
||||||
doc.setDrawColor(ink[0], ink[1], ink[2]);
|
|
||||||
doc.setFillColor(ink[0], ink[1], ink[2]);
|
|
||||||
doc.setLineWidth(0.28 * S);
|
|
||||||
const capH = 9 * S * PT * 0.72; // approx. cap height of the contact text
|
|
||||||
const cy = baselineY - capH * 0.5; // visual centre of the line
|
|
||||||
const h = 2.4 * S; // icon height
|
|
||||||
const top = cy - h / 2;
|
|
||||||
switch (kind) {
|
|
||||||
case 'telefon': { // smartphone
|
|
||||||
const w = 1.6 * S;
|
|
||||||
doc.roundedRect(x, top, w, h, 0.4 * S, 0.4 * S, 'S');
|
|
||||||
doc.circle(x + w / 2, top + h - 0.35 * S, 0.22 * S, 'F');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'email': { // envelope
|
|
||||||
const w = 2.6 * S, eh = 1.8 * S, etop = cy - eh / 2;
|
|
||||||
doc.rect(x, etop, w, eh, 'S');
|
|
||||||
doc.line(x, etop, x + w / 2, etop + eh * 0.65);
|
|
||||||
doc.line(x + w, etop, x + w / 2, etop + eh * 0.65);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ort': { // map pin
|
|
||||||
const r = 1.0 * S, cx = x + r, ccy = top + r;
|
|
||||||
doc.circle(cx, ccy, r, 'S');
|
|
||||||
doc.line(cx - r * 0.7, ccy + r * 0.6, cx, top + h);
|
|
||||||
doc.line(cx + r * 0.7, ccy + r * 0.6, cx, top + h);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'web': { // globe
|
|
||||||
const r = 1.2 * S, cx = x + r;
|
|
||||||
doc.circle(cx, cy, r, 'S');
|
|
||||||
doc.ellipse(cx, cy, r * 0.45, r, 'S'); // meridian
|
|
||||||
doc.line(cx - r, cy, cx + r, cy); // equator
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Centered header: position title, name, contact rows with icons.
|
|
||||||
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
function composeCvHeader(doc, S, dry, cur, header, titel) {
|
||||||
const title = String(titel || '').toUpperCase();
|
write(doc, S, dry, cur, header.name, { pt: 20, style: 'bold', color: RC.ink, x: RV.mx, width: RV_W, factor: 1.1 });
|
||||||
if (title) {
|
if (titel) write(doc, S, dry, cur, titel, { pt: 11, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
||||||
doc.setFont('helvetica', 'bold');
|
const contact = [header.email, header.telefon, cityName(header) || header.ort, header.webseite]
|
||||||
doc.setFontSize(22 * S);
|
.filter(Boolean).join(' · ');
|
||||||
const lineH = 22 * S * PT;
|
if (contact) {
|
||||||
if (!dry) {
|
cur.y += 1.4 * S;
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
write(doc, S, dry, cur, contact, { pt: 9, color: RC.sub, x: RV.mx, width: RV_W, factor: 1.2 });
|
||||||
doc.text(title, RV_CENTER, cur.y + lineH * 0.72, { align: 'center', maxWidth: RV_FULL });
|
|
||||||
}
|
}
|
||||||
cur.y += lineH * 1.2;
|
cur.y += 3 * S;
|
||||||
}
|
rule(doc, S, dry, cur.y, RV.mx, RV_R, RC.ink, 0.5);
|
||||||
centerText(doc, S, dry, cur, header.name, { pt: 15.5, style: 'bold', color: RC.ink, factor: 1.2 });
|
|
||||||
cur.y += 2.6 * S;
|
|
||||||
|
|
||||||
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
|
||||||
cur.y += 3.4 * S;
|
|
||||||
const items = [
|
|
||||||
header.telefon ? { icon: 'telefon', text: header.telefon } : null,
|
|
||||||
header.email ? { icon: 'email', text: header.email } : null,
|
|
||||||
(cityName(header) || header.ort) ? { icon: 'ort', text: cityName(header) || header.ort } : null,
|
|
||||||
header.webseite ? { icon: 'web', text: header.webseite } : null,
|
|
||||||
].filter(Boolean);
|
|
||||||
if (items.length) {
|
|
||||||
const cpt = 9;
|
|
||||||
const iconW = 3.4 * S; // icon box width incl. right padding
|
|
||||||
for (const it of items) {
|
|
||||||
doc.setFont('helvetica', 'normal');
|
|
||||||
doc.setFontSize(cpt * S);
|
|
||||||
const tw = doc.getTextWidth(it.text);
|
|
||||||
const totalW = iconW + tw;
|
|
||||||
const leftX = RV_CENTER - totalW / 2;
|
|
||||||
const baseY = cur.y + cpt * S * PT * 0.76;
|
|
||||||
if (!dry) {
|
|
||||||
drawContactIcon(doc, it.icon, leftX, baseY, S);
|
|
||||||
doc.setTextColor(RC.ink[0], RC.ink[1], RC.ink[2]);
|
|
||||||
doc.text(it.text, leftX + iconW, baseY);
|
|
||||||
}
|
|
||||||
cur.y += cpt * S * PT * 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur.y += 2.8 * S;
|
|
||||||
rule(doc, S, dry, cur.y, RV.mx, PAGE.w - RV.mx, RC.line, 0.4);
|
|
||||||
cur.y += 5 * S;
|
cur.y += 5 * S;
|
||||||
}
|
}
|
||||||
|
|
||||||
function composeCvLeft(doc, S, dry, cur, cv) {
|
|
||||||
const x = RV_LEFT_X, w = RV_LEFT_W;
|
|
||||||
if (cv.berufserfahrung.length) {
|
|
||||||
cvHeading(doc, S, dry, cur, 'Berufserfahrung', x, w);
|
|
||||||
cv.berufserfahrung.forEach((e) => cvExperience(doc, S, dry, cur, e, x, w));
|
|
||||||
}
|
|
||||||
[['Studium', cv.studium], ['Berufsausbildung', cv.berufsausbildung], ['Schulbildung', cv.schulbildung], ['Weiterbildungen', cv.weiterbildungen]].forEach(([t, entries]) => {
|
|
||||||
if (!entries || !entries.length) return;
|
|
||||||
cvHeading(doc, S, dry, cur, t, x, w);
|
|
||||||
entries.forEach((e) => cvEducation(doc, S, dry, cur, e, x, w));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function composeCvRight(doc, S, dry, cur, cv, header) {
|
|
||||||
const x = RV_RIGHT_X, w = RV_RIGHT_W;
|
|
||||||
if (cv.kenntnisse.length) {
|
|
||||||
cvHeading(doc, S, dry, cur, 'Fähigkeiten', x, w);
|
|
||||||
cvBullets(doc, S, dry, cur, cv.kenntnisse, x, w);
|
|
||||||
cur.y += 1 * S;
|
|
||||||
}
|
|
||||||
if (cv.sprachen.length) {
|
|
||||||
cvHeading(doc, S, dry, cur, 'Sprachen', x, w);
|
|
||||||
cv.sprachen.forEach((s) => {
|
|
||||||
const t = s.niveau ? `${s.sprache} – ${s.niveau}` : s.sprache;
|
|
||||||
write(doc, S, dry, cur, t, { pt: 9.3, color: RC.ink, x, width: w, factor: 1.4 });
|
|
||||||
});
|
|
||||||
cur.y += 1 * S;
|
|
||||||
}
|
|
||||||
if (cv.hobbys.length) {
|
|
||||||
cvHeading(doc, S, dry, cur, 'Interessen', x, w);
|
|
||||||
cvBullets(doc, S, dry, cur, cv.hobbys, x, w);
|
|
||||||
cur.y += 1 * S;
|
|
||||||
}
|
|
||||||
const pers = [
|
|
||||||
header.geburtsdatum ? `Geboren: ${header.geburtsdatum}` : null,
|
|
||||||
header.fuehrerschein ? `Führerschein: ${header.fuehrerschein}` : null,
|
|
||||||
].filter(Boolean);
|
|
||||||
if (pers.length) {
|
|
||||||
cvHeading(doc, S, dry, cur, 'Persönliches', x, w);
|
|
||||||
pers.forEach((p) => write(doc, S, dry, cur, p, { pt: 9.3, color: RC.ink, x, width: w, factor: 1.4 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function composeCV(doc, S, dry, cur, { cv, header, titel }) {
|
function composeCV(doc, S, dry, cur, { cv, header, titel }) {
|
||||||
composeCvHeader(doc, S, dry, cur, header, titel);
|
composeCvHeader(doc, S, dry, cur, header, titel);
|
||||||
if (cv.profil) {
|
if (cv.profil) {
|
||||||
cvHeading(doc, S, dry, cur, 'Profil', RV.mx, RV_FULL);
|
cvHeading(doc, S, dry, cur, 'Profil');
|
||||||
write(doc, S, dry, cur, cv.profil, { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_FULL, factor: 1.42 });
|
write(doc, S, dry, cur, cv.profil, { pt: 9.7, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.42 });
|
||||||
cur.y += 4 * S;
|
}
|
||||||
|
if (cv.berufserfahrung.length) {
|
||||||
|
cvHeading(doc, S, dry, cur, 'Berufserfahrung');
|
||||||
|
cv.berufserfahrung.forEach((e) => cvExperience(doc, S, dry, cur, e));
|
||||||
|
}
|
||||||
|
[['Studium', cv.studium], ['Berufsausbildung', cv.berufsausbildung], ['Weiterbildungen', cv.weiterbildungen]].forEach(([t, entries]) => {
|
||||||
|
if (!entries || !entries.length) return;
|
||||||
|
cvHeading(doc, S, dry, cur, t);
|
||||||
|
entries.forEach((e) => cvEducation(doc, S, dry, cur, e));
|
||||||
|
});
|
||||||
|
if (cv.kenntnisse.length) {
|
||||||
|
cvHeading(doc, S, dry, cur, 'Fähigkeiten');
|
||||||
|
cvBullets(doc, S, dry, cur, cv.kenntnisse, RV.mx, RV_W);
|
||||||
|
}
|
||||||
|
if (cv.sprachen.length) {
|
||||||
|
cvHeading(doc, S, dry, cur, 'Sprachen');
|
||||||
|
cv.sprachen.forEach((s) => {
|
||||||
|
const t = s.niveau ? `${s.sprache} – ${s.niveau}` : s.sprache;
|
||||||
|
write(doc, S, dry, cur, t, { pt: 9.3, color: RC.ink, x: RV.mx, width: RV_W, factor: 1.3 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (cv.hobbys.length) {
|
||||||
|
cvHeading(doc, S, dry, cur, 'Interessen');
|
||||||
|
cvBullets(doc, S, dry, cur, cv.hobbys, RV.mx, RV_W);
|
||||||
}
|
}
|
||||||
const bodyTop = cur.y;
|
|
||||||
const left = { y: bodyTop }; composeCvLeft(doc, S, dry, left, cv);
|
|
||||||
const right = { y: bodyTop }; composeCvRight(doc, S, dry, right, cv, header);
|
|
||||||
cur.y = Math.max(left.y, right.y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCV(cv, header, titel) {
|
function renderCV(cv, header, titel) {
|
||||||
|
|||||||
Reference in New Issue
Block a user