Let job offers carry a full description into AI generation
Job offers can now be edited in the UI — a large description textarea (any length) plus the other fields — so a full job posting can be pasted before turning it into an application. Taking an offer over copies its complete description into the application's stellenbeschreibung, which is exactly the text handed to the LLM. New primary action "Übernehmen & KI-Unterlagen" creates the application and immediately starts generation; "Nur als Entwurf" keeps the plain draft path. New routes: POST /jobangebote/:id/bearbeiten and /jobangebote/:id/uebernehmen-generieren. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1684,56 +1684,118 @@ initializeDatabase().then(() => {
|
||||
// Convert a job offer into a Bewerbung draft (mirrors the Indeed import flow:
|
||||
// creates a bewerbung with status "Entwurf", records the initial timeline entry,
|
||||
// and links the offer back to it).
|
||||
// Create a Bewerbung draft from an offer (or return the already-linked one).
|
||||
// The offer's FULL description is carried into `stellenbeschreibung`, which is
|
||||
// exactly the text handed to the LLM during generation. Returns the id.
|
||||
async function uebernehmeAngebot(angebot) {
|
||||
if (angebot.verknuepfte_bewerbung_id) return angebot.verknuepfte_bewerbung_id;
|
||||
|
||||
const datum = new Date().toISOString().split('T')[0];
|
||||
const notizParts = [
|
||||
angebot.ort ? `Ort: ${angebot.ort}` : null,
|
||||
angebot.gehalt ? `Gehalt: ${angebot.gehalt}` : null,
|
||||
angebot.kontakt_email ? `Kontakt: ${angebot.kontakt_email}` : null,
|
||||
angebot.quelle_url ? `Quelle: ${angebot.quelle_url}` : null,
|
||||
angebot.quelle ? `Importiert via: ${angebot.quelle}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const result = await dbRun(
|
||||
`INSERT INTO bewerbungen
|
||||
(datum, firma, stelle, art, status, notizen, ort, stellenbeschreibung,
|
||||
quelle_url, email_empfaenger, generierung_status)
|
||||
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
[
|
||||
datum,
|
||||
sanitizeInput(angebot.firma),
|
||||
sanitizeInput(angebot.stelle),
|
||||
sanitizeInput(angebot.art || deriveArt(angebot.quelle_url, null)),
|
||||
notizParts.join('\n'),
|
||||
angebot.ort || '',
|
||||
angebot.beschreibung || '',
|
||||
angebot.quelle_url || '',
|
||||
angebot.kontakt_email || '',
|
||||
]
|
||||
);
|
||||
|
||||
await dbRun(
|
||||
'INSERT INTO status_verlauf (bewerbung_id, datum, status, kommentar) VALUES (?, ?, ?, ?)',
|
||||
[result.lastID, datum, 'Entwurf', `Automatisch aus Jobangebot übernommen (${angebot.quelle || 'drittanbieter'})`]
|
||||
);
|
||||
await dbRun(
|
||||
'UPDATE jobangebote SET verknuepfte_bewerbung_id = ?, status = "uebernommen", updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[result.lastID, angebot.id]
|
||||
);
|
||||
return result.lastID;
|
||||
}
|
||||
|
||||
app.post('/jobangebote/:id/uebernehmen', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const angebot = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [id]);
|
||||
const angebot = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [req.params.id]);
|
||||
if (!angebot) return res.status(404).send('Jobangebot nicht gefunden');
|
||||
if (angebot.verknuepfte_bewerbung_id) {
|
||||
return res.redirect('/bewerbung/' + angebot.verknuepfte_bewerbung_id);
|
||||
}
|
||||
|
||||
const datum = new Date().toISOString().split('T')[0];
|
||||
const notizParts = [
|
||||
angebot.ort ? `Ort: ${angebot.ort}` : null,
|
||||
angebot.gehalt ? `Gehalt: ${angebot.gehalt}` : null,
|
||||
angebot.quelle_url ? `Quelle: ${angebot.quelle_url}` : null,
|
||||
angebot.quelle ? `Importiert via: ${angebot.quelle}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const result = await dbRun(
|
||||
`INSERT INTO bewerbungen
|
||||
(datum, firma, stelle, art, status, notizen, ort, stellenbeschreibung,
|
||||
quelle_url, generierung_status)
|
||||
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
[
|
||||
datum,
|
||||
sanitizeInput(angebot.firma),
|
||||
sanitizeInput(angebot.stelle),
|
||||
sanitizeInput(angebot.art || deriveArt(angebot.quelle_url, null)),
|
||||
notizParts.join('\n'),
|
||||
angebot.ort || '',
|
||||
angebot.beschreibung || '',
|
||||
angebot.quelle_url || '',
|
||||
]
|
||||
);
|
||||
|
||||
await dbRun(
|
||||
'INSERT INTO status_verlauf (bewerbung_id, datum, status, kommentar) VALUES (?, ?, ?, ?)',
|
||||
[result.lastID, datum, 'Entwurf', `Automatisch aus Jobangebot übernommen (${angebot.quelle || 'drittanbieter'})`]
|
||||
);
|
||||
await dbRun(
|
||||
'UPDATE jobangebote SET verknuepfte_bewerbung_id = ?, status = "uebernommen", updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[result.lastID, id]
|
||||
);
|
||||
|
||||
res.redirect('/bewerbung/' + result.lastID);
|
||||
const bewerbungId = await uebernehmeAngebot(angebot);
|
||||
res.redirect('/bewerbung/' + bewerbungId);
|
||||
} catch (error) {
|
||||
console.error('Error converting job offer:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Ü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
|
||||
// (any length) before turning it into an application.
|
||||
app.post('/jobangebote/:id/bearbeiten', 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 b = req.body || {};
|
||||
const quelleUrl = sanitizeInput(b.quelle_url || '');
|
||||
await dbRun(
|
||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, gehalt = ?,
|
||||
beschreibung = ?, kontakt_email = ?, quelle_url = ?, anzeige_datum = ?,
|
||||
url_norm = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
sanitizeInput((b.firma || '').trim()) || angebot.firma,
|
||||
sanitizeInput((b.stelle || '').trim()) || angebot.stelle,
|
||||
sanitizeInput(b.ort || ''),
|
||||
sanitizeInput(b.gehalt || ''),
|
||||
sanitizeInput(b.beschreibung || ''),
|
||||
sanitizeInput(b.kontakt_email || ''),
|
||||
quelleUrl,
|
||||
sanitizeInput(b.anzeige_datum || ''),
|
||||
blacklist.normalizeUrl(b.quelle_url || '') || null,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
res.redirect('/jobangebote');
|
||||
} catch (error) {
|
||||
console.error('Error editing job offer:', error);
|
||||
res.status(500).send('Serverfehler');
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a job offer. Deleting always blacklists it first, so the same offer
|
||||
// can never be ingested/listed again (the requirement: never appears twice,
|
||||
// even after deletion). The entry can be removed later on /blacklist.
|
||||
|
||||
Reference in New Issue
Block a user