Delete e-mails from the Postfach; searchable assignment picker
Each Postfach e-mail gets a Delete button (POST /postfach/:id/delete), which also removes stored attachment files. The plain assignment dropdown is replaced by a searchable combobox that filters applications by company, position and city, with keyboard navigation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1341,6 +1341,23 @@ initializeDatabase().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete an e-mail from the Postfach (removes its stored attachment files too).
|
||||
app.post('/postfach/:emailId/delete', async (req, res) => {
|
||||
try {
|
||||
const emailId = req.params.emailId;
|
||||
const atts = await dbAll('SELECT pfad FROM email_anhaenge WHERE email_id = ?', [emailId]);
|
||||
for (const a of atts) {
|
||||
fs.promises.unlink(path.join(emailAnhaengeDir, a.pfad)).catch(() => {});
|
||||
}
|
||||
await dbRun('DELETE FROM email_anhaenge WHERE email_id = ?', [emailId]);
|
||||
await dbRun('DELETE FROM emails WHERE id = ?', [emailId]);
|
||||
res.redirect('/postfach?mailok=' + encodeURIComponent('E-Mail wurde gelöscht.'));
|
||||
} catch (error) {
|
||||
console.error('Error deleting e-mail:', error);
|
||||
res.redirect('/postfach?mailerror=' + encodeURIComponent('Löschen 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 {
|
||||
|
||||
+105
-10
@@ -77,17 +77,16 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Assign to an existing application -->
|
||||
<form action="/postfach/<%= e.id %>/zuweisen" method="POST" class="mt-3 border-t border-gray-200 dark:border-gray-600 pt-3 flex flex-wrap items-end gap-2">
|
||||
<div class="flex-1 min-w-52">
|
||||
<!-- Assign to an existing application (searchable) + delete -->
|
||||
<div class="mt-3 border-t border-gray-200 dark:border-gray-600 pt-3 flex flex-wrap items-end gap-2">
|
||||
<form action="/postfach/<%= e.id %>/zuweisen" method="POST" class="flex-1 min-w-52 flex flex-wrap items-end gap-2" data-assign-form>
|
||||
<div class="flex-1 min-w-52 relative" data-combo>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Bewerbung zuweisen</label>
|
||||
<select name="bewerbung_id" required
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm">
|
||||
<option value="" disabled selected>— Bewerbung auswählen —</option>
|
||||
<% bewerbungen.forEach(function(b){ %>
|
||||
<option value="<%= b.id %>"><%= b.firma %><% if (b.stelle) { %> · <%= b.stelle %><% } %><% if (b.datum) { %> (<%= new Date(b.datum).toLocaleDateString('de-DE') %>)<% } %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
<input type="text" class="combo-input w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-800 dark:text-white text-sm"
|
||||
placeholder="Firma, Stelle oder Ort suchen…" autocomplete="off"
|
||||
role="combobox" aria-expanded="false" aria-autocomplete="list">
|
||||
<input type="hidden" name="bewerbung_id" class="combo-value">
|
||||
<ul class="combo-list hidden absolute z-20 left-0 right-0 mt-1 max-h-56 overflow-auto rounded-md border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 shadow-lg text-sm"></ul>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
|
||||
@@ -95,6 +94,15 @@
|
||||
Zuweisen
|
||||
</button>
|
||||
</form>
|
||||
<form action="/postfach/<%= e.id %>/delete" method="POST"
|
||||
onsubmit="return confirm('Diese E-Mail endgültig aus dem Postfach löschen?');">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
Löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
@@ -111,6 +119,9 @@
|
||||
|
||||
<%- include('partials/footer') %>
|
||||
|
||||
<script>
|
||||
window.__bewerbungen = <%- JSON.stringify((bewerbungen || []).map(function (b) { return { id: b.id, firma: b.firma, stelle: b.stelle, ort: b.ort, datum: b.datum }; })).replace(/</g, '\\u003c') %>;
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
const root = document.documentElement;
|
||||
@@ -147,6 +158,90 @@
|
||||
btn.textContent = expanded ? 'Vollständig anzeigen' : 'Weniger anzeigen';
|
||||
});
|
||||
});
|
||||
|
||||
// --- Searchable application picker (combobox) for assigning e-mails ---
|
||||
var apps = Array.isArray(window.__bewerbungen) ? window.__bewerbungen : [];
|
||||
function unesc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&');
|
||||
}
|
||||
function fmtDate(d) { if (!d) return ''; try { return new Date(d).toLocaleDateString('de-DE'); } catch (e) { return ''; } }
|
||||
apps.forEach(function (b) {
|
||||
b._label = unesc(b.firma) + (b.stelle ? ' · ' + unesc(b.stelle) : '');
|
||||
b._sub = [unesc(b.ort || ''), fmtDate(b.datum)].filter(Boolean).join(' · ');
|
||||
b._search = (b._label + ' ' + unesc(b.ort || '') + ' ' + fmtDate(b.datum)).toLowerCase();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-combo]').forEach(function (combo) {
|
||||
var input = combo.querySelector('.combo-input');
|
||||
var hidden = combo.querySelector('.combo-value');
|
||||
var list = combo.querySelector('.combo-list');
|
||||
var items = [], active = -1;
|
||||
|
||||
function open() { list.classList.remove('hidden'); input.setAttribute('aria-expanded', 'true'); }
|
||||
function close() { list.classList.add('hidden'); input.setAttribute('aria-expanded', 'false'); active = -1; }
|
||||
function choose(b) { hidden.value = b.id; input.value = b._label; close(); }
|
||||
function highlight() {
|
||||
Array.prototype.forEach.call(list.children, function (li, i) {
|
||||
li.classList.toggle('bg-blue-100', i === active);
|
||||
li.classList.toggle('dark:bg-gray-600', i === active);
|
||||
if (i === active && li.scrollIntoView) li.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
}
|
||||
function render(q) {
|
||||
var tokens = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
items = apps.filter(function (b) {
|
||||
return tokens.every(function (t) { return b._search.indexOf(t) > -1; });
|
||||
}).slice(0, 50);
|
||||
list.innerHTML = '';
|
||||
if (!items.length) {
|
||||
var empty = document.createElement('li');
|
||||
empty.className = 'px-3 py-2 text-gray-400';
|
||||
empty.textContent = apps.length ? 'Keine Treffer' : 'Noch keine Bewerbungen vorhanden';
|
||||
list.appendChild(empty);
|
||||
open(); return;
|
||||
}
|
||||
items.forEach(function (b) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'combo-item cursor-pointer px-3 py-2 hover:bg-blue-50 dark:hover:bg-gray-600';
|
||||
var top = document.createElement('div');
|
||||
top.className = 'font-medium text-gray-800 dark:text-gray-100';
|
||||
top.textContent = b._label;
|
||||
var sub = document.createElement('div');
|
||||
sub.className = 'text-xs text-gray-500 dark:text-gray-400';
|
||||
sub.textContent = b._sub;
|
||||
li.appendChild(top); li.appendChild(sub);
|
||||
li.addEventListener('mousedown', function (ev) { ev.preventDefault(); choose(b); });
|
||||
list.appendChild(li);
|
||||
});
|
||||
active = -1; open();
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () { hidden.value = ''; render(input.value); });
|
||||
input.addEventListener('focus', function () { render(input.value); });
|
||||
input.addEventListener('keydown', function (ev) {
|
||||
if (list.classList.contains('hidden')) return;
|
||||
if (ev.key === 'ArrowDown') { ev.preventDefault(); active = Math.min(active + 1, items.length - 1); highlight(); }
|
||||
else if (ev.key === 'ArrowUp') { ev.preventDefault(); active = Math.max(active - 1, 0); highlight(); }
|
||||
else if (ev.key === 'Enter') { if (active > -1 && items[active]) { ev.preventDefault(); choose(items[active]); } }
|
||||
else if (ev.key === 'Escape') { close(); }
|
||||
});
|
||||
document.addEventListener('click', function (ev) { if (!combo.contains(ev.target)) close(); });
|
||||
});
|
||||
|
||||
// Block assignment submits until an application has actually been picked.
|
||||
document.querySelectorAll('[data-assign-form]').forEach(function (f) {
|
||||
f.addEventListener('submit', function (ev) {
|
||||
var hidden = f.querySelector('.combo-value');
|
||||
if (!hidden || !hidden.value) {
|
||||
ev.preventDefault();
|
||||
var inp = f.querySelector('.combo-input');
|
||||
if (inp) inp.focus();
|
||||
alert('Bitte zuerst eine Bewerbung aus der Liste auswählen.');
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user