diff --git a/server.js b/server.js index 7d11477..2d0594d 100644 --- a/server.js +++ b/server.js @@ -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. diff --git a/views/jobangebote.ejs b/views/jobangebote.ejs index 6b32698..eb91eca 100644 --- a/views/jobangebote.ejs +++ b/views/jobangebote.ejs @@ -120,12 +120,20 @@ Zur Bewerbung <% } else { %> -
+ +
+
+
<% } %> @@ -139,6 +147,54 @@ + + +
+ + + Beschreibung / Details bearbeiten + +
+
+ + + + + + + +
+ + +
+
<% }); %>