Labels für Stellen (Bewerbungen + Jobangebote), inkl. API & Swagger
Mehrere Labels pro Stelle (Regional, Remote-Deutschlandweit, Homeoffice-Deutschlandweit), gespeichert als JSON-Array in einer neuen labels-Spalte beider Tabellen (Migration). Geteiltes lib/labels.js mit parse/serialize; wiederverwendbare Partials fuer Chips + Mehrfachauswahl. - Web: setzen im Hinzufuegen-Modal, auf der Bearbeiten-Seite und im Jobangebot-Bearbeiten-Formular; Anzeige als Chips in den Listen. - Uebernahme eines Angebots traegt dessen Labels in die neue Bewerbung. - REST-API: labels[] in /applications und /joboffers (GET/POST/PUT), Filter ?label=…; OpenAPI/Swagger-Schemas + Enums erweitert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-13
@@ -9,6 +9,13 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const blacklist = require('./blacklist');
|
||||
const { LABEL_OPTIONS, parseLabels, serializeLabels } = require('./labels');
|
||||
|
||||
// Replace the stored labels JSON string with a real array on outgoing rows.
|
||||
function withLabels(row) {
|
||||
if (row) row.labels = parseLabels(row.labels);
|
||||
return row;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
|
||||
@@ -60,12 +67,17 @@ function createExternalApi(deps) {
|
||||
// --- Applications --------------------------------------------------
|
||||
router.get('/applications', async (req, res) => {
|
||||
try {
|
||||
const { month, year, status, art, search } = req.query;
|
||||
const { month, year, status, art, search, label } = req.query;
|
||||
const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 100, 1), 500);
|
||||
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
||||
|
||||
const where = [];
|
||||
const params = [];
|
||||
if (label && LABEL_OPTIONS.includes(label)) {
|
||||
// labels is a JSON array string; match the quoted label token.
|
||||
where.push('labels LIKE ?');
|
||||
params.push(`%"${label}"%`);
|
||||
}
|
||||
if (month) {
|
||||
where.push('strftime("%m", datum) = ?');
|
||||
params.push(String(month).padStart(2, '0'));
|
||||
@@ -94,6 +106,7 @@ function createExternalApi(deps) {
|
||||
[...params, limit, offset]
|
||||
);
|
||||
await attachVerlauf(applications);
|
||||
applications.forEach(withLabels);
|
||||
res.json(applications);
|
||||
} catch (error) {
|
||||
console.error('API list applications error:', error);
|
||||
@@ -125,8 +138,8 @@ function createExternalApi(deps) {
|
||||
const result = await dbRun(
|
||||
`INSERT INTO bewerbungen
|
||||
(datum, firma, stelle, art, status, notizen, interne_notizen, ort,
|
||||
stellenbeschreibung, quelle_url, llm_notizen, generierung_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
stellenbeschreibung, quelle_url, llm_notizen, labels, generierung_status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
[
|
||||
datum,
|
||||
sanitizeInput(firma),
|
||||
@@ -139,6 +152,7 @@ function createExternalApi(deps) {
|
||||
sanitizeInput(b.stellenbeschreibung || ''),
|
||||
sanitizeInput(b.quelle_url || ''),
|
||||
sanitizeInput(b.llm_notizen || ''),
|
||||
serializeLabels(b.labels),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -150,7 +164,7 @@ function createExternalApi(deps) {
|
||||
}
|
||||
|
||||
const application = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [result.lastID]);
|
||||
res.json({ success: true, application });
|
||||
res.json({ success: true, application: withLabels(application) });
|
||||
} catch (error) {
|
||||
console.error('API create application error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
@@ -162,7 +176,7 @@ function createExternalApi(deps) {
|
||||
const application = await getApplication(req.params.id);
|
||||
if (!application) return res.status(404).json({ error: 'Bewerbung nicht gefunden' });
|
||||
await attachVerlauf([application]);
|
||||
res.json(application);
|
||||
res.json(withLabels(application));
|
||||
} catch (error) {
|
||||
console.error('API get application error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
@@ -184,7 +198,7 @@ function createExternalApi(deps) {
|
||||
`UPDATE bewerbungen SET
|
||||
datum = ?, firma = ?, stelle = ?, art = ?, status = ?, notizen = ?,
|
||||
interne_notizen = ?, ort = ?, stellenbeschreibung = ?, quelle_url = ?,
|
||||
llm_notizen = ?, updated_at = CURRENT_TIMESTAMP
|
||||
llm_notizen = ?, labels = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
b.datum,
|
||||
@@ -198,12 +212,15 @@ function createExternalApi(deps) {
|
||||
sanitizeInput(b.stellenbeschreibung ?? existing.stellenbeschreibung),
|
||||
sanitizeInput(b.quelle_url ?? existing.quelle_url),
|
||||
sanitizeInput(b.llm_notizen ?? existing.llm_notizen),
|
||||
Object.prototype.hasOwnProperty.call(b, 'labels')
|
||||
? serializeLabels(b.labels)
|
||||
: (existing.labels || '[]'),
|
||||
id,
|
||||
]
|
||||
);
|
||||
|
||||
const application = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [id]);
|
||||
res.json({ success: true, application });
|
||||
res.json({ success: true, application: withLabels(application) });
|
||||
} catch (error) {
|
||||
console.error('API update application error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
@@ -516,6 +533,7 @@ function createExternalApi(deps) {
|
||||
const rows = await dbAll(
|
||||
`SELECT * FROM jobangebote ORDER BY created_at DESC, id DESC`
|
||||
);
|
||||
rows.forEach(withLabels);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
console.error('API list job offers error:', error);
|
||||
@@ -583,7 +601,7 @@ function createExternalApi(deps) {
|
||||
try {
|
||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [req.params.id]);
|
||||
if (!row) return res.status(404).json({ error: 'Jobangebot nicht gefunden' });
|
||||
res.json(row);
|
||||
res.json(withLabels(row));
|
||||
} catch (error) {
|
||||
console.error('API get job offer error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
@@ -628,6 +646,7 @@ function createExternalApi(deps) {
|
||||
anzeigeDatum,
|
||||
kontaktEmail,
|
||||
sanitizeInput(b.status || 'offen'),
|
||||
serializeLabels(b.labels),
|
||||
urlNorm,
|
||||
];
|
||||
|
||||
@@ -651,21 +670,21 @@ function createExternalApi(deps) {
|
||||
await dbRun(
|
||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
||||
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?,
|
||||
url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||
labels = ?, url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||
[...fields, existing.id]
|
||||
);
|
||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [existing.id]);
|
||||
return res.json({ success: true, action: 'updated', joboffer: row });
|
||||
return res.json({ success: true, action: 'updated', joboffer: withLabels(row) });
|
||||
}
|
||||
|
||||
const result = await dbRun(
|
||||
`INSERT INTO jobangebote
|
||||
(external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, url_norm)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, labels, url_norm)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[externalId, quelle, ...fields]
|
||||
);
|
||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]);
|
||||
res.status(201).json({ success: true, action: 'created', joboffer: row });
|
||||
res.status(201).json({ success: true, action: 'created', joboffer: withLabels(row) });
|
||||
} catch (error) {
|
||||
console.error('API create job offer error:', error);
|
||||
res.status(500).json({ error: 'Serverfehler' });
|
||||
|
||||
Reference in New Issue
Block a user