From 03760d293fa044c7f5867830c55b82436d95b2ca Mon Sep 17 00:00:00 2001 From: Thomas Hackner Date: Sun, 5 Jul 2026 15:23:44 +0200 Subject: [PATCH] =?UTF-8?q?Labels=20f=C3=BCr=20Stellen=20(Bewerbungen=20+?= =?UTF-8?q?=20Jobangebote),=20inkl.=20API=20&=20Swagger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/api.js | 45 ++++++++++++++++++++++++---------- lib/labels.js | 44 +++++++++++++++++++++++++++++++++ lib/openapi.js | 14 +++++++++++ public/js/main.js | 3 ++- server.js | 43 +++++++++++++++++++++++--------- views/bewerbung.ejs | 4 +++ views/index.ejs | 18 ++++++++++++-- views/jobangebote.ejs | 7 ++++++ views/partials/labelChips.ejs | 12 +++++++++ views/partials/labelPicker.ejs | 16 ++++++++++++ 10 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 lib/labels.js create mode 100644 views/partials/labelChips.ejs create mode 100644 views/partials/labelPicker.ejs diff --git a/lib/api.js b/lib/api.js index 6d65ed7..91c1106 100644 --- a/lib/api.js +++ b/lib/api.js @@ -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' }); diff --git a/lib/labels.js b/lib/labels.js new file mode 100644 index 0000000..d4467ab --- /dev/null +++ b/lib/labels.js @@ -0,0 +1,44 @@ +// Shared label vocabulary for Stellen (Bewerbungen + Jobangebote). +// +// Labels describe the work-location character of a position. Several labels may +// be set per Stelle. They are stored as a JSON array string in the `labels` +// column and exposed as a string[] through the web UI and the REST API. + +const LABEL_OPTIONS = [ + 'Regional', + 'Remote-Deutschlandweit', + 'Homeoffice-Deutschlandweit', +]; + +// Normalize any incoming labels value (array, JSON string, comma-separated +// string, or a single label) into a clean, de-duplicated array limited to +// LABEL_OPTIONS and returned in canonical order for stable output. +function parseLabels(raw) { + if (raw == null || raw === '') return []; + let arr; + if (Array.isArray(raw)) { + arr = raw; + } else if (typeof raw === 'string') { + const s = raw.trim(); + if (!s) return []; + if (s[0] === '[') { + try { arr = JSON.parse(s); } catch (e) { arr = []; } + if (!Array.isArray(arr)) arr = []; + } else { + arr = s.split(','); + } + } else { + return []; + } + const chosen = new Set( + arr.map((v) => String(v).trim()).filter((v) => LABEL_OPTIONS.includes(v)) + ); + return LABEL_OPTIONS.filter((l) => chosen.has(l)); +} + +// Canonical JSON string for storage in the DB (always a valid array). +function serializeLabels(raw) { + return JSON.stringify(parseLabels(raw)); +} + +module.exports = { LABEL_OPTIONS, parseLabels, serializeLabels }; diff --git a/lib/openapi.js b/lib/openapi.js index 67300f3..16af0ab 100644 --- a/lib/openapi.js +++ b/lib/openapi.js @@ -11,6 +11,14 @@ const STATUS_OPTIONS = [ 'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch', 'Absage', 'Einstellung', 'Keine Rückmeldung', ]; +const { LABEL_OPTIONS } = require('./labels'); + +// Reusable schema for the labels array (work-location labels of a Stelle). +const labelsSchema = { + type: 'array', + items: { type: 'string', enum: LABEL_OPTIONS }, + description: 'Labels der Stelle (Arbeitsort). Mehrere möglich.', +}; // Shared reusable schemas ----------------------------------------------- @@ -79,6 +87,7 @@ function buildOpenApiSpec(baseUrl = '') { { name: 'year', in: 'query', schema: { type: 'string', pattern: '^[0-9]{4}$' }, description: 'Jahr (z. B. 2026)' }, { name: 'status', in: 'query', schema: { type: 'string', enum: STATUS_OPTIONS }, description: 'Filter nach Status' }, { name: 'art', in: 'query', schema: { type: 'string', enum: ART_OPTIONS }, description: 'Filter nach Bewerbungsart' }, + { name: 'label', in: 'query', schema: { type: 'string', enum: LABEL_OPTIONS }, description: 'Filter nach Label (Stellen mit diesem Label)' }, { name: 'search', in: 'query', schema: { type: 'string' }, description: 'Freitextsuche in Firma und Stelle' }, { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 500, default: 100 }, description: 'Max. Anzahl Ergebnisse' }, { name: 'offset', in: 'query', schema: { type: 'integer', minimum: 0, default: 0 }, description: 'Ergebnis-Offset (Paging)' }, @@ -663,6 +672,7 @@ function buildOpenApiSpec(baseUrl = '') { stelle: { type: 'string' }, art: { type: 'string', enum: ART_OPTIONS, nullable: true }, status: { type: 'string', enum: STATUS_OPTIONS, nullable: true }, + labels: { ...labelsSchema, nullable: true }, notizen: { type: 'string', nullable: true }, interne_notizen: { type: 'string', nullable: true }, ort: { type: 'string', nullable: true }, @@ -692,6 +702,7 @@ function buildOpenApiSpec(baseUrl = '') { stelle: { type: 'string' }, art: { type: 'string', enum: ART_OPTIONS }, status: { type: 'string', enum: STATUS_OPTIONS }, + labels: labelsSchema, notizen: { type: 'string' }, interne_notizen: { type: 'string' }, ort: { type: 'string' }, @@ -711,6 +722,7 @@ function buildOpenApiSpec(baseUrl = '') { stelle: { type: 'string' }, art: { type: 'string', enum: ART_OPTIONS }, status: { type: 'string', enum: STATUS_OPTIONS }, + labels: labelsSchema, notizen: { type: 'string' }, interne_notizen: { type: 'string' }, ort: { type: 'string' }, @@ -871,6 +883,7 @@ function buildOpenApiSpec(baseUrl = '') { anzeige_datum: { type: 'string', format: 'date', nullable: true, description: 'Datum der eigentlichen Stellenanzeige (vom Drittanbieter übergeben)' }, kontakt_email: { type: 'string', format: 'email', nullable: true, description: 'Kontakt-E-Mail-Adresse der Stelle' }, status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'] }, + labels: { ...labelsSchema, nullable: true }, verknuepfte_bewerbung_id: { type: 'integer', nullable: true }, url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL für die De-Duplizierung (serverseitig gesetzt)' }, created_at: { type: 'string', format: 'date-time' }, @@ -895,6 +908,7 @@ function buildOpenApiSpec(baseUrl = '') { anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' }, kontakt_email: { type: 'string', format: 'email', description: 'Kontakt-E-Mail-Adresse der Stelle' }, status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'], default: 'offen' }, + labels: labelsSchema, }, }, JobOfferResult: { diff --git a/public/js/main.js b/public/js/main.js index 97bf372..19901f1 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -184,7 +184,8 @@ function saveApplication(event) { status: formData.get('status'), notizen: formData.get('notizen'), interne_notizen: formData.get('interne_notizen'), - kommentar: formData.get('kommentar') + kommentar: formData.get('kommentar'), + labels: formData.getAll('labels') }; let url = '/api/bewerbungen'; diff --git a/server.js b/server.js index 73824b4..c5e7ab8 100644 --- a/server.js +++ b/server.js @@ -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, ] diff --git a/views/bewerbung.ejs b/views/bewerbung.ejs index bd6ea26..91da8ea 100644 --- a/views/bewerbung.ejs +++ b/views/bewerbung.ejs @@ -69,6 +69,10 @@ class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white"> +
+ + <%- include('partials/labelPicker', { labelOptions: labelOptions, selected: application.labelsArr }) %> +
+
+ Labels +
<%- include('partials/labelPicker', { labelOptions: labelOptions, selected: j.labelsArr }) %>
+