imap-mail-filter-service/app/templates/filters.html

284 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Filterregeln — IMAP Mail Filter{% endblock %}
{% block content %}
<h1>Filterregeln</h1>
<label for="account-select">Konto auswählen:</label>
<select id="account-select" onchange="loadFilters()">
<option value="">— Konto wählen —</option>
{% for acc in accounts %}
<option value="{{ acc.id }}" {{ 'selected' if selected_account_id == acc.id else '' }}>{{ acc.name }} ({{ acc.username }})</option>
{% endfor %}
</select>
<div id="filters-container">
<p>Bitte ein Konto auswählen.</p>
</div>
<dialog id="filter-dialog">
<article style="min-width: 60vw;">
<header>
<button aria-label="Close" rel="prev" onclick="document.getElementById('filter-dialog').close()"></button>
<h3 id="dialog-title">Neue Filterregel</h3>
</header>
<form id="filter-form">
<input type="hidden" id="filter-id" value="">
<label>
Name
<input type="text" id="filter-name" required placeholder="z.B. Newsletter sortieren">
</label>
<div class="grid">
<label>
Priorität
<input type="number" id="filter-priority" value="100">
</label>
<label>
Quellordner
<input type="text" id="filter-source-folder" value="INBOX">
</label>
</div>
<label>
<input type="checkbox" id="filter-stop-processing">
Nach Treffer keine weiteren Regeln anwenden
</label>
<h4>Bedingungen <small>(alle müssen zutreffen)</small></h4>
<div id="conditions-list"></div>
<button type="button" class="outline small" onclick="addCondition()">+ Bedingung</button>
<h4>Aktionen</h4>
<div id="actions-list"></div>
<button type="button" class="outline small" onclick="addAction()">+ Aktion</button>
<footer>
<div role="group">
<button type="submit">Speichern</button>
<button type="button" class="outline" onclick="document.getElementById('filter-dialog').close()">Abbrechen</button>
</div>
</footer>
</form>
</article>
</dialog>
{% endblock %}
{% block scripts %}
<script>
const FIELDS = [
{value: 'from', label: 'Von'},
{value: 'to', label: 'An'},
{value: 'subject', label: 'Betreff'},
{value: 'body', label: 'Text'},
{value: 'has_attachment', label: 'Hat Anhang'},
];
const MATCH_TYPES = [
{value: 'contains', label: 'enthält'},
{value: 'regex', label: 'Regex'},
{value: 'exact', label: 'exakt'},
];
const ACTION_TYPES = [
{value: 'move', label: 'Verschieben in Ordner', needsParam: true, paramLabel: 'Zielordner'},
{value: 'forward', label: 'Weiterleiten an', needsParam: true, paramLabel: 'E-Mail-Adresse'},
{value: 'delete', label: 'Löschen', needsParam: false},
{value: 'mark_read', label: 'Als gelesen markieren', needsParam: false},
];
async function loadFilters() {
const accountId = document.getElementById('account-select').value;
const container = document.getElementById('filters-container');
if (!accountId) {
container.innerHTML = '<p>Bitte ein Konto auswählen.</p>';
return;
}
const resp = await fetch(`/api/filters/account/${accountId}`);
const filters = await resp.json();
if (filters.length === 0) {
container.innerHTML = `
<p>Keine Filterregeln für dieses Konto.</p>
<button onclick="openNewFilter()">Neue Regel erstellen</button>
`;
return;
}
let html = '<button onclick="openNewFilter()" style="margin-bottom:1rem">Neue Regel</button><table><thead><tr><th>Prio</th><th>Name</th><th>Ordner</th><th>Bedingungen</th><th>Aktionen</th><th></th></tr></thead><tbody>';
for (const f of filters) {
const conds = f.conditions.map(c => {
const fieldLabel = FIELDS.find(x => x.value === c.field)?.label || c.field;
const matchLabel = MATCH_TYPES.find(x => x.value === c.match_type)?.label || c.match_type;
return `${c.negate ? 'NICHT ' : ''}${fieldLabel} ${matchLabel} "${c.value}"`;
}).join('<br>');
const acts = f.actions.map(a => {
const at = ACTION_TYPES.find(x => x.value === a.action_type);
return `${at?.label || a.action_type}${a.parameter ? ': ' + a.parameter : ''}`;
}).join('<br>');
html += `<tr>
<td>${f.priority}</td>
<td>${f.name}${f.stop_processing ? ' <small>(stop)</small>' : ''}</td>
<td>${f.source_folder}</td>
<td><small>${conds}</small></td>
<td><small>${acts}</small></td>
<td>
<div role="group">
<button class="outline small" onclick='editFilter(${JSON.stringify(f)})'>Bearbeiten</button>
<button class="outline small contrast" onclick="deleteFilter(${f.id})">Löschen</button>
</div>
</td>
</tr>`;
}
html += '</tbody></table>';
container.innerHTML = html;
}
function addCondition(data = null) {
const list = document.getElementById('conditions-list');
const idx = list.children.length;
const div = document.createElement('div');
div.className = 'grid';
div.style.marginBottom = '0.5rem';
div.innerHTML = `
<select name="cond_field_${idx}">
${FIELDS.map(f => `<option value="${f.value}" ${data?.field === f.value ? 'selected' : ''}>${f.label}</option>`).join('')}
</select>
<select name="cond_match_${idx}">
${MATCH_TYPES.map(m => `<option value="${m.value}" ${data?.match_type === m.value ? 'selected' : ''}>${m.label}</option>`).join('')}
</select>
<input type="text" name="cond_value_${idx}" value="${data?.value || ''}" placeholder="Wert" required>
<label style="white-space:nowrap"><input type="checkbox" name="cond_negate_${idx}" ${data?.negate ? 'checked' : ''}> NOT</label>
<button type="button" class="outline small contrast" onclick="this.parentElement.remove()">X</button>
`;
list.appendChild(div);
}
function addAction(data = null) {
const list = document.getElementById('actions-list');
const idx = list.children.length;
const div = document.createElement('div');
div.className = 'grid';
div.style.marginBottom = '0.5rem';
const selectedType = ACTION_TYPES.find(a => a.value === data?.action_type);
div.innerHTML = `
<select name="act_type_${idx}" onchange="toggleActionParam(this)">
${ACTION_TYPES.map(a => `<option value="${a.value}" data-needs-param="${a.needsParam}" data-param-label="${a.paramLabel || ''}" ${data?.action_type === a.value ? 'selected' : ''}>${a.label}</option>`).join('')}
</select>
<input type="text" name="act_param_${idx}" value="${data?.parameter || ''}" placeholder="${selectedType?.paramLabel || 'Parameter'}" ${selectedType?.needsParam === false && !data?.parameter ? 'style=display:none' : ''}>
<button type="button" class="outline small contrast" onclick="this.parentElement.remove()">X</button>
`;
list.appendChild(div);
}
function toggleActionParam(select) {
const opt = select.options[select.selectedIndex];
const input = select.parentElement.querySelector('input[type=text]');
if (opt.dataset.needsParam === 'true') {
input.style.display = '';
input.placeholder = opt.dataset.paramLabel;
} else {
input.style.display = 'none';
input.value = '';
}
}
function openNewFilter() {
document.getElementById('dialog-title').textContent = 'Neue Filterregel';
document.getElementById('filter-id').value = '';
document.getElementById('filter-name').value = '';
document.getElementById('filter-priority').value = '100';
document.getElementById('filter-source-folder').value = 'INBOX';
document.getElementById('filter-stop-processing').checked = false;
document.getElementById('conditions-list').innerHTML = '';
document.getElementById('actions-list').innerHTML = '';
addCondition();
addAction();
document.getElementById('filter-dialog').showModal();
}
function editFilter(f) {
document.getElementById('dialog-title').textContent = 'Regel bearbeiten';
document.getElementById('filter-id').value = f.id;
document.getElementById('filter-name').value = f.name;
document.getElementById('filter-priority').value = f.priority;
document.getElementById('filter-source-folder').value = f.source_folder;
document.getElementById('filter-stop-processing').checked = f.stop_processing;
document.getElementById('conditions-list').innerHTML = '';
document.getElementById('actions-list').innerHTML = '';
for (const c of f.conditions) addCondition(c);
for (const a of f.actions) addAction(a);
document.getElementById('filter-dialog').showModal();
}
function collectFormData() {
const accountId = document.getElementById('account-select').value;
const conditions = [];
const condRows = document.getElementById('conditions-list').children;
for (let i = 0; i < condRows.length; i++) {
const row = condRows[i];
const selects = row.querySelectorAll('select');
const input = row.querySelector('input[type=text]');
const checkbox = row.querySelector('input[type=checkbox]');
conditions.push({
field: selects[0].value,
match_type: selects[1].value,
value: input.value,
negate: checkbox.checked,
});
}
const actions = [];
const actRows = document.getElementById('actions-list').children;
for (let i = 0; i < actRows.length; i++) {
const row = actRows[i];
const select = row.querySelector('select');
const input = row.querySelector('input[type=text]');
actions.push({
action_type: select.value,
parameter: input.value || null,
});
}
return {
account_id: parseInt(accountId),
name: document.getElementById('filter-name').value,
priority: parseInt(document.getElementById('filter-priority').value),
source_folder: document.getElementById('filter-source-folder').value,
stop_processing: document.getElementById('filter-stop-processing').checked,
enabled: true,
conditions,
actions,
};
}
document.getElementById('filter-form').addEventListener('submit', async (e) => {
e.preventDefault();
const data = collectFormData();
const filterId = document.getElementById('filter-id').value;
let resp;
if (filterId) {
resp = await fetch(`/api/filters/${filterId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
} else {
resp = await fetch('/api/filters/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
}
if (resp.ok) {
document.getElementById('filter-dialog').close();
loadFilters();
} else {
const err = await resp.json();
alert('Fehler: ' + JSON.stringify(err));
}
});
async function deleteFilter(id) {
if (!confirm('Filterregel wirklich löschen?')) return;
await fetch(`/api/filters/${id}`, {method: 'DELETE'});
loadFilters();
}
// Auto-load if account is pre-selected
if (document.getElementById('account-select').value) loadFilters();
</script>
{% endblock %}