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:
@@ -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
@@ -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
@@ -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
@@ -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],
|
||||||
|
|||||||
@@ -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') }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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');
|
||||||
%>
|
%>
|
||||||
|
|||||||
Reference in New Issue
Block a user