Show job offer posting date (anzeige_datum) alongside ingestion date
- Add anzeige_datum DATE column to jobangebote (with ALTER TABLE migration for pre-existing tables) — the original posting date supplied by the third party - Accept anzeige_datum in POST /api/v1/joboffers (create + upsert) and document it in the OpenAPI spec - Render both on /jobangebote: "Anzeige: <anzeige_datum>" and "Einspielung: <created_at>" Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -536,6 +536,7 @@ function createExternalApi(deps) {
|
|||||||
}
|
}
|
||||||
const quelle = sanitizeInput(b.quelle || 'drittanbieter');
|
const quelle = sanitizeInput(b.quelle || 'drittanbieter');
|
||||||
const externalId = b.external_id != null ? sanitizeInput(String(b.external_id)) : null;
|
const externalId = b.external_id != null ? sanitizeInput(String(b.external_id)) : null;
|
||||||
|
const anzeigeDatum = sanitizeInput(b.anzeige_datum || '');
|
||||||
const fields = [
|
const fields = [
|
||||||
sanitizeInput(b.firma),
|
sanitizeInput(b.firma),
|
||||||
sanitizeInput(b.stelle),
|
sanitizeInput(b.stelle),
|
||||||
@@ -544,6 +545,7 @@ function createExternalApi(deps) {
|
|||||||
sanitizeInput(b.beschreibung || ''),
|
sanitizeInput(b.beschreibung || ''),
|
||||||
sanitizeInput(b.quelle_url || ''),
|
sanitizeInput(b.quelle_url || ''),
|
||||||
sanitizeInput(b.art || ''),
|
sanitizeInput(b.art || ''),
|
||||||
|
anzeigeDatum,
|
||||||
sanitizeInput(b.status || 'offen'),
|
sanitizeInput(b.status || 'offen'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -556,7 +558,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 = ?, gehalt = ?,
|
||||||
beschreibung = ?, quelle_url = ?, art = ?, status = ?,
|
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, status = ?,
|
||||||
updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||||
[...fields, existing.id]
|
[...fields, existing.id]
|
||||||
);
|
);
|
||||||
@@ -567,8 +569,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, status)
|
(external_id, quelle, firma, stelle, ort, gehalt, beschreibung, quelle_url, art, anzeige_datum, status)
|
||||||
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]);
|
||||||
|
|||||||
@@ -784,6 +784,7 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
beschreibung: { type: 'string', nullable: true },
|
beschreibung: { type: 'string', nullable: true },
|
||||||
quelle_url: { type: 'string', nullable: true },
|
quelle_url: { type: 'string', nullable: true },
|
||||||
art: { type: 'string', nullable: true },
|
art: { type: 'string', nullable: true },
|
||||||
|
anzeige_datum: { type: 'string', format: 'date', nullable: true, description: 'Datum der eigentlichen Stellenanzeige (vom Drittanbieter übergeben)' },
|
||||||
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'] },
|
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'] },
|
||||||
verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
|
verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
|
||||||
created_at: { type: 'string', format: 'date-time' },
|
created_at: { type: 'string', format: 'date-time' },
|
||||||
@@ -803,6 +804,7 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
||||||
quelle_url: { type: 'string' },
|
quelle_url: { type: 'string' },
|
||||||
art: { type: 'string', enum: ART_OPTIONS },
|
art: { type: 'string', enum: ART_OPTIONS },
|
||||||
|
anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' },
|
||||||
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'], default: 'offen' },
|
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'], default: 'offen' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -619,6 +619,7 @@ function initializeDatabase() {
|
|||||||
beschreibung TEXT,
|
beschreibung TEXT,
|
||||||
quelle_url TEXT,
|
quelle_url TEXT,
|
||||||
art TEXT,
|
art TEXT,
|
||||||
|
anzeige_datum DATE,
|
||||||
status TEXT NOT NULL DEFAULT 'offen',
|
status TEXT NOT NULL DEFAULT 'offen',
|
||||||
verknuepfte_bewerbung_id INTEGER,
|
verknuepfte_bewerbung_id INTEGER,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -627,6 +628,8 @@ function initializeDatabase() {
|
|||||||
FOREIGN KEY (verknuepfte_bewerbung_id) REFERENCES bewerbungen(id) ON DELETE SET NULL
|
FOREIGN KEY (verknuepfte_bewerbung_id) REFERENCES bewerbungen(id) ON DELETE SET NULL
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
// Migration: add anzeige_datum to pre-existing jobangebote tables.
|
||||||
|
db.run('ALTER TABLE jobangebote ADD COLUMN anzeige_datum DATE', () => {});
|
||||||
// Remember the last recipient address per application (prefill).
|
// Remember the last recipient address per application (prefill).
|
||||||
db.run('ALTER TABLE bewerbungen ADD COLUMN email_empfaenger TEXT', () => {});
|
db.run('ALTER TABLE bewerbungen ADD COLUMN email_empfaenger TEXT', () => {});
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -71,10 +71,17 @@
|
|||||||
<span class="truncate"><%= j.quelle_url %></span>
|
<span class="truncate"><%= j.quelle_url %></span>
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2">
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-2 flex flex-wrap gap-x-3 gap-y-1">
|
||||||
Eingegangen: <%= j.created_at ? new Date(j.created_at + 'Z').toLocaleString('de-DE') : '' %>
|
<% if (j.anzeige_datum) { %>
|
||||||
|
<span title="Datum der eigentlichen Stellenanzeige">
|
||||||
|
Anzeige: <span class="text-gray-500 dark:text-gray-400"><%= new Date(j.anzeige_datum).toLocaleDateString('de-DE') %></span>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
<span title="Wann das Angebot über die REST-API eingespielt wurde">
|
||||||
|
Einspielung: <span class="text-gray-500 dark:text-gray-400"><%= j.created_at ? new Date(j.created_at + 'Z').toLocaleString('de-DE') : '' %></span>
|
||||||
|
</span>
|
||||||
<% if (j.verknuepfte_bewerbung_id) { %>
|
<% if (j.verknuepfte_bewerbung_id) { %>
|
||||||
· <a href="/bewerbung/<%= j.verknuepfte_bewerbung_id %>" class="text-blue-600 dark:text-blue-400 hover:underline">Bewerbung #<%= j.verknuepfte_bewerbung_id %></a>
|
<span>· <a href="/bewerbung/<%= j.verknuepfte_bewerbung_id %>" class="text-blue-600 dark:text-blue-400 hover:underline">Bewerbung #<%= j.verknuepfte_bewerbung_id %></a></span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +150,8 @@
|
|||||||
"gehalt": "55.000 €",
|
"gehalt": "55.000 €",
|
||||||
"beschreibung": "…",
|
"beschreibung": "…",
|
||||||
"quelle_url": "https://acme.example/job/12345",
|
"quelle_url": "https://acme.example/job/12345",
|
||||||
"art": "Online-Portal"
|
"art": "Online-Portal",
|
||||||
|
"anzeige_datum": "2026-07-01"
|
||||||
}'</code></pre>
|
}'</code></pre>
|
||||||
<p class="text-xs">Wiederholtes Senden mit derselben <code>quelle</code> + <code>external_id</code> aktualisiert das Angebot (Upsert). Die Doku/Swagger-UI liegt unter <a href="/swagger" class="text-blue-600 dark:text-blue-400 hover:underline">/swagger</a>.</p>
|
<p class="text-xs">Wiederholtes Senden mit derselben <code>quelle</code> + <code>external_id</code> aktualisiert das Angebot (Upsert). Die Doku/Swagger-UI liegt unter <a href="/swagger" class="text-blue-600 dark:text-blue-400 hover:underline">/swagger</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user