diff --git a/server.js b/server.js index e3929e0..96451f0 100644 --- a/server.js +++ b/server.js @@ -1644,6 +1644,51 @@ initializeDatabase().then(async () => { // files are isolated on disk the same way their DB rows are. const userDir = userStorageDir; + // Einheitlicher Aktivitäten-Feed: Statusänderungen UND E-Mails (empfangen wie + // gesendet) werden zusammengeführt und chronologisch nach Erfassungszeit + // sortiert. `typ` ('status' | 'email') unterscheidet die Herkunft für die + // Darstellung; direction/from_addr/to_addr sind nur bei E-Mails gefüllt. + // Das UNION steckt in einem Subselect, weil SQLite ORDER BY an einem Compound- + // SELECT keine Ergebnisspalten nach Namen auflöst — erst der äußere SELECT + // sieht created_at/sort_id als echte Spalten. + const AKTIVITAET_FEED_INNER = ` + SELECT 'status' AS typ, sv.id AS sort_id, sv.created_at, sv.datum, + sv.status, sv.kommentar, sv.bewerbung_id, + b.firma, b.stelle, + NULL AS direction, NULL AS subject, NULL AS from_addr, NULL AS to_addr + 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 = ? + UNION ALL + SELECT 'email' AS typ, e.id AS sort_id, e.created_at, + COALESCE(e.email_date, e.created_at) AS datum, + CASE WHEN e.direction = 'out' THEN 'E-Mail gesendet' ELSE 'E-Mail erhalten' END AS status, + e.subject AS kommentar, e.bewerbung_id, + b.firma, b.stelle, + e.direction, e.subject, e.from_addr, e.to_addr + FROM emails e + LEFT JOIN bewerbungen b ON b.id = e.bewerbung_id AND b.user_id = e.user_id + WHERE e.user_id = ? + `; + + async function loadAktivitaet(U, { limit = null, offset = 0 } = {}) { + const params = [U, U]; + let sql = `SELECT * FROM (${AKTIVITAET_FEED_INNER}) AS feed ORDER BY created_at DESC, typ DESC, sort_id DESC`; + if (limit != null) { + sql += ' LIMIT ? OFFSET ?'; + params.push(limit, offset); + } + return dbAll(sql, params); + } + + // Gesamtzahl der Aktivitäten = Statusänderungen + E-Mails (für die Paginierung + // der /aktivitaeten-Seite). + async function countAktivitaet(U) { + const sv = await dbGet('SELECT COUNT(*) AS n FROM status_verlauf WHERE user_id = ?', [U]); + const em = await dbGet('SELECT COUNT(*) AS n FROM emails WHERE user_id = ?', [U]); + return (sv ? sv.n : 0) + (em ? em.n : 0); + } + // Routes app.get('/', async (req, res) => { try { @@ -1679,19 +1724,12 @@ 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 + // Letzte Aktivitäten für die Startseite: die jüngsten Ereignisse quer über + // alle Bewerbungen — Statusänderungen ebenso wie empfangene und gesendete + // E-Mails. 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]); + const aktivitaet = await loadAktivitaet(U, { limit: 8 }); // Get available months/years for filter const availableMonths = await dbAll(` @@ -1798,8 +1836,7 @@ initializeDatabase().then(async () => { 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 gesamt = await countAktivitaet(U); const seiten = Math.max(1, Math.ceil(gesamt / AKTIVITAETEN_PRO_SEITE)); // Eine Seite außerhalb des Bereichs (getippt oder nach dem Löschen von @@ -1807,15 +1844,10 @@ initializeDatabase().then(async () => { 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]); + const aktivitaet = await loadAktivitaet(U, { + limit: AKTIVITAETEN_PRO_SEITE, + offset: (seite - 1) * AKTIVITAETEN_PRO_SEITE + }); res.render('aktivitaeten', { aktivitaet, @@ -1934,16 +1966,9 @@ initializeDatabase().then(async () => { { label: 'Einstellung', count: ever('Einstellung') } ]; - // Recent activity: latest status changes with the application they belong to. - 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 - WHERE sv.user_id = ? - ORDER BY datetime(sv.created_at) DESC, sv.id DESC - LIMIT 12 - `, [U]); + // Recent activity: latest status changes and emails with the application + // they belong to. + const aktivitaet = await loadAktivitaet(U, { limit: 12 }); res.render('statistik', { total, diff --git a/views/aktivitaeten.ejs b/views/aktivitaeten.ejs index 84d8d92..4c64136 100644 --- a/views/aktivitaeten.ejs +++ b/views/aktivitaeten.ejs @@ -69,28 +69,39 @@ <% if (aktivitaet.length) { %> <% } else { %>
- Noch keine Statusänderungen protokolliert. + Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
<% } %> diff --git a/views/index.ejs b/views/index.ejs index d708070..d7d0511 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -348,27 +348,42 @@ <% if (aktivitaet && aktivitaet.length) { %>
<% } else { %>
- Noch keine Statusänderungen protokolliert. + Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
<% } %> diff --git a/views/statistik.ejs b/views/statistik.ejs index 9e349d9..76be2d3 100644 --- a/views/statistik.ejs +++ b/views/statistik.ejs @@ -309,7 +309,7 @@

Letzte Aktivität

- Die jüngsten Statusänderungen deiner Bewerbungen. + Die jüngsten Statusänderungen und E-Mails deiner Bewerbungen. Alle

<% 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'; %>
- <% aktivitaet.forEach(a => { %> -
+ <% 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'; } + %> +
<% if (a.bewerbung_id) { %> - <%= a.firma || '(ohne Firma)' %> - · <%= a.stelle || '' %> + <%= titel %> + <% if (titelSub) { %>· <%= titelSub %><% } %> <% } else { %> - Gelöschte Bewerbung + <%= titel %> <% } %>
- + <%= a.status %> - · <%= a.datum %> + <% if (a.datum) { %>· <%= a.datum %><% } %> <% if (a.kommentar) { %>· <%= a.kommentar %><% } %>
<% }); %>
- <% } else { %>
Noch keine Statusänderungen protokolliert.
<% } %> + <% } else { %>
Noch keine Aktivitäten — Statusänderungen und E-Mails erscheinen hier.
<% } %>
<% } %>