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:
2026-07-05 15:23:44 +02:00
co-authored by Claude Opus 4.8
parent d1c6743d2f
commit 03760d293f
10 changed files with 178 additions and 28 deletions
+4
View File
@@ -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">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Labels</label>
<%- include('partials/labelPicker', { labelOptions: labelOptions, selected: application.labelsArr }) %>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notizen</label>
<textarea name="notizen" rows="6"
+16 -2
View File
@@ -276,7 +276,12 @@
</span>
<span class="whitespace-nowrap text-gray-900 dark:text-white"><%= new Date(app.datum).toLocaleDateString('de-DE') %></span>
<span class="min-w-0 truncate font-medium text-gray-900 dark:text-white" title="<%= app.firma %>"><%= app.firma %></span>
<span class="min-w-0 truncate text-gray-900 dark:text-white" title="<%= app.stelle %>"><%= app.stelle %></span>
<span class="min-w-0">
<span class="block truncate text-gray-900 dark:text-white" title="<%= app.stelle %>"><%= app.stelle %></span>
<% if (app.labelsArr && app.labelsArr.length) { %>
<span class="flex flex-wrap gap-1 mt-1"><%- include('partials/labelChips', { labels: app.labelsArr }) %></span>
<% } %>
</span>
<span class="min-w-0 truncate text-gray-500 dark:text-gray-400"><%= app.art || '-' %></span>
<span class="min-w-0">
<span class="inline-block max-w-full truncate align-middle px-2 py-1 rounded-full text-xs font-medium
@@ -600,7 +605,16 @@
<% }); %>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Labels
</label>
<div id="applicationLabels">
<%- include('partials/labelPicker', { labelOptions: labelOptions, selected: [] }) %>
</div>
</div>
<div>
<label for="applicationStatus" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Status (Anfangsstatus)
+7
View File
@@ -68,6 +68,9 @@
</div>
<p class="text-base font-semibold text-gray-800 dark:text-gray-100 break-words"><%= j.firma %></p>
<p class="text-sm text-gray-700 dark:text-gray-200 break-words"><%= j.stelle %></p>
<% if (j.labelsArr && j.labelsArr.length) { %>
<div class="flex flex-wrap gap-1 mt-1.5"><%- include('partials/labelChips', { labels: j.labelsArr }) %></div>
<% } %>
<% if (j.ort || j.gehalt) { %>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
<% if (j.ort) { %><%= j.ort %><% } %>
@@ -212,6 +215,10 @@
<span class="text-gray-500 dark:text-gray-400">Stellenbeschreibung <span class="text-gray-400">(beliebig lang — wird bei „Übernehmen“ als Stellenbeschreibung für die KI mitgenommen)</span></span>
<textarea name="beschreibung" rows="10" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm leading-relaxed" placeholder="Vollständige Stellenbeschreibung hier einfügen …"><%= j.beschreibung || '' %></textarea>
</label>
<div class="text-xs">
<span class="text-gray-500 dark:text-gray-400">Labels</span>
<div class="mt-1"><%- include('partials/labelPicker', { labelOptions: labelOptions, selected: j.labelsArr }) %></div>
</div>
<button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
Speichern
+12
View File
@@ -0,0 +1,12 @@
<%
// Display-only label chips. Expects local `labels` (array of label strings).
var _labelClasses = {
'Regional': 'bg-sky-100 text-sky-800 dark:bg-sky-900/50 dark:text-sky-200',
'Remote-Deutschlandweit': 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/50 dark:text-emerald-200',
'Homeoffice-Deutschlandweit': 'bg-violet-100 text-violet-800 dark:bg-violet-900/50 dark:text-violet-200'
};
var _labels = (typeof labels !== 'undefined' && labels) ? labels : [];
%>
<% _labels.forEach(function (lbl) { %>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium leading-tight <%= _labelClasses[lbl] || 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200' %>"><%= lbl %></span>
<% }); %>
+16
View File
@@ -0,0 +1,16 @@
<%
// Multi-select label picker (checkboxes named "labels"). Expects locals:
// labelOptions array of all selectable labels
// selected array of currently selected labels (optional)
var _opts = (typeof labelOptions !== 'undefined' && labelOptions) ? labelOptions : [];
var _sel = (typeof selected !== 'undefined' && selected) ? selected : [];
%>
<div class="flex flex-wrap gap-2">
<% _opts.forEach(function (opt) { %>
<label class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border cursor-pointer text-sm transition-colors border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700">
<input type="checkbox" name="labels" value="<%= opt %>" <%= _sel.indexOf(opt) !== -1 ? 'checked' : '' %>
class="rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500">
<span class="text-gray-700 dark:text-gray-200"><%= opt %></span>
</label>
<% }); %>
</div>