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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user