Bewerbungsstatus "Vertragsverhandlung" ergaenzt

Neue Station zwischen Vorstellungsgespraech und Einstellung - in der
Statusliste, der Gruppierung auf der Startseite, den Farbskalen (Lime,
zwischen dem Gelb des Gespraechs und dem Gruen der Einstellung), im
PDF-Export, in der Statistik und in der REST-API/OpenAPI.

Der Statistik-Funnel zaehlte bisher ever('Vorstellungsgespraech') +
ever('Einstellung') und damit eine Bewerbung, die beide Stationen
durchlaufen hat, doppelt. Mit einer weiteren Stufe waere das schlimmer
geworden, deshalb zaehlt der Funnel jetzt pro Bewerbung statt pro
Statuszeile: jede Stufe zaehlt eine Bewerbung genau einmal, ein spaeterer
Status impliziert die frueheren.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:10:02 +02:00
co-authored by Claude Opus 4.8
parent 2b90e4bbad
commit 5ce835ff3a
8 changed files with 49 additions and 11 deletions
+7
View File
@@ -162,10 +162,17 @@ bewerbungs-tracker/
- Sonstiges - Sonstiges
### Status ### Status
- Entwurf
- Gesendet - Gesendet
- Eingangsbestätigung - Eingangsbestätigung
- In Bearbeitung
- Interessiert
- Warten auf Rückmeldung
- Warten auf meine Antwort
- Vorstellungsgespräch - Vorstellungsgespräch
- Vertragsverhandlung
- Absage - Absage
- Absage von meiner Seite
- Einstellung - Einstellung
- Keine Rückmeldung - Keine Rückmeldung
+2 -1
View File
@@ -27,7 +27,8 @@ function withLabels(row) {
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung', 'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
]; ];
function createExternalApi(deps) { function createExternalApi(deps) {
+2 -1
View File
@@ -10,7 +10,8 @@ const ART_OPTIONS = [
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung', 'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
]; ];
const { LABEL_OPTIONS } = require('./labels'); const { LABEL_OPTIONS } = require('./labels');
+2 -1
View File
@@ -356,7 +356,7 @@ function generatePdfDocument(settings, applications, month, year) {
// ---- Group entries by status (order kept in sync with views/index.ejs) ---- // ---- Group entries by status (order kept in sync with views/index.ejs) ----
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung', const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung']; 'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
// Accent colour per status (RGB) reused by the chart and the list headings // Accent colour per status (RGB) reused by the chart and the list headings
const statusColors = { const statusColors = {
'Entwurf': [168, 85, 247], 'Entwurf': [168, 85, 247],
@@ -367,6 +367,7 @@ function generatePdfDocument(settings, applications, month, year) {
'Warten auf Rückmeldung': [249, 115, 22], 'Warten auf Rückmeldung': [249, 115, 22],
'Warten auf meine Antwort': [217, 70, 239], 'Warten auf meine Antwort': [217, 70, 239],
'Vorstellungsgespräch': [245, 158, 11], 'Vorstellungsgespräch': [245, 158, 11],
'Vertragsverhandlung': [132, 204, 22],
'Einstellung': [34, 197, 94], 'Einstellung': [34, 197, 94],
'Absage': [239, 68, 68], 'Absage': [239, 68, 68],
'Absage von meiner Seite': [225, 29, 72], 'Absage von meiner Seite': [225, 29, 72],
+30 -5
View File
@@ -62,7 +62,8 @@ const ART_OPTIONS = [
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung' 'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung'
]; ];
// Base document types the user can provide as a foundation for AI tailoring // Base document types the user can provide as a foundation for AI tailoring
const BASIS_TYP_OPTIONS = ['Anschreiben', 'Lebenslauf', 'Profil/Kurzprofil', 'Sonstiges']; const BASIS_TYP_OPTIONS = ['Anschreiben', 'Lebenslauf', 'Profil/Kurzprofil', 'Sonstiges'];
@@ -1745,6 +1746,27 @@ initializeDatabase().then(async () => {
const ever = (s) => verlaufMap[s] || 0; const ever = (s) => verlaufMap[s] || 0;
const cur = (s) => statusMap[s] || 0; const cur = (s) => statusMap[s] || 0;
// Für den Funnel: welche Stationen hat JEDE Bewerbung durchlaufen. Eine
// Stufe zählt eine Bewerbung genau einmal — ein späterer Status impliziert
// die früheren (eine Einstellung ist auch ein geführtes Gespräch), und ein
// Aufsummieren von ever() würde eine Bewerbung, die beide Stationen hat,
// doppelt zählen.
const stationen = new Map();
const verlaufPaare = await dbAll(
'SELECT DISTINCT bewerbung_id, status FROM status_verlauf WHERE user_id = ?', [U]
);
verlaufPaare.forEach((r) => {
if (!stationen.has(r.bewerbung_id)) stationen.set(r.bewerbung_id, new Set());
stationen.get(r.bewerbung_id).add(r.status);
});
const everAny = (...statusListe) => {
let n = 0;
for (const menge of stationen.values()) {
if (statusListe.some((s) => menge.has(s))) n++;
}
return n;
};
const entwuerfe = cur('Entwurf'); const entwuerfe = cur('Entwurf');
const gesendet = total - entwuerfe; const gesendet = total - entwuerfe;
const gespraech = cur('Vorstellungsgespräch'); const gespraech = cur('Vorstellungsgespräch');
@@ -1755,7 +1777,8 @@ initializeDatabase().then(async () => {
const rueckStatus = [ const rueckStatus = [
'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert', 'Eingangsbestätigung', 'In Bearbeitung', 'Interessiert',
'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Absage', 'Absage von meiner Seite', 'Einstellung' 'Vorstellungsgespräch', 'Vertragsverhandlung',
'Absage', 'Absage von meiner Seite', 'Einstellung'
]; ];
const antworten = rueckStatus.reduce((a, s) => a + cur(s), 0); const antworten = rueckStatus.reduce((a, s) => a + cur(s), 0);
@@ -1763,13 +1786,15 @@ initializeDatabase().then(async () => {
const antwortquote = gesendet > 0 ? Math.round((antworten / gesendet) * 100) : 0; const antwortquote = gesendet > 0 ? Math.round((antworten / gesendet) * 100) : 0;
const avgProMonat = byMonth.length > 0 ? Math.round((total / byMonth.length) * 10) / 10 : 0; const avgProMonat = byMonth.length > 0 ? Math.round((total / byMonth.length) * 10) / 10 : 0;
// Funnel: distinct applications that ever reached the stage. Einstellung // Funnel: Bewerbungen, die die Stufe je erreicht haben. Ein späterer Status
// implies a Gespräch happened, so count those toward the Gespräch stage too. // impliziert die früheren — wer eingestellt wurde, hat auch verhandelt und
// ein Gespräch geführt, selbst wenn diese Zwischenschritte nie erfasst wurden.
const funnel = [ const funnel = [
{ label: 'Bewerbungen gesamt', count: total }, { label: 'Bewerbungen gesamt', count: total },
{ label: 'Versendet', count: ever('Gesendet') || gesendet }, { label: 'Versendet', count: ever('Gesendet') || gesendet },
{ label: 'Eingangsbestätigung', count: ever('Eingangsbestätigung') }, { label: 'Eingangsbestätigung', count: ever('Eingangsbestätigung') },
{ label: 'Vorstellungsgespräch', count: ever('Vorstellungsgespräch') + ever('Einstellung') }, { label: 'Vorstellungsgespräch', count: everAny('Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung') },
{ label: 'Vertragsverhandlung', count: everAny('Vertragsverhandlung', 'Einstellung') },
{ label: 'Einstellung', count: ever('Einstellung') } { label: 'Einstellung', count: ever('Einstellung') }
]; ];
+1
View File
@@ -16,6 +16,7 @@
'Warten auf Rückmeldung': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', 'Warten auf Rückmeldung': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
'Warten auf meine Antwort': 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-900 dark:text-fuchsia-200', 'Warten auf meine Antwort': 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-900 dark:text-fuchsia-200',
'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', 'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
'Vertragsverhandlung': 'bg-lime-100 text-lime-800 dark:bg-lime-900 dark:text-lime-200',
'Einstellung': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', 'Einstellung': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
'Absage': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', 'Absage': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
'Absage von meiner Seite': 'bg-rose-100 text-rose-800 dark:bg-rose-900 dark:text-rose-200', 'Absage von meiner Seite': 'bg-rose-100 text-rose-800 dark:bg-rose-900 dark:text-rose-200',
+3 -1
View File
@@ -147,7 +147,7 @@
// Status-Reihenfolge synchron zu public/js/main.js halten // Status-Reihenfolge synchron zu public/js/main.js halten
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung', const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort', 'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung']; 'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
const statusDot = { const statusDot = {
'Entwurf': 'bg-purple-500', 'Entwurf': 'bg-purple-500',
'Gesendet': 'bg-indigo-500', 'Gesendet': 'bg-indigo-500',
@@ -157,6 +157,7 @@
'Warten auf Rückmeldung': 'bg-orange-500', 'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500', 'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500', 'Vorstellungsgespräch': 'bg-yellow-500',
'Vertragsverhandlung': 'bg-lime-500',
'Einstellung': 'bg-green-500', 'Einstellung': 'bg-green-500',
'Absage': 'bg-red-500', 'Absage': 'bg-red-500',
'Absage von meiner Seite': 'bg-rose-500', 'Absage von meiner Seite': 'bg-rose-500',
@@ -219,6 +220,7 @@
app.status === 'Absage' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' : app.status === 'Absage' ? 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100' :
app.status === 'Absage von meiner Seite' ? 'bg-rose-100 text-rose-800 dark:bg-rose-800 dark:text-rose-100' : app.status === 'Absage von meiner Seite' ? 'bg-rose-100 text-rose-800 dark:bg-rose-800 dark:text-rose-100' :
app.status === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' : app.status === 'Vorstellungsgespräch' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100' :
app.status === 'Vertragsverhandlung' ? 'bg-lime-100 text-lime-800 dark:bg-lime-800 dark:text-lime-100' :
app.status === 'In Bearbeitung' ? 'bg-teal-100 text-teal-800 dark:bg-teal-800 dark:text-teal-100' : app.status === 'In Bearbeitung' ? 'bg-teal-100 text-teal-800 dark:bg-teal-800 dark:text-teal-100' :
app.status === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' : app.status === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' :
app.status === 'Warten auf Rückmeldung' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' : app.status === 'Warten auf Rückmeldung' ? 'bg-orange-100 text-orange-800 dark:bg-orange-800 dark:text-orange-100' :
+2 -2
View File
@@ -239,7 +239,7 @@
const statusColor = (s) => ({ const statusColor = (s) => ({
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1', 'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3', 'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Vorstellungsgespräch':'#10b981','Absage':'#f43f5e','Absage von meiner Seite':'#f97316', 'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1' 'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
}[s] || '#2563eb'); }[s] || '#2563eb');
%> %>
@@ -313,7 +313,7 @@
const statusColor = (s) => ({ const statusColor = (s) => ({
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1', 'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3', 'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Vorstellungsgespräch':'#10b981','Absage':'#f43f5e','Absage von meiner Seite':'#f97316', 'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1' 'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
}[s] || '#2563eb'); }[s] || '#2563eb');
%> %>