Aktivitaeten: eigene Seite mit Pagination, kaputte Eintragslinks repariert
Der Link unter "Letzte Aktivitaeten" heisst jetzt "Alle" und fuehrt auf /aktivitaeten - den vollstaendigen Verlauf ueber alle Bewerbungen, 25 Eintraege je Seite. Die Blaetterleiste sind echte Links (kein JS), damit Zurueck-Taste, Lesezeichen und Neuer-Tab funktionieren; eine Seitenzahl ausserhalb des Bereichs wird auf den gueltigen Rand geklemmt statt eine leere Seite zu zeigen. Dabei ein Fehler von mir aus dem letzten Commit behoben: Ich hatte den Tag-Namen samt href per <%= %> zusammengesetzt, was die Anfuehrungszeichen escaped - heraus kam href=""/bewerbung/121"" und damit die URL /%22/bewerbung/121%22. Jetzt zwei ausgeschriebene Zweige (Link bzw. Block). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1710,6 +1710,46 @@ initializeDatabase().then(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Alle Aktivitäten (Statusänderungen), seitenweise. Die Startseite zeigt nur die
|
||||||
|
// jüngsten acht — hier steht der vollständige Verlauf über alle Bewerbungen.
|
||||||
|
const AKTIVITAETEN_PRO_SEITE = 25;
|
||||||
|
|
||||||
|
app.get('/aktivitaeten', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const U = uid();
|
||||||
|
const gesamtRow = await dbGet('SELECT COUNT(*) AS anzahl FROM status_verlauf WHERE user_id = ?', [U]);
|
||||||
|
const gesamt = gesamtRow ? gesamtRow.anzahl : 0;
|
||||||
|
const seiten = Math.max(1, Math.ceil(gesamt / AKTIVITAETEN_PRO_SEITE));
|
||||||
|
|
||||||
|
// Eine Seite außerhalb des Bereichs (getippt oder nach dem Löschen von
|
||||||
|
// Einträgen) klemmen wir auf den gültigen Rand, statt eine leere Seite zu zeigen.
|
||||||
|
const gewuenscht = parseInt(req.query.seite, 10);
|
||||||
|
const seite = Math.min(Math.max(Number.isInteger(gewuenscht) ? gewuenscht : 1, 1), seiten);
|
||||||
|
|
||||||
|
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 ? OFFSET ?
|
||||||
|
`, [U, AKTIVITAETEN_PRO_SEITE, (seite - 1) * AKTIVITAETEN_PRO_SEITE]);
|
||||||
|
|
||||||
|
res.render('aktivitaeten', {
|
||||||
|
aktivitaet,
|
||||||
|
gesamt,
|
||||||
|
seite,
|
||||||
|
seiten,
|
||||||
|
proSeite: AKTIVITAETEN_PRO_SEITE,
|
||||||
|
hideSettings: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading aktivitaeten:', error);
|
||||||
|
res.status(500).send('Serverfehler');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Statistik — comprehensive, on its own page. The dashboard only carries a
|
// Statistik — comprehensive, on its own page. The dashboard only carries a
|
||||||
// quick glance now; everything that needs room to breathe lives here.
|
// quick glance now; everything that needs room to breathe lives here.
|
||||||
app.get('/statistik', async (req, res) => {
|
app.get('/statistik', async (req, res) => {
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<%- include('partials/head') %>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
||||||
|
<%- include('partials/header') %>
|
||||||
|
|
||||||
|
<main class="flex-1 container mx-auto px-4 py-8 max-w-4xl">
|
||||||
|
<%
|
||||||
|
// Farben synchron zur Startseite (views/index.ejs) halten.
|
||||||
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Welche Seitenzahlen die Blätterleiste zeigt: erste, letzte, die aktuelle
|
||||||
|
// und je eine daneben — dazwischen Auslassungen (null). Bei wenigen Seiten
|
||||||
|
// stehen ohnehin alle da.
|
||||||
|
const zahlen = [];
|
||||||
|
for (let i = 1; i <= seiten; i++) {
|
||||||
|
const nah = Math.abs(i - seite) <= 1;
|
||||||
|
const rand = (i === 1 || i === seiten);
|
||||||
|
if (nah || rand) {
|
||||||
|
if (zahlen.length && zahlen[zahlen.length - 1] !== null && i - zahlen[zahlen.length - 1] > 1) zahlen.push(null);
|
||||||
|
zahlen.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const von = gesamt === 0 ? 0 : (seite - 1) * proSeite + 1;
|
||||||
|
const bis = Math.min(seite * proSeite, gesamt);
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Aktivitäten</h1>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Jede Statusänderung, über alle Bewerbungen hinweg – die jüngste zuerst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a href="/" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Zurück zu den Bewerbungen</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||||
|
<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">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<% if (gesamt) { %>
|
||||||
|
<span class="font-semibold text-gray-800 dark:text-gray-100"><%= von %>–<%= bis %></span>
|
||||||
|
von <span class="font-semibold text-gray-800 dark:text-gray-100"><%= gesamt %></span>
|
||||||
|
<%= gesamt === 1 ? 'Eintrag' : 'Einträgen' %>
|
||||||
|
<% } else { %>
|
||||||
|
Keine Einträge
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
<% if (seiten > 1) { %>
|
||||||
|
<p class="text-sm text-gray-400 dark:text-gray-500">Seite <%= seite %> von <%= seiten %></p>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (aktivitaet.length) { %>
|
||||||
|
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
||||||
|
<% aktivitaet.forEach(function (a) { %>
|
||||||
|
<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="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>
|
||||||
|
<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>
|
||||||
|
<% if (a.bewerbung_id) { %></a><% } else { %></div><% } %>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="px-6 py-16 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Noch keine Statusänderungen protokolliert.
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (seiten > 1) { %>
|
||||||
|
<!-- Blätterleiste: echte Links (kein JS), damit Zurück-Taste, Lesezeichen
|
||||||
|
und das Öffnen in einem neuen Tab funktionieren. -->
|
||||||
|
<nav class="flex flex-wrap items-center justify-center gap-1 px-6 py-4 border-t border-gray-200 dark:border-gray-700" aria-label="Seiten">
|
||||||
|
<% if (seite > 1) { %>
|
||||||
|
<a href="/aktivitaeten?seite=<%= seite - 1 %>" rel="prev"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 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="M15 19l-7-7 7-7"></path></svg>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded-md border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
|
||||||
|
<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="M15 19l-7-7 7-7"></path></svg>
|
||||||
|
Zurück
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% zahlen.forEach(function (n) { %>
|
||||||
|
<% if (n === null) { %>
|
||||||
|
<span class="px-2 text-sm text-gray-400 dark:text-gray-600">…</span>
|
||||||
|
<% } else if (n === seite) { %>
|
||||||
|
<span aria-current="page"
|
||||||
|
class="min-w-[2.25rem] text-center px-2 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white"><%= n %></span>
|
||||||
|
<% } else { %>
|
||||||
|
<a href="/aktivitaeten?seite=<%= n %>"
|
||||||
|
class="min-w-[2.25rem] text-center px-2 py-1.5 text-sm rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"><%= n %></a>
|
||||||
|
<% } %>
|
||||||
|
<% }); %>
|
||||||
|
|
||||||
|
<% if (seite < seiten) { %>
|
||||||
|
<a href="/aktivitaeten?seite=<%= seite + 1 %>" rel="next"
|
||||||
|
class="inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
Weiter
|
||||||
|
<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="M9 5l7 7-7 7"></path></svg>
|
||||||
|
</a>
|
||||||
|
<% } else { %>
|
||||||
|
<span class="inline-flex items-center gap-1 px-3 py-1.5 text-sm rounded-md border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
|
||||||
|
Weiter
|
||||||
|
<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="M9 5l7 7-7 7"></path></svg>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
</nav>
|
||||||
|
<% } %>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<%- include('partials/footer') %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+11
-4
@@ -333,15 +333,22 @@
|
|||||||
<section id="aktivitaetCard" class="bg-white dark:bg-gray-800 rounded-lg shadow-md mt-8">
|
<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">
|
<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>
|
<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>
|
<a href="/aktivitaeten" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Alle</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if (aktivitaet && aktivitaet.length) { %>
|
<% if (aktivitaet && aktivitaet.length) { %>
|
||||||
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
<ul class="divide-y divide-gray-100 dark:divide-gray-700/60">
|
||||||
<% aktivitaet.forEach(function (a) { %>
|
<% aktivitaet.forEach(function (a) { %>
|
||||||
<li>
|
<li>
|
||||||
<% var ziel = a.bewerbung_id ? ('/bewerbung/' + a.bewerbung_id) : null; %>
|
<!-- Eintrag zu einer noch existierenden Bewerbung ist ein Link, einer zu
|
||||||
<<%= 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' : '' %>">
|
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. -->
|
||||||
|
<% 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 <%= statusDot[a.status] || 'bg-gray-300' %>"></span>
|
||||||
<span class="min-w-0 flex-1">
|
<span class="min-w-0 flex-1">
|
||||||
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
<span class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||||
@@ -365,7 +372,7 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</<%= ziel ? 'a' : 'div' %>>
|
<% if (a.bewerbung_id) { %></a><% } else { %></div><% } %>
|
||||||
</li>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+4
-1
@@ -308,7 +308,10 @@
|
|||||||
<!-- Recent activity -->
|
<!-- Recent activity -->
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h2 id="aktivitaet" 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>
|
<p class="sub mb-5">
|
||||||
|
Die jüngsten Statusänderungen deiner Bewerbungen.
|
||||||
|
<a href="/aktivitaeten" class="text-blue-600 dark:text-sky-400 hover:underline">Alle</a>
|
||||||
|
</p>
|
||||||
<% if (aktivitaet.length) {
|
<% if (aktivitaet.length) {
|
||||||
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',
|
||||||
|
|||||||
Reference in New Issue
Block a user