feat: Email-Eingangsverarbeitung für Destinatäre implementieren
Neues System zur automatischen Verarbeitung eingehender E-Mails von Destinatären. IMAP-Polling alle 15 Minuten via Celery Beat, automatische Zuordnung zu Destinatären anhand der E-Mail-Adresse, Upload von Anhängen zu Paperless-NGX. Umfasst: - DestinataerEmailEingang Model mit Status-Tracking - Celery Task für IMAP-Polling und Paperless-Integration - Web-UI (Liste + Detail) mit Such- und Filterfunktion - Admin-Interface mit Bulk-Actions - Agent-Dokumentation (SysAdmin, RentmeisterAI) - Dev-Environment Modernisierung (docker compose v2) Reviewed by: SysAdmin (STI-15), RentmeisterAI (STI-16) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
227
app/templates/stiftung/email_eingang/detail.html
Normal file
227
app/templates/stiftung/email_eingang/detail.html
Normal file
@@ -0,0 +1,227 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}E-Mail-Eingang Detail - van Hees-Theyssen-Vogel'sche Stiftung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">
|
||||
<i class="fas fa-envelope me-2"></i>E-Mail-Eingang
|
||||
</h1>
|
||||
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i>Zurück zur Übersicht
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Linke Spalte: E-Mail-Details -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="fas fa-envelope-open me-2"></i>E-Mail-Details</span>
|
||||
<span>
|
||||
{% if eingang.status == "neu" %}<span class="badge bg-warning text-dark">Neu</span>
|
||||
{% elif eingang.status == "zugewiesen" %}<span class="badge bg-primary">Zugewiesen</span>
|
||||
{% elif eingang.status == "verarbeitet" %}<span class="badge bg-success">Verarbeitet</span>
|
||||
{% elif eingang.status == "unbekannt" %}<span class="badge bg-danger">Unbekannter Absender</span>
|
||||
{% elif eingang.status == "fehler" %}<span class="badge bg-secondary">Fehler</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Eingangsdatum</dt>
|
||||
<dd class="col-sm-9">{{ eingang.eingangsdatum|date:"d.m.Y H:i" }} Uhr</dd>
|
||||
|
||||
<dt class="col-sm-3">Absender</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if eingang.absender_name %}{{ eingang.absender_name }} <{% endif %}
|
||||
<a href="mailto:{{ eingang.absender_email }}">{{ eingang.absender_email }}</a>
|
||||
{% if eingang.absender_name %}>{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Betreff</dt>
|
||||
<dd class="col-sm-9">{{ eingang.betreff|default:"(kein Betreff)" }}</dd>
|
||||
|
||||
<dt class="col-sm-3">Destinatär</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if eingang.destinataer %}
|
||||
<a href="{% url 'stiftung:destinataer_detail' eingang.destinataer.pk %}">
|
||||
{{ eingang.destinataer }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fas fa-exclamation-circle me-1"></i>Nicht zugeordnet</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
{% if eingang.quartalsnachweis %}
|
||||
<dt class="col-sm-3">Quartalsnachweis</dt>
|
||||
<dd class="col-sm-9">
|
||||
Q{{ eingang.quartalsnachweis.quartal }} / {{ eingang.quartalsnachweis.jahr }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
{% if eingang.email_text %}
|
||||
<hr>
|
||||
<h6 class="text-muted"><i class="fas fa-align-left me-1"></i>E-Mail-Text</h6>
|
||||
<div class="bg-light rounded p-3" style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; max-height: 400px; overflow-y: auto;">{{ eingang.email_text }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if eingang.fehler_details %}
|
||||
<hr>
|
||||
<div class="alert alert-danger">
|
||||
<strong><i class="fas fa-exclamation-triangle me-1"></i>Fehlerdetails:</strong>
|
||||
<pre class="mb-0 mt-1" style="font-size: 0.8rem;">{{ eingang.fehler_details }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anhänge / Paperless-Dokumente -->
|
||||
{% if dokument_links %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-paperclip me-2"></i>Anhänge in Paperless-NGX
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Kontext</th>
|
||||
<th>Paperless-ID</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for link in dokument_links %}
|
||||
<tr>
|
||||
<td>{{ link.titel }}</td>
|
||||
<td>{{ link.get_kontext_display }}</td>
|
||||
<td><code>{{ link.paperless_document_id }}</code></td>
|
||||
<td>
|
||||
<a href="{{ link.get_paperless_url }}" target="_blank" class="btn btn-sm btn-outline-info">
|
||||
<i class="fas fa-external-link-alt me-1"></i>Öffnen
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% elif eingang.paperless_dokument_ids %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
{{ eingang.paperless_dokument_ids|length }} Anhang/-hänge in Paperless hochgeladen
|
||||
(IDs: {{ eingang.paperless_dokument_ids|join:", " }}), aber noch kein DokumentLink erstellt.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body text-muted text-center py-3">
|
||||
<i class="fas fa-paperclip me-1"></i>Keine Anhänge in dieser E-Mail.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Aktionen -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<!-- Manuelle Destinatär-Zuordnung -->
|
||||
{% if not eingang.destinataer or eingang.status == "unbekannt" %}
|
||||
<div class="card mb-4 border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<i class="fas fa-user-plus me-2"></i>Destinatär manuell zuordnen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">
|
||||
Die E-Mail-Adresse <strong>{{ eingang.absender_email }}</strong>
|
||||
konnte keinem Destinatär automatisch zugeordnet werden.
|
||||
Bitte wählen Sie den passenden Destinatär aus.
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="assign_destinataer">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Destinatär</label>
|
||||
<select class="form-select" name="destinataer_id" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
{% for d in alle_destinataere %}
|
||||
<option value="{{ d.pk }}">{{ d.nachname }}, {{ d.vorname }}
|
||||
{% if d.email %} ({{ d.email }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-warning w-100">
|
||||
<i class="fas fa-link me-1"></i>Zuordnen & Speichern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Als verarbeitet markieren -->
|
||||
{% if eingang.status != "verarbeitet" %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-check-circle me-2"></i>Als verarbeitet markieren
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="mark_verarbeitet">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Interne Notiz (optional)</label>
|
||||
<textarea class="form-control" name="notizen" rows="3"
|
||||
placeholder="Z. B. 'Studiennachweis für WS 2025/26 eingegangen und geprüft.'">{{ eingang.notizen }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="fas fa-check me-1"></i>Verarbeitet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Notizen bearbeiten -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-sticky-note me-2"></i>Interne Notizen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="save_notizen">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" name="notizen" rows="5"
|
||||
placeholder="Interne Notizen zur E-Mail...">{{ eingang.notizen }}</textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-save me-1"></i>Notizen speichern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadaten -->
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="fas fa-info-circle me-2"></i>Metadaten</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 small">
|
||||
<dt class="col-6">Erfasst am</dt>
|
||||
<dd class="col-6">{{ eingang.created_at|date:"d.m.Y H:i" }}</dd>
|
||||
<dt class="col-6">Datensatz-ID</dt>
|
||||
<dd class="col-6 text-muted"><code>{{ eingang.pk|stringformat:"s"|slice:":8" }}…</code></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
229
app/templates/stiftung/email_eingang/list.html
Normal file
229
app/templates/stiftung/email_eingang/list.html
Normal file
@@ -0,0 +1,229 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}E-Mail-Eingang (Destinatäre) - van Hees-Theyssen-Vogel'sche Stiftung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">
|
||||
<i class="fas fa-envelope-open-text me-2"></i>E-Mail-Eingang (Destinatäre)
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<form method="post" action="{% url 'stiftung:email_eingang_poll_trigger' %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-sync-alt me-1"></i>Jetzt abrufen
|
||||
</button>
|
||||
</form>
|
||||
<a href="{% url 'stiftung:destinataer_list' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i>Destinatäre
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statuskarten -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-left-primary h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Gesamt</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.gesamt }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-envelope fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-left-warning h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Neu / Unbearbeitet</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.neu }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-exclamation-circle fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-left-danger h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Unbekannter Absender</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.unbekannt }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-user-times fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-left-secondary h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">Fehler</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ counts.fehler }}</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header"><i class="fas fa-filter me-2"></i>Filter</div>
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Suche</label>
|
||||
<input type="text" class="form-control" name="q" value="{{ search }}"
|
||||
placeholder="Absender, Betreff, Destinatär...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">Alle</option>
|
||||
{% for value, label in status_choices %}
|
||||
<option value="{{ value }}" {% if status_filter == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search me-1"></i>Filtern
|
||||
</button>
|
||||
</div>
|
||||
{% if search or status_filter %}
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-times me-1"></i>Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabelle -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span><i class="fas fa-inbox me-2"></i>Eingegangene E-Mails</span>
|
||||
<span class="text-muted small">{{ page_obj.paginator.count }} Einträge</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if page_obj %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Absender</th>
|
||||
<th>Destinatär</th>
|
||||
<th>Betreff</th>
|
||||
<th>Anhänge</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in page_obj %}
|
||||
<tr>
|
||||
<td class="text-nowrap">
|
||||
<small>{{ e.eingangsdatum|date:"d.m.Y H:i" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ e.absender_name|default:e.absender_email }}</div>
|
||||
{% if e.absender_name %}
|
||||
<small class="text-muted">{{ e.absender_email }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if e.destinataer %}
|
||||
<a href="{% url 'stiftung:destinataer_detail' e.destinataer.pk %}">
|
||||
{{ e.destinataer }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fas fa-question-circle me-1"></i>Unbekannt</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.betreff|truncatechars:60 }}</td>
|
||||
<td class="text-center">
|
||||
{% if e.paperless_dokument_ids %}
|
||||
<span class="badge bg-info">{{ e.paperless_dokument_ids|length }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if e.status == "neu" %}
|
||||
<span class="badge bg-warning text-dark">Neu</span>
|
||||
{% elif e.status == "zugewiesen" %}
|
||||
<span class="badge bg-primary">Zugewiesen</span>
|
||||
{% elif e.status == "verarbeitet" %}
|
||||
<span class="badge bg-success">Verarbeitet</span>
|
||||
{% elif e.status == "unbekannt" %}
|
||||
<span class="badge bg-danger">Unbekannt</span>
|
||||
{% elif e.status == "fehler" %}
|
||||
<span class="badge bg-secondary">Fehler</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'stiftung:email_eingang_detail' e.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav>
|
||||
<ul class="pagination mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&q={{ search }}&status={{ status_filter }}">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Seite {{ page_obj.number }} von {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}&q={{ search }}&status={{ status_filter }}">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||
<p>Keine E-Mails gefunden.</p>
|
||||
<small>Der automatische Abruf erfolgt alle 15 Minuten. Über den Button "Jetzt abrufen" kann der Vorgang manuell ausgelöst werden.</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user