Confirm before sending; add address + contact to job offers
"Bewerbung senden" now asks for an extra confirmation (with the recipient) before dispatching. Job offers gain adresse and ansprechpartner fields — shown, editable, and carried through the REST API upsert (Swagger updated). Taking an offer over now writes the employer address (street, number, city) and the contact person into the application's AI notes (llm_notizen), so the LLM can use them for the letter's Anschriftfeld and salutation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -614,6 +614,8 @@ function createExternalApi(deps) {
|
|||||||
sanitizeInput(b.firma),
|
sanitizeInput(b.firma),
|
||||||
sanitizeInput(b.stelle),
|
sanitizeInput(b.stelle),
|
||||||
sanitizeInput(b.ort || ''),
|
sanitizeInput(b.ort || ''),
|
||||||
|
sanitizeInput(b.adresse || ''),
|
||||||
|
sanitizeInput(b.ansprechpartner || ''),
|
||||||
sanitizeInput(b.gehalt || ''),
|
sanitizeInput(b.gehalt || ''),
|
||||||
sanitizeInput(b.beschreibung || ''),
|
sanitizeInput(b.beschreibung || ''),
|
||||||
sanitizeInput(b.quelle_url || ''),
|
sanitizeInput(b.quelle_url || ''),
|
||||||
@@ -642,7 +644,7 @@ function createExternalApi(deps) {
|
|||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await dbRun(
|
await dbRun(
|
||||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, gehalt = ?,
|
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
||||||
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?,
|
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?,
|
||||||
url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||||
[...fields, existing.id]
|
[...fields, existing.id]
|
||||||
@@ -653,8 +655,8 @@ function createExternalApi(deps) {
|
|||||||
|
|
||||||
const result = await dbRun(
|
const result = await dbRun(
|
||||||
`INSERT INTO jobangebote
|
`INSERT INTO jobangebote
|
||||||
(external_id, quelle, firma, stelle, ort, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, url_norm)
|
(external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, url_norm)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[externalId, quelle, ...fields]
|
[externalId, quelle, ...fields]
|
||||||
);
|
);
|
||||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]);
|
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]);
|
||||||
|
|||||||
@@ -855,6 +855,8 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
firma: { type: 'string' },
|
firma: { type: 'string' },
|
||||||
stelle: { type: 'string' },
|
stelle: { type: 'string' },
|
||||||
ort: { type: 'string', nullable: true },
|
ort: { type: 'string', nullable: true },
|
||||||
|
adresse: { type: 'string', nullable: true, description: 'Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort)' },
|
||||||
|
ansprechpartner: { type: 'string', nullable: true, description: 'Name des Ansprechpartners für die Bewerbung' },
|
||||||
gehalt: { type: 'string', nullable: true },
|
gehalt: { type: 'string', nullable: true },
|
||||||
beschreibung: { type: 'string', nullable: true },
|
beschreibung: { type: 'string', nullable: true },
|
||||||
quelle_url: { type: 'string', nullable: true },
|
quelle_url: { type: 'string', nullable: true },
|
||||||
@@ -877,6 +879,8 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
firma: { type: 'string' },
|
firma: { type: 'string' },
|
||||||
stelle: { type: 'string' },
|
stelle: { type: 'string' },
|
||||||
ort: { type: 'string' },
|
ort: { type: 'string' },
|
||||||
|
adresse: { type: 'string', description: 'Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort)' },
|
||||||
|
ansprechpartner: { type: 'string', description: 'Name des Ansprechpartners für die Bewerbung' },
|
||||||
gehalt: { type: 'string' },
|
gehalt: { type: 'string' },
|
||||||
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
||||||
quelle_url: { type: 'string' },
|
quelle_url: { type: 'string' },
|
||||||
|
|||||||
@@ -640,6 +640,8 @@ function initializeDatabase() {
|
|||||||
firma TEXT NOT NULL,
|
firma TEXT NOT NULL,
|
||||||
stelle TEXT NOT NULL,
|
stelle TEXT NOT NULL,
|
||||||
ort TEXT,
|
ort TEXT,
|
||||||
|
adresse TEXT,
|
||||||
|
ansprechpartner TEXT,
|
||||||
gehalt TEXT,
|
gehalt TEXT,
|
||||||
beschreibung TEXT,
|
beschreibung TEXT,
|
||||||
quelle_url TEXT,
|
quelle_url TEXT,
|
||||||
@@ -657,6 +659,8 @@ function initializeDatabase() {
|
|||||||
// Migration: add columns to pre-existing jobangebote tables.
|
// Migration: add columns to pre-existing jobangebote tables.
|
||||||
db.run('ALTER TABLE jobangebote ADD COLUMN anzeige_datum DATE', () => {});
|
db.run('ALTER TABLE jobangebote ADD COLUMN anzeige_datum DATE', () => {});
|
||||||
db.run('ALTER TABLE jobangebote ADD COLUMN kontakt_email TEXT', () => {});
|
db.run('ALTER TABLE jobangebote ADD COLUMN kontakt_email TEXT', () => {});
|
||||||
|
db.run('ALTER TABLE jobangebote ADD COLUMN adresse TEXT', () => {});
|
||||||
|
db.run('ALTER TABLE jobangebote ADD COLUMN ansprechpartner TEXT', () => {});
|
||||||
// Normalized URL for URL-based de-duplication of offers (see lib/blacklist).
|
// Normalized URL for URL-based de-duplication of offers (see lib/blacklist).
|
||||||
db.run('ALTER TABLE jobangebote ADD COLUMN url_norm TEXT', () => {
|
db.run('ALTER TABLE jobangebote ADD COLUMN url_norm TEXT', () => {
|
||||||
// Backfill url_norm for rows ingested before this column existed.
|
// Backfill url_norm for rows ingested before this column existed.
|
||||||
@@ -1726,11 +1730,23 @@ initializeDatabase().then(() => {
|
|||||||
angebot.quelle ? `Importiert via: ${angebot.quelle}` : null,
|
angebot.quelle ? `Importiert via: ${angebot.quelle}` : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Employer address (street, house number, city) and contact person go into
|
||||||
|
// the AI notes so the LLM can use them for the letter's Anschriftfeld and
|
||||||
|
// salutation.
|
||||||
|
let anschrift = (angebot.adresse || '').trim();
|
||||||
|
if (angebot.ort && !anschrift.toLowerCase().includes(String(angebot.ort).toLowerCase())) {
|
||||||
|
anschrift = anschrift ? `${anschrift}, ${angebot.ort}` : String(angebot.ort);
|
||||||
|
}
|
||||||
|
const llmNotizen = [
|
||||||
|
anschrift ? `Anschrift des Arbeitgebers: ${anschrift}` : null,
|
||||||
|
angebot.ansprechpartner ? `Ansprechpartner: ${angebot.ansprechpartner}` : null,
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
const result = await dbRun(
|
const result = await dbRun(
|
||||||
`INSERT INTO bewerbungen
|
`INSERT INTO bewerbungen
|
||||||
(datum, firma, stelle, art, status, notizen, ort, stellenbeschreibung,
|
(datum, firma, stelle, art, status, notizen, ort, stellenbeschreibung,
|
||||||
quelle_url, email_empfaenger, generierung_status)
|
quelle_url, email_empfaenger, llm_notizen, generierung_status)
|
||||||
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||||
[
|
[
|
||||||
datum,
|
datum,
|
||||||
sanitizeInput(angebot.firma),
|
sanitizeInput(angebot.firma),
|
||||||
@@ -1741,6 +1757,7 @@ initializeDatabase().then(() => {
|
|||||||
angebot.beschreibung || '',
|
angebot.beschreibung || '',
|
||||||
angebot.quelle_url || '',
|
angebot.quelle_url || '',
|
||||||
angebot.kontakt_email || '',
|
angebot.kontakt_email || '',
|
||||||
|
llmNotizen,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1799,7 +1816,7 @@ initializeDatabase().then(() => {
|
|||||||
const b = req.body || {};
|
const b = req.body || {};
|
||||||
const quelleUrl = sanitizeInput(b.quelle_url || '');
|
const quelleUrl = sanitizeInput(b.quelle_url || '');
|
||||||
await dbRun(
|
await dbRun(
|
||||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, gehalt = ?,
|
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
||||||
beschreibung = ?, kontakt_email = ?, quelle_url = ?, anzeige_datum = ?,
|
beschreibung = ?, kontakt_email = ?, quelle_url = ?, anzeige_datum = ?,
|
||||||
url_norm = ?, updated_at = CURRENT_TIMESTAMP
|
url_norm = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
@@ -1807,6 +1824,8 @@ initializeDatabase().then(() => {
|
|||||||
sanitizeInput((b.firma || '').trim()) || angebot.firma,
|
sanitizeInput((b.firma || '').trim()) || angebot.firma,
|
||||||
sanitizeInput((b.stelle || '').trim()) || angebot.stelle,
|
sanitizeInput((b.stelle || '').trim()) || angebot.stelle,
|
||||||
sanitizeInput(b.ort || ''),
|
sanitizeInput(b.ort || ''),
|
||||||
|
sanitizeInput(b.adresse || ''),
|
||||||
|
sanitizeInput(b.ansprechpartner || ''),
|
||||||
sanitizeInput(b.gehalt || ''),
|
sanitizeInput(b.gehalt || ''),
|
||||||
sanitizeInput(b.beschreibung || ''),
|
sanitizeInput(b.beschreibung || ''),
|
||||||
sanitizeInput(b.kontakt_email || ''),
|
sanitizeInput(b.kontakt_email || ''),
|
||||||
|
|||||||
+2
-1
@@ -360,7 +360,8 @@
|
|||||||
Versand über <code><%= mailFrom %></code>. Empfängeradresse eingeben, Betreff und Text prüfen,
|
Versand über <code><%= mailFrom %></code>. Empfängeradresse eingeben, Betreff und Text prüfen,
|
||||||
Anhänge auswählen und senden.
|
Anhänge auswählen und senden.
|
||||||
</p>
|
</p>
|
||||||
<form action="/bewerbung/<%= application.id %>/email/send" method="POST">
|
<form action="/bewerbung/<%= application.id %>/email/send" method="POST"
|
||||||
|
onsubmit="return confirm('Bewerbung jetzt verbindlich an ' + (this.to.value.trim() || 'den Empfänger') + ' senden?');">
|
||||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Empfänger (E-Mail)</label>
|
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Empfänger (E-Mail)</label>
|
||||||
<input type="email" name="to" required value="<%= application.email_empfaenger || '' %>"
|
<input type="email" name="to" required value="<%= application.email_empfaenger || '' %>"
|
||||||
placeholder="personal@unternehmen.de"
|
placeholder="personal@unternehmen.de"
|
||||||
|
|||||||
@@ -75,6 +75,18 @@
|
|||||||
<% if (j.gehalt) { %><%= j.gehalt %><% } %>
|
<% if (j.gehalt) { %><%= j.gehalt %><% } %>
|
||||||
</p>
|
</p>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (j.adresse) { %>
|
||||||
|
<p class="text-sm mt-1 flex items-start gap-1.5 min-w-0">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 shrink-0">Anschrift:</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200 break-words whitespace-pre-line"><%= j.adresse %></span>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
<% if (j.ansprechpartner) { %>
|
||||||
|
<p class="text-sm mt-1 flex items-start gap-1.5 min-w-0">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 shrink-0">Ansprechpartner:</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200 break-words"><%= j.ansprechpartner %></span>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
<% if (j.anzeige_datum) { %>
|
<% if (j.anzeige_datum) { %>
|
||||||
<p class="text-sm mt-1 flex items-center gap-1.5">
|
<p class="text-sm mt-1 flex items-center gap-1.5">
|
||||||
<span class="text-gray-500 dark:text-gray-400 shrink-0">Anzeige:</span>
|
<span class="text-gray-500 dark:text-gray-400 shrink-0">Anzeige:</span>
|
||||||
@@ -179,6 +191,14 @@
|
|||||||
<span class="text-gray-500 dark:text-gray-400">Ort</span>
|
<span class="text-gray-500 dark:text-gray-400">Ort</span>
|
||||||
<input name="ort" value="<%= j.ort || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
<input name="ort" value="<%= j.ort || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="block text-xs">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Ansprechpartner</span>
|
||||||
|
<input name="ansprechpartner" value="<%= j.ansprechpartner || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
||||||
|
</label>
|
||||||
|
<label class="block text-xs sm:col-span-2">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Anschrift <span class="text-gray-400">(Straße, Hausnummer, PLZ, Ort — wird bei „Übernehmen“ in die KI-Notizen übernommen)</span></span>
|
||||||
|
<input name="adresse" value="<%= j.adresse || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
||||||
|
</label>
|
||||||
<label class="block text-xs">
|
<label class="block text-xs">
|
||||||
<span class="text-gray-500 dark:text-gray-400">Gehalt</span>
|
<span class="text-gray-500 dark:text-gray-400">Gehalt</span>
|
||||||
<input name="gehalt" value="<%= j.gehalt || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
<input name="gehalt" value="<%= j.gehalt || '' %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
|
||||||
|
|||||||
@@ -64,6 +64,18 @@
|
|||||||
<% if (j.gehalt) { %><%= j.gehalt %><% } %>
|
<% if (j.gehalt) { %><%= j.gehalt %><% } %>
|
||||||
</p>
|
</p>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (j.adresse) { %>
|
||||||
|
<p class="text-sm mt-1 flex items-start gap-1.5 min-w-0">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 shrink-0">Anschrift:</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200 break-words whitespace-pre-line"><%= j.adresse %></span>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
|
<% if (j.ansprechpartner) { %>
|
||||||
|
<p class="text-sm mt-1 flex items-start gap-1.5 min-w-0">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 shrink-0">Ansprechpartner:</span>
|
||||||
|
<span class="text-gray-700 dark:text-gray-200 break-words"><%= j.ansprechpartner %></span>
|
||||||
|
</p>
|
||||||
|
<% } %>
|
||||||
<% if (j.quelle_url) { %>
|
<% if (j.quelle_url) { %>
|
||||||
<a href="<%= j.quelle_url %>" target="_blank" rel="noopener noreferrer"
|
<a href="<%= j.quelle_url %>" target="_blank" rel="noopener noreferrer"
|
||||||
class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline mt-2 break-all">
|
class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline mt-2 break-all">
|
||||||
|
|||||||
Reference in New Issue
Block a user