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:
@@ -33,6 +33,7 @@ const { createExternalApi } = require('./lib/api');
|
||||
const { buildOpenApiSpec } = require('./lib/openapi');
|
||||
const blacklist = require('./lib/blacklist');
|
||||
const caldav = require('./lib/caldav');
|
||||
const { LABEL_OPTIONS, parseLabels, serializeLabels } = require('./lib/labels');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -766,6 +767,9 @@ function initializeDatabase() {
|
||||
FOREIGN KEY (verknuepfte_bewerbung_id) REFERENCES bewerbungen(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
// Migration: labels (JSON array of work-location labels) on both Stellen tables.
|
||||
db.run('ALTER TABLE bewerbungen ADD COLUMN labels TEXT', () => {});
|
||||
db.run('ALTER TABLE jobangebote ADD COLUMN labels TEXT', () => {});
|
||||
// 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 kontakt_email TEXT', () => {});
|
||||
@@ -898,6 +902,7 @@ initializeDatabase().then(() => {
|
||||
|
||||
const applications = await dbAll(query, params);
|
||||
await attachVerlauf(applications);
|
||||
applications.forEach((a) => { a.labelsArr = parseLabels(a.labels); });
|
||||
const settings = await dbGet('SELECT * FROM settings WHERE id = 1');
|
||||
// Upcoming calendar appointments for the dashboard widget.
|
||||
const kommendeTermine = await upcomingTermine(6);
|
||||
@@ -954,7 +959,8 @@ initializeDatabase().then(() => {
|
||||
kommendeTermine,
|
||||
caldavTz: caldav.TZ,
|
||||
artOptions: ART_OPTIONS,
|
||||
statusOptions: STATUS_OPTIONS
|
||||
statusOptions: STATUS_OPTIONS,
|
||||
labelOptions: LABEL_OPTIONS
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@@ -1108,6 +1114,7 @@ initializeDatabase().then(() => {
|
||||
app.post('/api/bewerbungen', async (req, res) => {
|
||||
try {
|
||||
const { datum, firma, stelle, art, status, notizen, interne_notizen, kommentar } = req.body;
|
||||
const labels = serializeLabels(req.body.labels);
|
||||
|
||||
// Duplicate guard: warn before creating a second application for the same
|
||||
// company + role (the client re-submits with force=true to confirm).
|
||||
@@ -1124,9 +1131,9 @@ initializeDatabase().then(() => {
|
||||
}
|
||||
|
||||
const result = await dbRun(
|
||||
'INSERT INTO bewerbungen (datum, firma, stelle, art, status, notizen, interne_notizen) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
'INSERT INTO bewerbungen (datum, firma, stelle, art, status, notizen, interne_notizen, labels) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[datum, sanitizeInput(firma), sanitizeInput(stelle),
|
||||
sanitizeInput(art), sanitizeInput(status), sanitizeInput(notizen), sanitizeInput(interne_notizen)]
|
||||
sanitizeInput(art), sanitizeInput(status), sanitizeInput(notizen), sanitizeInput(interne_notizen), labels]
|
||||
);
|
||||
|
||||
// Record the initial status as the first timeline entry
|
||||
@@ -1151,11 +1158,15 @@ initializeDatabase().then(() => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { datum, firma, stelle, art, status, notizen } = req.body;
|
||||
|
||||
const existing = await dbGet('SELECT labels FROM bewerbungen WHERE id = ?', [id]);
|
||||
const labels = Object.prototype.hasOwnProperty.call(req.body, 'labels')
|
||||
? serializeLabels(req.body.labels)
|
||||
: (existing ? existing.labels : '[]');
|
||||
|
||||
await dbRun(
|
||||
'UPDATE bewerbungen SET datum = ?, firma = ?, stelle = ?, art = ?, status = ?, notizen = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[datum, sanitizeInput(firma), sanitizeInput(stelle),
|
||||
sanitizeInput(art), sanitizeInput(status), sanitizeInput(notizen), id]
|
||||
'UPDATE bewerbungen SET datum = ?, firma = ?, stelle = ?, art = ?, status = ?, notizen = ?, labels = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[datum, sanitizeInput(firma), sanitizeInput(stelle),
|
||||
sanitizeInput(art), sanitizeInput(status), sanitizeInput(notizen), labels, id]
|
||||
);
|
||||
|
||||
const updatedApplication = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [id]);
|
||||
@@ -1234,6 +1245,7 @@ initializeDatabase().then(() => {
|
||||
const { id } = req.params;
|
||||
const application = await dbGet('SELECT * FROM bewerbungen WHERE id = ?', [id]);
|
||||
if (!application) return res.status(404).send('Bewerbung nicht gefunden');
|
||||
application.labelsArr = parseLabels(application.labels);
|
||||
|
||||
const verlauf = await dbAll(
|
||||
'SELECT * FROM status_verlauf WHERE bewerbung_id = ? ORDER BY date(datum) ASC, id ASC',
|
||||
@@ -1294,6 +1306,7 @@ initializeDatabase().then(() => {
|
||||
terminError: req.query.terminerror ? String(req.query.terminerror) : '',
|
||||
artOptions: ART_OPTIONS,
|
||||
statusOptions: STATUS_OPTIONS,
|
||||
labelOptions: LABEL_OPTIONS,
|
||||
hideSettings: true
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1307,10 +1320,11 @@ initializeDatabase().then(() => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { datum, firma, stelle, art, notizen, interne_notizen } = req.body;
|
||||
const labels = serializeLabels(req.body.labels);
|
||||
|
||||
await dbRun(
|
||||
'UPDATE bewerbungen SET datum = ?, firma = ?, stelle = ?, art = ?, notizen = ?, interne_notizen = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[datum, sanitizeInput(firma), sanitizeInput(stelle), sanitizeInput(art), sanitizeInput(notizen), sanitizeInput(interne_notizen), id]
|
||||
'UPDATE bewerbungen SET datum = ?, firma = ?, stelle = ?, art = ?, notizen = ?, interne_notizen = ?, labels = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[datum, sanitizeInput(firma), sanitizeInput(stelle), sanitizeInput(art), sanitizeInput(notizen), sanitizeInput(interne_notizen), labels, id]
|
||||
);
|
||||
|
||||
res.redirect('/bewerbung/' + id);
|
||||
@@ -1995,12 +2009,14 @@ initializeDatabase().then(() => {
|
||||
WHERE j.status = 'offen'
|
||||
ORDER BY j.created_at DESC, j.id DESC`
|
||||
);
|
||||
jobangebote.forEach((j) => { j.labelsArr = parseLabels(j.labels); });
|
||||
const uebernommenRow = await dbGet("SELECT COUNT(*) AS c FROM jobangebote WHERE status = 'uebernommen'");
|
||||
res.render('jobangebote', {
|
||||
jobangebote,
|
||||
uebernommenCount: uebernommenRow ? uebernommenRow.c : 0,
|
||||
artOptions: ART_OPTIONS,
|
||||
statusOptions: STATUS_OPTIONS,
|
||||
labelOptions: LABEL_OPTIONS,
|
||||
hideSettings: false,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -2019,6 +2035,7 @@ initializeDatabase().then(() => {
|
||||
WHERE j.status = 'uebernommen'
|
||||
ORDER BY j.updated_at DESC, j.id DESC`
|
||||
);
|
||||
uebernommen.forEach((j) => { j.labelsArr = parseLabels(j.labels); });
|
||||
const offenRow = await dbGet("SELECT COUNT(*) AS c FROM jobangebote WHERE status = 'offen'");
|
||||
res.render('jobangebote_uebernommen', {
|
||||
uebernommen,
|
||||
@@ -2064,8 +2081,8 @@ initializeDatabase().then(() => {
|
||||
const result = await dbRun(
|
||||
`INSERT INTO bewerbungen
|
||||
(datum, firma, stelle, art, status, notizen, ort, stellenbeschreibung,
|
||||
quelle_url, email_empfaenger, llm_notizen, generierung_status)
|
||||
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
quelle_url, email_empfaenger, llm_notizen, labels, generierung_status)
|
||||
VALUES (?, ?, ?, ?, 'Entwurf', ?, ?, ?, ?, ?, ?, ?, 'nicht_gestartet')`,
|
||||
[
|
||||
datum,
|
||||
sanitizeInput(angebot.firma),
|
||||
@@ -2077,6 +2094,7 @@ initializeDatabase().then(() => {
|
||||
angebot.quelle_url || '',
|
||||
angebot.kontakt_email || '',
|
||||
llmNotizen,
|
||||
serializeLabels(angebot.labels),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2115,7 +2133,7 @@ initializeDatabase().then(() => {
|
||||
await dbRun(
|
||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
||||
beschreibung = ?, kontakt_email = ?, quelle_url = ?, anzeige_datum = ?,
|
||||
url_norm = ?, updated_at = CURRENT_TIMESTAMP
|
||||
labels = ?, url_norm = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
sanitizeInput((b.firma || '').trim()) || angebot.firma,
|
||||
@@ -2128,6 +2146,7 @@ initializeDatabase().then(() => {
|
||||
sanitizeInput(b.kontakt_email || ''),
|
||||
quelleUrl,
|
||||
sanitizeInput(b.anzeige_datum || ''),
|
||||
serializeLabels(b.labels),
|
||||
blacklist.normalizeUrl(b.quelle_url || '') || null,
|
||||
req.params.id,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user