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.
|
// Count of unlinked e-mails — drives the header badge on every page.
|
||||||
app.get('/api/emails/unassigned-count', async (req, res) => {
|
app.get('/api/emails/unassigned-count', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+105
-10
@@ -77,17 +77,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- Assign to an existing application -->
|
<!-- Assign to an existing application (searchable) + delete -->
|
||||||
<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="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">
|
<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>
|
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Bewerbung zuweisen</label>
|
||||||
<select name="bewerbung_id" required
|
<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"
|
||||||
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">
|
placeholder="Firma, Stelle oder Ort suchen…" autocomplete="off"
|
||||||
<option value="" disabled selected>— Bewerbung auswählen —</option>
|
role="combobox" aria-expanded="false" aria-autocomplete="list">
|
||||||
<% bewerbungen.forEach(function(b){ %>
|
<input type="hidden" name="bewerbung_id" class="combo-value">
|
||||||
<option value="<%= b.id %>"><%= b.firma %><% if (b.stelle) { %> · <%= b.stelle %><% } %><% if (b.datum) { %> (<%= new Date(b.datum).toLocaleDateString('de-DE') %>)<% } %></option>
|
<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>
|
||||||
<% }); %>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit"
|
<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">
|
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
|
Zuweisen
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -111,6 +119,9 @@
|
|||||||
|
|
||||||
<%- include('partials/footer') %>
|
<%- 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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@@ -147,6 +158,90 @@
|
|||||||
btn.textContent = expanded ? 'Vollständig anzeigen' : 'Weniger anzeigen';
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user