Choose enclosed attachments before generating; drop instant-generate
The generate form now lets the user pick which extra attachments (basis_anhaenge) to enclose — none selected by default. Only the chosen ones are attached, and their names are passed to the LLM so the cover letter's "Anlagen" list and wording reflect exactly what is enclosed. Job offers keep only "Als Bewerbung übernehmen" (draft first); the "Übernehmen & KI-Unterlagen" button and its route are removed. REST API: POST /applications/:id/generate accepts an optional `anlagen` array of attachment IDs (Swagger updated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -387,7 +387,12 @@ function createExternalApi(deps) {
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
runGeneration(id);
|
// Optional: IDs of static attachments (basis_anhaenge) to enclose; none
|
||||||
|
// by default. Also reflected in the cover letter's "Anlagen" list.
|
||||||
|
const anlagenIds = Array.isArray((req.body || {}).anlagen)
|
||||||
|
? req.body.anlagen.map((v) => parseInt(v, 10)).filter((n) => !Number.isNaN(n))
|
||||||
|
: [];
|
||||||
|
runGeneration(id, { anlagenIds });
|
||||||
res.status(202).json({ success: true });
|
res.status(202).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API generate error:', error);
|
console.error('API generate error:', error);
|
||||||
|
|||||||
+14
-2
@@ -127,7 +127,7 @@ const OUTPUT_SCHEMA = {
|
|||||||
required: ['headline', 'kontakt', 'anschreiben', 'email', 'lebenslauf'],
|
required: ['headline', 'kontakt', 'anschreiben', 'email', 'lebenslauf'],
|
||||||
};
|
};
|
||||||
|
|
||||||
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '' }) {
|
async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen = '', zusatzAnlagen = [] }) {
|
||||||
const apiKey = process.env.OLLAMA_API_KEY;
|
const apiKey = process.env.OLLAMA_API_KEY;
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -215,11 +215,19 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
`${llmNotizen.trim()}\n\n`
|
`${llmNotizen.trim()}\n\n`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
// Attachments actually enclosed with this application. The Lebenslauf is
|
||||||
|
// always part of the set; the extra items are the ones the user selected.
|
||||||
|
const anlagenListe = ['Lebenslauf', ...(zusatzAnlagen || []).filter(Boolean)];
|
||||||
|
const anlagenText =
|
||||||
|
`# Beigefügte Anlagen (genau diese Dokumente liegen der Bewerbung bei)\n` +
|
||||||
|
anlagenListe.map((a) => `- ${a}`).join('\n') + `\n\n`;
|
||||||
|
|
||||||
const userPrompt =
|
const userPrompt =
|
||||||
`# Bewerberdaten\n${bewerber}\n\n` +
|
`# Bewerberdaten\n${bewerber}\n\n` +
|
||||||
`# Basis-Unterlagen des Bewerbers (Faktengrundlage — NUR diese Fakten verwenden)\n${basisText}\n\n` +
|
`# Basis-Unterlagen des Bewerbers (Faktengrundlage — NUR diese Fakten verwenden)\n${basisText}\n\n` +
|
||||||
`# Zielstelle\n${stelleText}\n\n` +
|
`# Zielstelle\n${stelleText}\n\n` +
|
||||||
notizenText +
|
notizenText +
|
||||||
|
anlagenText +
|
||||||
`# Aufgabe\n` +
|
`# Aufgabe\n` +
|
||||||
`Erzeuge die TEXTE für ein Anschreiben und einen Lebenslauf, passgenau auf diese ` +
|
`Erzeuge die TEXTE für ein Anschreiben und einen Lebenslauf, passgenau auf diese ` +
|
||||||
`Stelle zugeschnitten. Layout/Design ist bereits vorgegeben — liefere nur die Inhalte. ` +
|
`Stelle zugeschnitten. Layout/Design ist bereits vorgegeben — liefere nur die Inhalte. ` +
|
||||||
@@ -257,6 +265,10 @@ async function generateTailoredTexts({ job, basisDokumente, settings, llmNotizen
|
|||||||
`Nenne den AKTUELLEN Arbeitgeber NIE beim Namen im Anschreiben (keine Firmenbezeichnung wie z. B. ` +
|
`Nenne den AKTUELLEN Arbeitgeber NIE beim Namen im Anschreiben (keine Firmenbezeichnung wie z. B. ` +
|
||||||
`"IT-Problemlöser GmbH") — beschreibe höchstens die Tätigkeit/Rolle allgemein, falls überhaupt relevant. ` +
|
`"IT-Problemlöser GmbH") — beschreibe höchstens die Tätigkeit/Rolle allgemein, falls überhaupt relevant. ` +
|
||||||
`Der Lebenslauf nennt die Arbeitgeber wie gehabt; diese Regel gilt nur für das Anschreiben. ` +
|
`Der Lebenslauf nennt die Arbeitgeber wie gehabt; diese Regel gilt nur für das Anschreiben. ` +
|
||||||
|
`Berücksichtige die beigefügten Anlagen (siehe Abschnitt "Beigefügte Anlagen"): Du darfst im Text ` +
|
||||||
|
`natürlich auf relevante Anlagen verweisen (z. B. "wie meine beigefügten Arbeitszeugnisse zeigen"), ` +
|
||||||
|
`aber erfinde KEINE Anlagen, die dort nicht aufgeführt sind, und stütze keine Aussage auf einen Nachweis, ` +
|
||||||
|
`der nicht beiliegt. Erstelle KEINE eigene "Anlagen:"-Auflistung im Text — die Anlagenliste wird separat erzeugt. ` +
|
||||||
`Nur der ERSTE Absatz beginnt mit einem KLEINbuchstaben (die Anrede endet mit Komma, der Satz wird ` +
|
`Nur der ERSTE Absatz beginnt mit einem KLEINbuchstaben (die Anrede endet mit Komma, der Satz wird ` +
|
||||||
`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` +
|
||||||
@@ -1129,7 +1141,7 @@ function hasLebenslauf(l) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateApplicationDocuments({ job, basisDokumente, settings, zusatzAnlagen = [], llmNotizen = '', signatur = null, bewerbungsfoto = null }) {
|
async function generateApplicationDocuments({ job, basisDokumente, settings, zusatzAnlagen = [], llmNotizen = '', signatur = null, bewerbungsfoto = null }) {
|
||||||
const data = await generateTailoredTexts({ job, basisDokumente, settings, llmNotizen });
|
const data = await generateTailoredTexts({ job, basisDokumente, settings, llmNotizen, zusatzAnlagen });
|
||||||
const headline = data.headline || job.stelle || '';
|
const headline = data.headline || job.stelle || '';
|
||||||
const header = buildHeader(settings, data.kontakt, headline);
|
const header = buildHeader(settings, data.kontakt, headline);
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,13 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
llm_notizen: { type: 'string', description: 'Freitext-Kontext für die KI (optional)' },
|
llm_notizen: { type: 'string', description: 'Freitext-Kontext für die KI (optional)' },
|
||||||
|
anlagen: {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'integer' },
|
||||||
|
description:
|
||||||
|
'IDs der zusätzlich beizulegenden statischen Anlagen (basis_anhaenge). ' +
|
||||||
|
'Standard: keine. Die Auswahl erscheint auch im Anschreiben unter "Anlagen".',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ async function attachVerlauf(applications) {
|
|||||||
// Run the AI document generation for one application (async, fire-and-forget).
|
// Run the AI document generation for one application (async, fire-and-forget).
|
||||||
// Loads the base documents + user settings, asks the LLM to tailor them to the
|
// Loads the base documents + user settings, asks the LLM to tailor them to the
|
||||||
// job, writes the resulting PDFs to disk and links them as attachments.
|
// job, writes the resulting PDFs to disk and links them as attachments.
|
||||||
async function runGeneration(bewerbungId) {
|
async function runGeneration(bewerbungId, options = {}) {
|
||||||
try {
|
try {
|
||||||
const bewerbung = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [bewerbungId]);
|
const bewerbung = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [bewerbungId]);
|
||||||
if (!bewerbung) return;
|
if (!bewerbung) return;
|
||||||
@@ -443,6 +443,10 @@ async function runGeneration(bewerbungId) {
|
|||||||
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
|
const basisAnhaenge = await dbAll('SELECT * FROM basis_anhaenge ORDER BY id ASC');
|
||||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||||
|
|
||||||
|
// Only the explicitly selected extra attachments are enclosed (default: none).
|
||||||
|
const anlagenIds = Array.isArray(options.anlagenIds) ? options.anlagenIds.map(Number) : [];
|
||||||
|
const selectedAnhaenge = basisAnhaenge.filter((a) => anlagenIds.includes(a.id));
|
||||||
|
|
||||||
const { documents, email } = await generateApplicationDocuments({
|
const { documents, email } = await generateApplicationDocuments({
|
||||||
job: {
|
job: {
|
||||||
firma: bewerbung.firma,
|
firma: bewerbung.firma,
|
||||||
@@ -453,8 +457,9 @@ async function runGeneration(bewerbungId) {
|
|||||||
},
|
},
|
||||||
basisDokumente,
|
basisDokumente,
|
||||||
settings,
|
settings,
|
||||||
// Names of the static attachments so the cover letter can list them under "Anlagen".
|
// Names of the selected attachments so the cover letter (and the LLM) lists
|
||||||
zusatzAnlagen: basisAnhaenge.map((a) => a.name || a.dateiname),
|
// exactly these under "Anlagen".
|
||||||
|
zusatzAnlagen: selectedAnhaenge.map((a) => a.name || a.dateiname),
|
||||||
// Free-text notes (company address, contact person, extra context) for the LLM.
|
// Free-text notes (company address, contact person, extra context) for the LLM.
|
||||||
llmNotizen: bewerbung.llm_notizen || '',
|
llmNotizen: bewerbung.llm_notizen || '',
|
||||||
// Signature image placed under the closing salutation (instead of the typed name).
|
// Signature image placed under the closing salutation (instead of the typed name).
|
||||||
@@ -478,8 +483,8 @@ async function runGeneration(bewerbungId) {
|
|||||||
await storeAnhang(doc.name, doc.filename, doc.mime, doc.buffer);
|
await storeAnhang(doc.name, doc.filename, doc.mime, doc.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static extra attachments (e.g. Zeugnisse) — copied as-is
|
// Selected extra attachments (e.g. Zeugnisse) — copied as-is
|
||||||
for (const ba of basisAnhaenge) {
|
for (const ba of selectedAnhaenge) {
|
||||||
const src = path.join(basisAnhaengeDir, ba.pfad);
|
const src = path.join(basisAnhaengeDir, ba.pfad);
|
||||||
if (!fs.existsSync(src)) continue;
|
if (!fs.existsSync(src)) continue;
|
||||||
await storeAnhang(ba.name || ba.dateiname, ba.dateiname, ba.mime || 'application/octet-stream', fs.readFileSync(src));
|
await storeAnhang(ba.name || ba.dateiname, ba.dateiname, ba.mime || 'application/octet-stream', fs.readFileSync(src));
|
||||||
@@ -1078,6 +1083,8 @@ initializeDatabase().then(() => {
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
const basisCountRow = await dbGet('SELECT COUNT(*) as count FROM basis_dokumente');
|
const basisCountRow = await dbGet('SELECT COUNT(*) as count FROM basis_dokumente');
|
||||||
|
// Available static attachments (Zeugnisse etc.) to optionally enclose.
|
||||||
|
const basisAnhaenge = await dbAll('SELECT id, name, dateiname FROM basis_anhaenge ORDER BY id ASC');
|
||||||
|
|
||||||
// E-Mail correspondence (sent + received), oldest first, with attachments.
|
// E-Mail correspondence (sent + received), oldest first, with attachments.
|
||||||
const emails = await dbAll(
|
const emails = await dbAll(
|
||||||
@@ -1112,6 +1119,7 @@ initializeDatabase().then(() => {
|
|||||||
mailError: req.query.mailerror ? String(req.query.mailerror) : '',
|
mailError: req.query.mailerror ? String(req.query.mailerror) : '',
|
||||||
mailOk: req.query.mailok ? String(req.query.mailok) : '',
|
mailOk: req.query.mailok ? String(req.query.mailok) : '',
|
||||||
basisCount: basisCountRow ? basisCountRow.count : 0,
|
basisCount: basisCountRow ? basisCountRow.count : 0,
|
||||||
|
basisAnhaenge,
|
||||||
artOptions: ART_OPTIONS,
|
artOptions: ART_OPTIONS,
|
||||||
statusOptions: STATUS_OPTIONS,
|
statusOptions: STATUS_OPTIONS,
|
||||||
hideSettings: true
|
hideSettings: true
|
||||||
@@ -1642,7 +1650,9 @@ initializeDatabase().then(() => {
|
|||||||
await dbRun('DELETE FROM anhaenge WHERE bewerbung_id = ?', [id]);
|
await dbRun('DELETE FROM anhaenge WHERE bewerbung_id = ?', [id]);
|
||||||
await dbRun("UPDATE bewerbungen SET generierung_status = 'ausstehend', generierung_fehler = NULL WHERE id = ?", [id]);
|
await dbRun("UPDATE bewerbungen SET generierung_status = 'ausstehend', generierung_fehler = NULL WHERE id = ?", [id]);
|
||||||
|
|
||||||
runGeneration(id);
|
// Selected extra attachments (checkbox values); none by default.
|
||||||
|
const anlagenIds = [].concat(req.body.anlage || []).map((v) => parseInt(v, 10)).filter((n) => !Number.isNaN(n));
|
||||||
|
runGeneration(id, { anlagenIds });
|
||||||
res.redirect('/bewerbung/' + id);
|
res.redirect('/bewerbung/' + id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating documents:', error);
|
console.error('Error generating documents:', error);
|
||||||
@@ -1784,28 +1794,6 @@ initializeDatabase().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Übernehmen AND immediately kick off the AI generation, then jump to the
|
|
||||||
// application page where the progress is shown.
|
|
||||||
app.post('/jobangebote/:id/uebernehmen-generieren', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const angebot = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [req.params.id]);
|
|
||||||
if (!angebot) return res.status(404).send('Jobangebot nicht gefunden');
|
|
||||||
const bewerbungId = await uebernehmeAngebot(angebot);
|
|
||||||
|
|
||||||
// Clear any previously generated files, then (re)start generation.
|
|
||||||
const alte = await dbAll('SELECT * FROM anhaenge WHERE bewerbung_id = ?', [bewerbungId]);
|
|
||||||
for (const a of alte) fs.promises.unlink(path.join(anhaengeDir, a.pfad)).catch(() => {});
|
|
||||||
await dbRun('DELETE FROM anhaenge WHERE bewerbung_id = ?', [bewerbungId]);
|
|
||||||
await dbRun("UPDATE bewerbungen SET generierung_status = 'ausstehend', generierung_fehler = NULL WHERE id = ?", [bewerbungId]);
|
|
||||||
runGeneration(bewerbungId);
|
|
||||||
|
|
||||||
res.redirect('/bewerbung/' + bewerbungId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error converting + generating from job offer:', error);
|
|
||||||
res.status(500).send('Serverfehler');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Edit an offer's fields — mainly to paste/adjust the full job description
|
// Edit an offer's fields — mainly to paste/adjust the full job description
|
||||||
// (any length) before turning it into an application.
|
// (any length) before turning it into an application.
|
||||||
app.post('/jobangebote/:id/bearbeiten', async (req, res) => {
|
app.post('/jobangebote/:id/bearbeiten', async (req, res) => {
|
||||||
|
|||||||
+31
-1
@@ -233,7 +233,37 @@
|
|||||||
<textarea name="llm_notizen" rows="4"
|
<textarea name="llm_notizen" rows="4"
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white leading-relaxed"
|
||||||
placeholder="z. B. Musterfirma GmbH Musterstraße 12 45128 Essen Ansprechpartner: Frau Müller"><%= application.llm_notizen || '' %></textarea>
|
placeholder="z. B. Musterfirma GmbH Musterstraße 12 45128 Essen Ansprechpartner: Frau Müller"><%= application.llm_notizen || '' %></textarea>
|
||||||
<div class="flex justify-end mt-3">
|
|
||||||
|
<!-- Optional extra attachments to enclose (none by default). -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Zusätzliche Anlagen</label>
|
||||||
|
<% if (basisAnhaenge && basisAnhaenge.length) { %>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Wähle aus, welche Anlagen (z. B. Zeugnisse) mitgeschickt werden. Standardmäßig
|
||||||
|
ist nichts ausgewählt; die Auswahl wird auch im Anschreiben unter „Anlagen“ berücksichtigt.
|
||||||
|
</p>
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<% basisAnhaenge.forEach(function(ba){ %>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
<input type="checkbox" name="anlage" value="<%= ba.id %>"
|
||||||
|
class="rounded border-gray-300 dark:border-gray-600 text-blue-600">
|
||||||
|
<%= ba.name || ba.dateiname %>
|
||||||
|
<% if (ba.name && ba.dateiname && ba.name !== ba.dateiname) { %>
|
||||||
|
<span class="text-xs text-gray-400">(<%= ba.dateiname %>)</span>
|
||||||
|
<% } %>
|
||||||
|
</label>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<p class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Keine zusätzlichen Anlagen hinterlegt. Unter
|
||||||
|
<a href="/vorlagen" class="text-blue-600 dark:text-blue-400 hover:underline">Vorlagen</a>
|
||||||
|
kannst du statische Anlagen (z. B. Zeugnisse) hochladen.
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
<button type="submit" <%= genStatus === 'ausstehend' ? 'disabled' : '' %>
|
<button type="submit" <%= genStatus === 'ausstehend' ? 'disabled' : '' %>
|
||||||
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-md transition-colors">
|
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-md transition-colors">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
+3
-11
@@ -143,20 +143,12 @@
|
|||||||
Zur Bewerbung
|
Zur Bewerbung
|
||||||
</a>
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<form action="/jobangebote/<%= j.id %>/uebernehmen-generieren" method="POST">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors w-full"
|
|
||||||
title="Bewerbung anlegen und sofort KI-Unterlagen (Anschreiben + Lebenslauf) generieren">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
|
|
||||||
Übernehmen & KI-Unterlagen
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action="/jobangebote/<%= j.id %>/uebernehmen" method="POST">
|
<form action="/jobangebote/<%= j.id %>/uebernehmen" method="POST">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors w-full"
|
class="inline-flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors w-full"
|
||||||
title="Nur einen Bewerbungs-Entwurf anlegen (ohne KI-Generierung)">
|
title="Aus diesem Angebot einen Bewerbungs-Entwurf anlegen">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
|
||||||
Nur als Entwurf
|
Als Bewerbung übernehmen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
Reference in New Issue
Block a user