Add Postfach page to manually assign unlinked e-mails

Incoming e-mails that IMAP matching could not link to an application
(bewerbung_id IS NULL) were invisible. Add a dedicated page listing
them with a dropdown of existing applications to assign each one by
hand. A header badge on every page shows the count of unlinked e-mails
via a small /api/emails/unassigned-count endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-03 19:07:54 +02:00
co-authored by Claude
parent 931bb85c6a
commit 4418c15553
3 changed files with 226 additions and 0 deletions
+70
View File
@@ -1181,6 +1181,76 @@ initializeDatabase().then(() => {
}
});
// ---------------------------------------------------------------------------
// Postfach: incoming e-mails that landed in the inbox but could not be
// matched to an application automatically. The user assigns them by hand.
// ---------------------------------------------------------------------------
// Page listing all e-mails with no bewerbung_id, plus every application as
// assignment target.
app.get('/postfach', async (req, res) => {
try {
const emails = await dbAll(
`SELECT * FROM emails WHERE bewerbung_id IS NULL ORDER BY datetime(email_date) DESC, id DESC`
);
if (emails.length) {
const eIds = emails.map((e) => e.id);
const atts = await dbAll(
`SELECT id, email_id, name, mime FROM email_anhaenge WHERE email_id IN (${eIds.map(() => '?').join(',')})`,
eIds
);
const byEmail = {};
atts.forEach((a) => { (byEmail[a.email_id] = byEmail[a.email_id] || []).push(a); });
emails.forEach((e) => { e.anhaenge = byEmail[e.id] || []; });
}
// Applications the user can assign an e-mail to (newest first).
const bewerbungen = await dbAll(
`SELECT id, firma, stelle, ort, datum FROM bewerbungen ORDER BY datum DESC, created_at DESC`
);
res.render('postfach', {
emails,
bewerbungen,
mailConfigured: mailer.isConfigured(),
mailError: req.query.mailerror ? String(req.query.mailerror) : '',
mailOk: req.query.mailok ? String(req.query.mailok) : '',
hideSettings: true,
});
} catch (error) {
console.error('Error loading postfach:', error);
res.status(500).send('Serverfehler');
}
});
// Assign an unlinked e-mail to an existing application.
app.post('/postfach/:emailId/zuweisen', async (req, res) => {
try {
const bewerbungId = Number(req.body.bewerbung_id);
if (!bewerbungId) {
return res.redirect('/postfach?mailerror=' + encodeURIComponent('Bitte eine Bewerbung auswählen.'));
}
// Make sure the target application exists and the e-mail is still unlinked.
const app = await dbGet('SELECT id FROM bewerbungen WHERE id = ?', [bewerbungId]);
if (!app) {
return res.redirect('/postfach?mailerror=' + encodeURIComponent('Ausgewählte Bewerbung existiert nicht.'));
}
await dbRun('UPDATE emails SET bewerbung_id = ? WHERE id = ? AND bewerbung_id IS NULL', [bewerbungId, req.params.emailId]);
res.redirect('/postfach?mailok=' + encodeURIComponent('E-Mail wurde der Bewerbung zugewiesen.'));
} catch (error) {
console.error('Error assigning e-mail:', error);
res.redirect('/postfach?mailerror=' + encodeURIComponent('Zuweisung fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler')));
}
});
// Count of unlinked e-mails — drives the 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');
res.json({ count: row ? row.count : 0 });
} catch (error) {
res.status(500).json({ count: 0 });
}
});
// Add a timeline entry (status change with date + comment)
app.post('/bewerbung/:id/verlauf', async (req, res) => {
try {