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:
@@ -1647,6 +1647,20 @@ initializeDatabase().then(async () => {
|
|||||||
GROUP BY status ORDER BY count DESC
|
GROUP BY status ORDER BY count DESC
|
||||||
`, [U]);
|
`, [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
|
// Get available months/years for filter
|
||||||
const availableMonths = await dbAll(`
|
const availableMonths = await dbAll(`
|
||||||
SELECT DISTINCT strftime("%Y-%m", datum) as yearmonth,
|
SELECT DISTINCT strftime("%Y-%m", datum) as yearmonth,
|
||||||
@@ -1683,6 +1697,7 @@ initializeDatabase().then(async () => {
|
|||||||
availableMonths,
|
availableMonths,
|
||||||
exportMonths,
|
exportMonths,
|
||||||
currentFilter: { month, year },
|
currentFilter: { month, year },
|
||||||
|
aktivitaet,
|
||||||
kommendeTermine,
|
kommendeTermine,
|
||||||
caldavTz: caldav.TZ,
|
caldavTz: caldav.TZ,
|
||||||
artOptions: ART_OPTIONS,
|
artOptions: ART_OPTIONS,
|
||||||
|
|||||||
+85
-20
@@ -130,7 +130,39 @@
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
</style>
|
</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">
|
<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]">
|
<div class="md:min-w-[760px]">
|
||||||
<!-- Column labels (desktop only — on phones the rows become stacked cards) -->
|
<!-- 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">
|
<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>
|
</div>
|
||||||
<% if (applications.length > 0) { %>
|
<% 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 = {};
|
const groups = {};
|
||||||
applications.forEach(a => {
|
applications.forEach(a => {
|
||||||
const key = (a.status && a.status.trim()) ? a.status.trim() : 'Ohne Status';
|
const key = (a.status && a.status.trim()) ? a.status.trim() : 'Ohne Status';
|
||||||
@@ -315,6 +327,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
function qsa(sel, ctx) {
|
function qsa(sel, ctx) {
|
||||||
@@ -364,6 +425,7 @@
|
|||||||
var fehler = document.getElementById('searchError');
|
var fehler = document.getElementById('searchError');
|
||||||
var listCard = document.getElementById('listCard');
|
var listCard = document.getElementById('listCard');
|
||||||
var termineCard = document.getElementById('termineCard');
|
var termineCard = document.getElementById('termineCard');
|
||||||
|
var aktivitaetCard = document.getElementById('aktivitaetCard');
|
||||||
|
|
||||||
var MIN = 2; // kürzere Eingaben liefern nur Rauschen
|
var MIN = 2; // kürzere Eingaben liefern nur Rauschen
|
||||||
var MARK_START = '\u0002'; // Fundstellen-Marker aus lib/suche.js
|
var MARK_START = '\u0002'; // Fundstellen-Marker aus lib/suche.js
|
||||||
@@ -479,6 +541,7 @@
|
|||||||
panel.classList.add('hidden');
|
panel.classList.add('hidden');
|
||||||
if (listCard) listCard.classList.remove('hidden');
|
if (listCard) listCard.classList.remove('hidden');
|
||||||
if (termineCard) termineCard.classList.remove('hidden');
|
if (termineCard) termineCard.classList.remove('hidden');
|
||||||
|
if (aktivitaetCard) aktivitaetCard.classList.remove('hidden');
|
||||||
input.setAttribute('aria-expanded', 'false');
|
input.setAttribute('aria-expanded', 'false');
|
||||||
aktiv = -1;
|
aktiv = -1;
|
||||||
}
|
}
|
||||||
@@ -498,6 +561,7 @@
|
|||||||
panel.classList.remove('hidden');
|
panel.classList.remove('hidden');
|
||||||
if (listCard) listCard.classList.add('hidden');
|
if (listCard) listCard.classList.add('hidden');
|
||||||
if (termineCard) termineCard.classList.add('hidden');
|
if (termineCard) termineCard.classList.add('hidden');
|
||||||
|
if (aktivitaetCard) aktivitaetCard.classList.add('hidden');
|
||||||
input.setAttribute('aria-expanded', 'true');
|
input.setAttribute('aria-expanded', 'true');
|
||||||
aktiv = -1;
|
aktiv = -1;
|
||||||
}
|
}
|
||||||
@@ -511,6 +575,7 @@
|
|||||||
panel.classList.remove('hidden');
|
panel.classList.remove('hidden');
|
||||||
if (listCard) listCard.classList.add('hidden');
|
if (listCard) listCard.classList.add('hidden');
|
||||||
if (termineCard) termineCard.classList.add('hidden');
|
if (termineCard) termineCard.classList.add('hidden');
|
||||||
|
if (aktivitaetCard) aktivitaetCard.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function suchen(q) {
|
async function suchen(q) {
|
||||||
|
|||||||
+1
-1
@@ -307,7 +307,7 @@
|
|||||||
|
|
||||||
<!-- Recent activity -->
|
<!-- Recent activity -->
|
||||||
<div class="stat-card">
|
<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>
|
<p class="sub mb-5">Die jüngsten Statusänderungen deiner Bewerbungen.</p>
|
||||||
<% if (aktivitaet.length) {
|
<% if (aktivitaet.length) {
|
||||||
const statusColor = (s) => ({
|
const statusColor = (s) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user