Aktivitäten-Feed: empfangene und gesendete E-Mails einbeziehen

Der Aktivitäten-Feed (Startseite, /aktivitaeten, /statistik) führt Status-
änderungen und E-Mails per UNION zusammen, sortiert nach Erfassungszeit.
E-Mail-Einträge bekommen einen eigenen Farbpunkt (blau = erhalten, sky =
gesendet); ohne verknüpfte Bewerbung wird Absender bzw. Empfänger als
Kopfzeile gezeigt. Das UNION steckt in einem Subselect, weil SQLite ORDER BY
an einem Compound-SELECT keine Ergebnisspalten nach Namen aufloest. Die
Paginierung von /aktivitaeten zaehlt jetzt Statusaenderungen + E-Mails.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:03:20 +02:00
co-authored by Claude
parent fc1b68ebb4
commit 7c92f23351
4 changed files with 127 additions and 67 deletions
+22 -11
View File
@@ -69,28 +69,39 @@
<% if (aktivitaet.length) { %>
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
<% aktivitaet.forEach(function (a) { %>
<% aktivitaet.forEach(function (a) {
var dotCls = a.typ === 'email'
? (a.direction === 'out' ? 'bg-sky-500' : 'bg-blue-500')
: (statusDot[a.status] || 'bg-gray-300');
var titel = '', titelSub = '';
if (a.bewerbung_id) {
titel = a.firma || '(ohne Firma)';
titelSub = a.stelle || '';
} else if (a.typ === 'email') {
titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail');
} else {
titel = 'Gelöschte Bewerbung';
}
%>
<li>
<% if (a.bewerbung_id) { %>
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<% } else { %>
<div class="flex items-start gap-3 px-6 py-3">
<% } %>
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= statusDot[a.status] || 'bg-gray-300' %>"></span>
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= dotCls %>"></span>
<span class="min-w-0 flex-1">
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<% if (a.bewerbung_id) { %>
<span class="text-sm font-medium text-gray-900 dark:text-white break-words"><%= a.firma || '(ohne Firma)' %></span>
<% if (a.stelle) { %>
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= a.stelle %></span>
<% } %>
<% } else { %>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Gelöschte Bewerbung</span>
<span class="text-sm font-medium <%= a.bewerbung_id ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400' %> break-words"><%= titel %></span>
<% if (titelSub) { %>
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= titelSub %></span>
<% } %>
</span>
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
<% if (a.datum) { %>
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
<% } %>
<% if (a.kommentar && a.kommentar.trim()) { %>
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
<% } %>
@@ -102,7 +113,7 @@
</ul>
<% } else { %>
<div class="px-6 py-16 text-center text-sm text-gray-500 dark:text-gray-400">
Noch keine Statusänderungen protokolliert.
Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
</div>
<% } %>
+31 -14
View File
@@ -348,27 +348,42 @@
<% if (aktivitaet && aktivitaet.length) { %>
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
<% aktivitaet.forEach(function (a) { %>
<% aktivitaet.forEach(function (a) {
// E-Mail-Einträge bekommen einen eigenen Farbpunkt; bei Status-
// änderungen greift die Status-Farbpalette.
var dotCls = a.typ === 'email'
? (a.direction === 'out' ? 'bg-sky-500' : 'bg-blue-500')
: (statusDot[a.status] || 'bg-gray-300');
// Kopzeile: verknüpfte Bewerbung -> Firma·Stelle, sonst bei E-Mail
// der Absender/Empfänger, sonst Hinweis auf gelöschte Bewerbung.
var titel = '', titelSub = '';
if (a.bewerbung_id) {
titel = a.firma || '(ohne Firma)';
titelSub = a.stelle || '';
} else if (a.typ === 'email') {
titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail');
titelSub = '';
} else {
titel = 'Gelöschte Bewerbung';
}
%>
<li>
<!-- Eintrag zu einer noch existierenden Bewerbung ist ein Link, einer zu
einer gelöschten bleibt ein Block. Bewusst zwei ausgeschriebene Zweige:
den Tag-Namen samt href per <%%= %> zusammenzusetzen escaped die
Anführungszeichen und erzeugt Links wie /%22/bewerbung/121%22. -->
einer gelöschten oder einer nicht zugeordneten E-Mail bleibt ein Block.
Bewusst zwei ausgeschriebene Zweige: den Tag-Namen samt href per <%%= %>
zusammenzusetzen escaped die Anführungszeichen und erzeugt Links wie
/%22/bewerbung/121%22. -->
<% if (a.bewerbung_id) { %>
<a href="/bewerbung/<%= a.bewerbung_id %>" class="flex items-start gap-3 px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<% } else { %>
<div class="flex items-start gap-3 px-6 py-3">
<% } %>
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= statusDot[a.status] || 'bg-gray-300' %>"></span>
<span class="mt-1.5 inline-block w-2.5 h-2.5 rounded-full shrink-0 <%= dotCls %>"></span>
<span class="min-w-0 flex-1">
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<% if (a.bewerbung_id) { %>
<span class="text-sm font-medium text-gray-900 dark:text-white break-words"><%= a.firma || '(ohne Firma)' %></span>
<% if (a.stelle) { %>
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= a.stelle %></span>
<% } %>
<% } else { %>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Gelöschte Bewerbung</span>
<span class="text-sm font-medium <%= a.bewerbung_id ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400' %> break-words"><%= titel %></span>
<% if (titelSub) { %>
<span class="text-sm text-gray-500 dark:text-gray-400 break-words">· <%= titelSub %></span>
<% } %>
</span>
<!-- Status, Datum und Kommentar in einer Zeile: ein rechtsbündiges
@@ -376,7 +391,9 @@
dort ohne Bezug stehen. -->
<span class="mt-0.5 flex flex-wrap items-baseline gap-x-2 text-sm">
<span class="font-medium text-gray-700 dark:text-gray-200"><%= a.status %></span>
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
<% if (a.datum) { %>
<span class="text-gray-400 dark:text-gray-500 whitespace-nowrap">· <%= new Date(a.datum).toLocaleDateString('de-DE') %></span>
<% } %>
<% if (a.kommentar && a.kommentar.trim()) { %>
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
<% } %>
@@ -388,7 +405,7 @@
</ul>
<% } else { %>
<div class="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
Noch keine Statusänderungen protokolliert.
Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
</div>
<% } %>
</section>
+16 -9
View File
@@ -309,7 +309,7 @@
<div class="stat-card">
<h2 id="aktivitaet" class="text-gray-900 dark:text-white">Letzte Aktivität</h2>
<p class="sub mb-5">
Die jüngsten Statusänderungen deiner Bewerbungen.
Die jüngsten Statusänderungen und E-Mails deiner Bewerbungen.
<a href="/aktivitaeten" class="text-blue-600 dark:text-sky-400 hover:underline">Alle</a>
</p>
<% if (aktivitaet.length) {
@@ -319,29 +319,36 @@
'Vorstellungsgespräch':'#10b981','Vertragsverhandlung':'#84cc16','Absage':'#f43f5e','Absage von meiner Seite':'#f97316',
'Einstellung':'#059669','Keine Rückmeldung':'#cbd5e1'
}[s] || '#2563eb');
const emailColor = (a) => a.direction === 'out' ? '#0ea5e9' : '#3b82f6';
%>
<div class="tl">
<% aktivitaet.forEach(a => { %>
<div class="tl-item" style="--stat-accent:<%= statusColor(a.status) %>">
<% aktivitaet.forEach(a => {
const accent = a.typ === 'email' ? emailColor(a) : statusColor(a.status);
let titel = '', titelSub = '';
if (a.bewerbung_id) { titel = a.firma || '(ohne Firma)'; titelSub = a.stelle || ''; }
else if (a.typ === 'email') { titel = a.direction === 'out' ? ('An: ' + (a.to_addr || '?')) : (a.from_addr || a.subject || 'E-Mail'); }
else { titel = 'Gelöschte Bewerbung'; }
%>
<div class="tl-item" style="--stat-accent:<%= accent %>">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<% if (a.bewerbung_id) { %>
<a href="/bewerbung/<%= a.bewerbung_id %>" class="text-sm font-medium text-gray-900 dark:text-white hover:underline"><%= a.firma || '(ohne Firma)' %></a>
<span class="text-sm text-gray-500 dark:text-slate-400">· <%= a.stelle || '' %></span>
<a href="/bewerbung/<%= a.bewerbung_id %>" class="text-sm font-medium text-gray-900 dark:text-white hover:underline"><%= titel %></a>
<% if (titelSub) { %><span class="text-sm text-gray-500 dark:text-slate-400">· <%= titelSub %></span><% } %>
<% } else { %>
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Gelöschte Bewerbung</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-200"><%= titel %></span>
<% } %>
</div>
<div class="mt-0.5 flex flex-wrap items-center gap-x-2 text-sm">
<span class="inline-flex items-center gap-1.5 font-medium" style="color:<%= statusColor(a.status) %>">
<span class="inline-flex items-center gap-1.5 font-medium" style="color:<%= accent %>">
<span class="swatch" style="margin:0"></span><%= a.status %>
</span>
<span class="text-gray-400 dark:text-slate-500">· <%= a.datum %></span>
<% if (a.datum) { %><span class="text-gray-400 dark:text-slate-500">· <%= a.datum %></span><% } %>
<% if (a.kommentar) { %><span class="text-gray-500 dark:text-slate-400 truncate">· <%= a.kommentar %></span><% } %>
</div>
</div>
<% }); %>
</div>
<% } else { %><div class="stat-empty">Noch keine Statusänderungen protokolliert.</div><% } %>
<% } else { %><div class="stat-empty">Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.</div><% } %>
</div>
<% } %>