Startseite: Letzte Aktivitaeten und Gesamtzahl der Bewerbungen

Unter der Liste stehen jetzt die acht juengsten Statusaenderungen quer ueber
alle Bewerbungen - mit Firma, Stelle, Status, Datum und Kommentar, verlinkt auf
die Bewerbung. Sortiert nach Erfassungszeit statt nach fachlichem Datum, damit
ein nachgetragener alter Eintrag nicht oben steht.

Der Kopf der Listen-Karte nennt die Gesamtzahl. Er sitzt bewusst ausserhalb des
md:min-w-Rahmens, damit er auf dem Handy nicht horizontal mitscrollt.

Beides verschwindet waehrend einer Suche, wie Liste und Termine auch. Die
Status-Farbkarte ist eine Ebene hoeher gewandert, weil Liste und Aktivitaeten
sie jetzt beide brauchen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 11:33:39 +02:00
co-authored by Claude Opus 4.8
parent ab62399f6f
commit 7b60fe734a
3 changed files with 101 additions and 21 deletions
+15
View File
@@ -1647,6 +1647,20 @@ initializeDatabase().then(async () => {
GROUP BY status ORDER BY count DESC
`, [U]);
// Letzte Aktivitäten für die Startseite: die jüngsten Statusänderungen, quer
// über alle Bewerbungen. Sortiert nach Erfassungszeit (created_at), nicht nach
// dem fachlichen Datum — sonst würde ein nachgetragener alter Eintrag oben
// stehen, obwohl gerade etwas anderes passiert ist.
const aktivitaet = await dbAll(`
SELECT sv.datum, sv.status, sv.kommentar,
b.id AS bewerbung_id, b.firma, b.stelle
FROM status_verlauf sv
LEFT JOIN bewerbungen b ON b.id = sv.bewerbung_id AND b.user_id = sv.user_id
WHERE sv.user_id = ?
ORDER BY datetime(sv.created_at) DESC, sv.id DESC
LIMIT 8
`, [U]);
// Get available months/years for filter
const availableMonths = await dbAll(`
SELECT DISTINCT strftime("%Y-%m", datum) as yearmonth,
@@ -1683,6 +1697,7 @@ initializeDatabase().then(async () => {
availableMonths,
exportMonths,
currentFilter: { month, year },
aktivitaet,
kommendeTermine,
caldavTz: caldav.TZ,
artOptions: ART_OPTIONS,
+85 -20
View File
@@ -130,7 +130,39 @@
transform: rotate(90deg);
}
</style>
<%
// 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',
'Vorstellungsgespräch', 'Vertragsverhandlung', 'Einstellung', 'Absage', 'Absage von meiner Seite', 'Keine Rückmeldung'];
const statusDot = {
'Entwurf': 'bg-purple-500',
'Gesendet': 'bg-indigo-500',
'Eingangsbestätigung': 'bg-blue-500',
'In Bearbeitung': 'bg-teal-500',
'Interessiert': 'bg-pink-500',
'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500',
'Vertragsverhandlung': 'bg-lime-500',
'Einstellung': 'bg-green-500',
'Absage': 'bg-red-500',
'Absage von meiner Seite': 'bg-rose-500',
'Keine Rückmeldung': 'bg-gray-400',
'Ohne Status': 'bg-gray-300'
};
%>
<div id="listCard" class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-x-auto">
<!-- Kopf der Karte: Gesamtzahl der Bewerbungen. Bewusst außerhalb des
md:min-w-Rahmens unten, damit er auf dem Handy nicht mitscrollt. -->
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-6 py-4">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Bewerbungen</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
<span class="font-semibold text-gray-800 dark:text-gray-100"><%= statistics.total %></span>
<%= statistics.total === 1 ? 'Bewerbung' : 'Bewerbungen' %> gesamt
</p>
</div>
<div class="md:min-w-[760px]">
<!-- Column labels (desktop only — on phones the rows become stacked cards) -->
<div class="app-head app-grid px-6 py-3 bg-gray-50 dark:bg-gray-700 text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
@@ -144,26 +176,6 @@
</div>
<% if (applications.length > 0) { %>
<%
// Status-Reihenfolge synchron zu public/js/main.js halten
const statusOrder = ['Entwurf', 'Gesendet', 'Eingangsbestätigung',
'In Bearbeitung', 'Interessiert', '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',
'Gesendet': 'bg-indigo-500',
'Eingangsbestätigung': 'bg-blue-500',
'In Bearbeitung': 'bg-teal-500',
'Interessiert': 'bg-pink-500',
'Warten auf Rückmeldung': 'bg-orange-500',
'Warten auf meine Antwort': 'bg-fuchsia-500',
'Vorstellungsgespräch': 'bg-yellow-500',
'Vertragsverhandlung': 'bg-lime-500',
'Einstellung': 'bg-green-500',
'Absage': 'bg-red-500',
'Absage von meiner Seite': 'bg-rose-500',
'Keine Rückmeldung': 'bg-gray-400',
'Ohne Status': 'bg-gray-300'
};
const groups = {};
applications.forEach(a => {
const key = (a.status && a.status.trim()) ? a.status.trim() : 'Ohne Status';
@@ -315,6 +327,55 @@
</div>
</div>
<!-- Letzte Aktivitäten: die jüngsten Statusänderungen quer über alle
Bewerbungen — der schnellste Blick darauf, was zuletzt passiert ist,
ohne jede Gruppe aufklappen zu müssen. -->
<section id="aktivitaetCard" class="bg-white dark:bg-gray-800 rounded-lg shadow-md mt-8">
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white">Letzte Aktivitäten</h2>
<a href="/statistik#aktivitaet" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Alle in der Statistik</a>
</div>
<% if (aktivitaet && aktivitaet.length) { %>
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
<% aktivitaet.forEach(function (a) { %>
<li>
<% var ziel = a.bewerbung_id ? ('/bewerbung/' + a.bewerbung_id) : null; %>
<<%= ziel ? 'a href="' + ziel + '"' : 'div' %> class="flex items-start gap-3 px-6 py-3 <%= ziel ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors' : '' %>">
<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="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>
<!-- Status, Datum und Kommentar in einer Zeile: ein rechtsbündiges
Datum würde auf dem Handy in eine eigene Zeile umbrechen und
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.kommentar && a.kommentar.trim()) { %>
<span class="text-gray-500 dark:text-gray-400 break-words">· <%= a.kommentar %></span>
<% } %>
</span>
</span>
</<%= ziel ? 'a' : 'div' %>>
</li>
<% }); %>
</ul>
<% } else { %>
<div class="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400">
Noch keine Statusänderungen protokolliert.
</div>
<% } %>
</section>
<script>
(function () {
function qsa(sel, ctx) {
@@ -364,6 +425,7 @@
var fehler = document.getElementById('searchError');
var listCard = document.getElementById('listCard');
var termineCard = document.getElementById('termineCard');
var aktivitaetCard = document.getElementById('aktivitaetCard');
var MIN = 2; // kürzere Eingaben liefern nur Rauschen
var MARK_START = '\u0002'; // Fundstellen-Marker aus lib/suche.js
@@ -479,6 +541,7 @@
panel.classList.add('hidden');
if (listCard) listCard.classList.remove('hidden');
if (termineCard) termineCard.classList.remove('hidden');
if (aktivitaetCard) aktivitaetCard.classList.remove('hidden');
input.setAttribute('aria-expanded', 'false');
aktiv = -1;
}
@@ -498,6 +561,7 @@
panel.classList.remove('hidden');
if (listCard) listCard.classList.add('hidden');
if (termineCard) termineCard.classList.add('hidden');
if (aktivitaetCard) aktivitaetCard.classList.add('hidden');
input.setAttribute('aria-expanded', 'true');
aktiv = -1;
}
@@ -511,6 +575,7 @@
panel.classList.remove('hidden');
if (listCard) listCard.classList.add('hidden');
if (termineCard) termineCard.classList.add('hidden');
if (aktivitaetCard) aktivitaetCard.classList.add('hidden');
}
async function suchen(q) {
+1 -1
View File
@@ -307,7 +307,7 @@
<!-- Recent activity -->
<div class="stat-card">
<h2 class="text-gray-900 dark:text-white">Letzte Aktivität</h2>
<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.</p>
<% if (aktivitaet.length) {
const statusColor = (s) => ({