Termine: alle anstehenden auf Startseite + Bearbeiten; Status Telefonat

- Startseite zeigt alle anstehenden Termine (vorher auf 6 begrenzt)
- Termine lassen sich inline bearbeiten (CalDAV-Update, Neu-Anlage bei 404/412)
- Neuer Bewerbungsstatus Telefonat (Listen, Farben, API, PDF, Statistik)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:27:19 +02:00
co-authored by Claude
parent ce5fa1eab1
commit d9ba8698e0
9 changed files with 184 additions and 14 deletions
+1
View File
@@ -15,6 +15,7 @@
'Eingangsbestätigung': 'bg-blue-500',
'In Bearbeitung': 'bg-teal-500',
'Interessiert': 'bg-pink-500',
'Telefonat': 'bg-sky-500',
'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500',
+79 -5
View File
@@ -13,6 +13,7 @@
'Eingangsbestätigung': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
'In Bearbeitung': 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200',
'Interessiert': 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200',
'Telefonat': 'bg-sky-100 text-sky-800 dark:bg-sky-900 dark:text-sky-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',
'Vorstellungsgespräch': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
@@ -730,7 +731,7 @@
var bis = (!t.ganztags && t.ende) ? new Date(t.ende).toLocaleTimeString('de-DE', { timeZone: caldavTz, hour: '2-digit', minute: '2-digit' }) : '';
%>
<li class="flex flex-wrap items-start justify-between gap-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/40 px-3 py-2">
<div class="min-w-0">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= t.typ === 'vorstellungsgespraech' ? 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200' : 'bg-gray-200 text-gray-700 dark:bg-gray-600 dark:text-gray-200' %>"><%= t.typ === 'vorstellungsgespraech' ? 'Vorstellungsgespräch' : 'Termin' %></span>
<span class="text-sm font-medium text-gray-800 dark:text-gray-100 break-words"><%= t.titel %></span>
@@ -739,12 +740,74 @@
<% if (t.ort) { %><p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">📍 <%= t.ort %></p><% } %>
<% if (t.notiz) { %><p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 whitespace-pre-line"><%= t.notiz %></p><% } %>
</div>
<form action="/bewerbung/<%= application.id %>/termine/<%= t.id %>/delete" method="POST" onsubmit="return confirm('Diesen Termin löschen (auch im Kalender)?');">
<button type="submit" class="inline-flex items-center gap-1 px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
Löschen
<div class="flex shrink-0 items-center gap-2">
<button type="button" onclick="document.getElementById('terminEdit_<%= t.id %>').open = !document.getElementById('terminEdit_<%= t.id %>').open;" class="inline-flex items-center gap-1 px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Bearbeiten
</button>
<form action="/bewerbung/<%= application.id %>/termine/<%= t.id %>/delete" method="POST" onsubmit="return confirm('Diesen Termin löschen (auch im Kalender)?');">
<button type="submit" class="inline-flex items-center gap-1 px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
Löschen
</button>
</form>
</div>
<details id="terminEdit_<%= t.id %>" class="mt-2">
<form action="/bewerbung/<%= application.id %>/termine/<%= t.id %>/edit" method="POST" class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3 space-y-3">
<div class="grid gap-3 sm:grid-cols-2">
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Art</span>
<select name="typ" class="terminEditTyp mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<option value="vorstellungsgespraech" <%= t.typ === 'vorstellungsgespraech' ? 'selected' : '' %>>Vorstellungsgespräch</option>
<option value="termin" <%= t.typ === 'termin' ? 'selected' : '' %>>Allgemeiner Termin</option>
</select>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Titel</span>
<input name="titel" value="<%= t.titel %>" placeholder="z. B. Vorstellungsgespräch" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Datum</span>
<input name="datum" type="date" required value="<%= t.editDatum %>" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs terminEditZeit">
<span class="text-gray-500 dark:text-gray-400">Uhrzeit (von / bis)</span>
<span class="mt-1 flex items-center gap-2">
<input name="von" type="time" value="<%= t.editVon %>" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<span class="text-gray-400"></span>
<input name="bis" type="time" value="<%= t.editBis %>" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</span>
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Ort</span>
<input name="ort" value="<%= t.ort || '' %>" placeholder="Adresse oder Ort" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Erinnerung</span>
<select name="erinnerung_min" class="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-2 py-1.5 text-sm">
<% [0,15,30,60,120,1440].forEach(function(m){ %>
<option value="<%= m %>" <%= Number(t.erinnerung_min) === m ? 'selected' : '' %>><%= m === 0 ? 'keine' : (m < 60 ? m + ' Min vorher' : (m < 1440 ? (m/60) + ' Stunde' + (m/60 > 1 ? 'n' : '') + ' vorher' : '1 Tag vorher')) %></option>
<% }); %>
</select>
</label>
</div>
<label class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300">
<input type="checkbox" name="ganztags" value="1" class="terminEditGanztags rounded border-gray-300 dark:border-gray-600 text-blue-600" <%= t.ganztags ? 'checked' : '' %>>
Ganztägig
</label>
<label class="block text-xs">
<span class="text-gray-500 dark:text-gray-400">Notiz</span>
<textarea name="notiz" rows="2" 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"><%= t.notiz || '' %></textarea>
</label>
<div class="flex items-center gap-2">
<button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm 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
</button>
<button type="button" onclick="document.getElementById('terminEdit_<%= t.id %>').open = false;" class="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">Abbrechen</button>
</div>
</form>
</details>
</li>
<% }); %>
</ul>
@@ -820,6 +883,17 @@
var sync = function () { wrap.style.opacity = chk.checked ? '0.4' : '1'; wrap.querySelectorAll('input').forEach(function (i) { i.disabled = chk.checked; }); };
chk.addEventListener('change', sync); sync();
}
// Bearbeiten-Formulare: „Ganztägig" deaktiviert die Uhrzeit-Felder.
document.querySelectorAll('input.terminEditGanztags').forEach(function (g) {
var form = g.closest('form');
if (!form) return;
var zeit = form.querySelector('.terminEditZeit');
var sync = function () {
zeit.style.opacity = g.checked ? '0.4' : '1';
zeit.querySelectorAll('input').forEach(function (i) { i.disabled = g.checked; });
};
g.addEventListener('change', sync); sync();
});
})();
</script>
</section>
+3 -1
View File
@@ -145,7 +145,7 @@
// Status-Reihenfolge synchron zu public/js/main.js halten. Steht hier oben,
// weil sowohl die Liste als auch die Aktivitäten weiter unten sie brauchen.
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
const statusDot = {
'Entwurf': 'bg-purple-500',
@@ -153,6 +153,7 @@
'Eingangsbestätigung': 'bg-blue-500',
'In Bearbeitung': 'bg-teal-500',
'Interessiert': 'bg-pink-500',
'Telefonat': 'bg-sky-500',
'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500',
@@ -246,6 +247,7 @@
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 === 'Interessiert' ? 'bg-pink-100 text-pink-800 dark:bg-pink-800 dark:text-pink-100' :
app.status === 'Telefonat' ? 'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-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 meine Antwort' ? 'bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-800 dark:text-fuchsia-100' :
app.status === 'Entwurf' ? 'bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100' :
+2 -2
View File
@@ -238,7 +238,7 @@
const maxS = Math.max(...byStatus.map(s => s.count));
const statusColor = (s) => ({
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Interessiert':'#8b5cf6','Telefonat':'#f472b6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
}[s] || '#2563eb');
@@ -315,7 +315,7 @@
<% if (aktivitaet.length) {
const statusColor = (s) => ({
'Entwurf':'#94a3b8','Gesendet':'#3b82f6','Eingangsbestätigung':'#0ea5e9','In Bearbeitung':'#6366f1',
'Interessiert':'#8b5cf6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Interessiert':'#8b5cf6','Telefonat':'#f472b6','Warten auf Rückmeldung':'#a3a3a3','Warten auf meine Antwort':'#a3a3a3',
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
}[s] || '#2563eb');