feat: Email-Eingangsverarbeitung für Destinatäre implementieren
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled

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:
Stiftung CEO Agent
2026-03-09 21:11:22 +00:00
parent 6c8ddbb4f0
commit 4b21f553c3
16 changed files with 1554 additions and 49 deletions

View 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 }}">
&laquo;
</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 }}">
&raquo;
</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 %}