diff --git a/server.js b/server.js index d8114df..a5ad17b 100644 --- a/server.js +++ b/server.js @@ -1446,6 +1446,10 @@ initializeDatabase().then(() => { atts.forEach((a) => { (byEmail[a.email_id] = byEmail[a.email_id] || []).push(a); }); emails.forEach((e) => { e.anhaenge = byEmail[e.id] || []; }); decorateEmails(emails); + // These are read now that they are shown — clears them from the bell. + // (The `seen` values above are captured pre-update, so the "Neu" badge + // still renders on this view.) + await dbRun("UPDATE emails SET seen = 1 WHERE bewerbung_id IS NULL AND direction = 'in' AND seen = 0"); } // Applications the user can assign an e-mail to (newest first). const bewerbungen = await dbAll( @@ -1502,7 +1506,7 @@ initializeDatabase().then(() => { } }); - // Count of unlinked e-mails — drives the header badge on every page. + // Count of unlinked e-mails — drives the "Postfach" header badge on every page. app.get('/api/emails/unassigned-count', async (req, res) => { try { const row = await dbGet('SELECT COUNT(*) as count FROM emails WHERE bewerbung_id IS NULL'); @@ -1512,6 +1516,54 @@ initializeDatabase().then(() => { } }); + // Unread received e-mails — drives the notification bell on every page. Returns + // the total unread count plus the newest few as a ready-to-render list. Replies + // auto-assigned to an application carry a link to that application; unassigned + // mail links to the Postfach. + app.get('/api/notifications', async (req, res) => { + try { + const cntRow = await dbGet( + "SELECT COUNT(*) AS count FROM emails WHERE direction = 'in' AND seen = 0" + ); + const rows = await dbAll( + `SELECT e.id, e.from_addr, e.subject, e.body_text, e.body_html, e.email_date, e.bewerbung_id, + b.firma, b.stelle + FROM emails e LEFT JOIN bewerbungen b ON b.id = e.bewerbung_id + WHERE e.direction = 'in' AND e.seen = 0 + ORDER BY datetime(e.email_date) DESC, e.id DESC + LIMIT 30` + ); + const items = rows.map((e) => { + const fromName = String(e.from_addr || '').replace(/<[^>]*>/, '').replace(/"/g, '').trim() + || String(e.from_addr || '').trim(); + const snippet = emailPlainText(e).replace(/\s+/g, ' ').trim().slice(0, 140); + return { + id: e.id, + from: fromName || '(unbekannt)', + subject: e.subject || '(kein Betreff)', + snippet, + date: e.email_date || null, + bewerbung_id: e.bewerbung_id || null, + kontext: e.bewerbung_id ? [e.firma, e.stelle].filter(Boolean).join(' · ') : '', + url: e.bewerbung_id ? ('/bewerbung/' + e.bewerbung_id + '#korrespondenz') : '/postfach', + }; + }); + res.json({ count: cntRow ? cntRow.count : 0, items }); + } catch (error) { + res.status(500).json({ count: 0, items: [] }); + } + }); + + // Mark every received e-mail as read (clears the notification bell). + app.post('/api/emails/mark-all-read', async (req, res) => { + try { + await dbRun("UPDATE emails SET seen = 1 WHERE direction = 'in' AND seen = 0"); + res.json({ ok: true }); + } catch (error) { + res.status(500).json({ ok: false }); + } + }); + // Add a timeline entry (status change with date + comment) app.post('/bewerbung/:id/verlauf', async (req, res) => { try { diff --git a/views/partials/header.ejs b/views/partials/header.ejs index 0ac991f..e5f2d52 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -9,6 +9,26 @@