Add Vorlagen editor, upload portal, onboarding, and participant import command
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

- Dokument-Vorlagen-Editor: create/edit/reset document templates (admin)
- Upload-Portal: public portal for Nachweis uploads via token
- Onboarding: invite Destinatäre via email with multi-step wizard
- Bestätigungsschreiben: preview and send confirmation letters
- Email settings: SMTP configuration UI
- Management command: import_veranstaltung_teilnehmer for bulk participant import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-21 09:25:18 +00:00
parent fdf078fa10
commit aed540fe4b
51 changed files with 5335 additions and 33 deletions

View File

@@ -271,6 +271,12 @@
<span>Dokumentenverwaltung</span>
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{% url 'stiftung:vorlagen_liste' %}" class="btn btn-outline-primary w-100">
<i class="fas fa-file-code d-block mb-2 fa-2x"></i>
<span>Dokument-Vorlagen</span>
</a>
</div>
</div>
</div>
</div>

View File

@@ -63,6 +63,21 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'stiftung:destinataer_export' pk=destinataer.pk %}"><i class="fas fa-download me-2"></i>Export</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{% url 'stiftung:bestaetigung_vorschau' pk=destinataer.pk %}" target="_blank">
<i class="fas fa-file-pdf me-2"></i>Bestätigung (Vorschau)
</a>
</li>
<li>
<form method="post" action="{% url 'stiftung:bestaetigung_versenden' pk=destinataer.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="dropdown-item" onclick="return confirm('Bestätigungsschreiben per E-Mail an {{ destinataer.email|default:'(keine E-Mail)'}} senden?')">
<i class="fas fa-envelope me-2"></i>Bestätigung versenden
</button>
</form>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="post" action="{% url 'stiftung:destinataer_toggle_archiv' pk=destinataer.pk %}" class="d-inline">
{% csrf_token %}
@@ -493,6 +508,14 @@
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'stiftung:quarterly_confirmation_edit' nachweis.id %}" class="btn btn-outline-primary btn-sm" title="Bearbeiten"><i class="fas fa-edit"></i></a>
{% if nachweis.status == 'offen' or nachweis.status == 'teilweise' or nachweis.status == 'nachbesserung' %}
{% if destinataer.email %}
<form method="post" action="{% url 'stiftung:nachweis_aufforderung_senden' nachweis_pk=nachweis.id %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn btn-outline-info btn-sm" title="Upload-Aufforderung per E-Mail senden" onclick="return confirm('Upload-Link für {{ nachweis.jahr }} Q{{ nachweis.quartal }} an {{ destinataer.email }} senden?')"><i class="fas fa-paper-plane"></i></button>
</form>
{% endif %}
{% endif %}
{% if user.is_staff %}
{% if nachweis.status == 'eingereicht' %}
<button type="button" class="btn btn-outline-success btn-sm" onclick="approveQuarterly('{{ nachweis.id }}')" title="Freigeben"><i class="fas fa-check"></i></button>

View File

@@ -14,11 +14,21 @@
<div class="container-fluid">
<div class="row">
<div class="col-lg-8">
<div class="card">
{% if test_result %}
<div class="alert alert-{% if test_result.success %}success{% else %}danger{% endif %} alert-dismissible fade show">
<i class="fas fa-{% if test_result.success %}check-circle{% else %}exclamation-triangle{% endif %} me-1"></i>
{{ test_result.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<!-- IMAP Section -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">
<i class="fas fa-envelope"></i>
{{ title }}
<i class="fas fa-inbox"></i>
E-Mail Eingang (IMAP)
</h3>
<a href="{% url 'stiftung:administration' %}" class="btn btn-sm btn-secondary">
<i class="fas fa-arrow-left"></i> Zurück
@@ -26,14 +36,6 @@
</div>
<div class="card-body">
{% if test_result %}
<div class="alert alert-{% if test_result.success %}success{% else %}danger{% endif %} alert-dismissible fade show">
<i class="fas fa-{% if test_result.success %}check-circle{% else %}exclamation-triangle{% endif %} me-1"></i>
{{ test_result.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
<form method="post">
{% csrf_token %}
@@ -93,7 +95,7 @@
<i class="fas fa-save me-1"></i> Speichern
</button>
<button type="submit" name="action" value="test" class="btn btn-outline-primary">
<i class="fas fa-plug me-1"></i> Verbindung testen
<i class="fas fa-plug me-1"></i> IMAP testen
</button>
<a href="{% url 'stiftung:email_eingang_list' %}" class="btn btn-outline-secondary ms-auto">
<i class="fas fa-inbox me-1"></i> Zum Posteingang
@@ -102,13 +104,150 @@
</form>
</div>
</div>
<!-- SMTP Section -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title mb-0">
<i class="fas fa-paper-plane"></i>
E-Mail Ausgang (SMTP)
</h3>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for setting in smtp_settings %}
<div class="mb-3">
<label for="setting_{{ setting.key }}" class="form-label">
<strong>{{ setting.display_name }}</strong>
</label>
{% if setting.description %}
<div class="form-text mb-1">{{ setting.description }}</div>
{% endif %}
{% if setting.setting_type == 'boolean' %}
<div class="form-check form-switch">
<input type="checkbox"
class="form-check-input"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="True"
{% if setting.get_typed_value %}checked{% endif %}>
<label class="form-check-label" for="setting_{{ setting.key }}">Aktiviert</label>
</div>
{% elif setting.setting_type == 'password' %}
<div class="input-group">
<input type="password"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.value }}"
placeholder="{% if setting.value %}••••••••{% else %}Passwort eingeben{% endif %}">
<button type="button" class="btn btn-outline-secondary" onclick="togglePassword(this)" title="Passwort anzeigen">
<i class="fas fa-eye"></i>
</button>
</div>
{% elif setting.setting_type == 'number' %}
<input type="number"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.value }}">
{% else %}
<input type="text"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.value }}">
{% endif %}
</div>
{% endfor %}
<hr>
<div class="d-flex gap-2">
<button type="submit" name="action" value="save" class="btn btn-primary">
<i class="fas fa-save me-1"></i> Speichern
</button>
<button type="submit" name="action" value="test_smtp" class="btn btn-outline-primary">
<i class="fas fa-plug me-1"></i> Verbindung testen
</button>
</div>
<hr>
<div class="mb-3">
<label for="test_email" class="form-label">
<strong>Test-E-Mail senden</strong>
</label>
<div class="form-text mb-1">Sendet eine echte Test-E-Mail an die angegebene Adresse, um den vollständigen Versandweg zu prüfen.</div>
<div class="input-group">
<input type="email"
class="form-control"
id="test_email"
name="test_email"
placeholder="empfaenger@example.de"
value="{{ request.POST.test_email|default:'' }}">
<button type="submit" name="action" value="test_smtp_send" class="btn btn-outline-success">
<i class="fas fa-paper-plane me-1"></i> Test senden
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Notification Section -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title mb-0">
<i class="fas fa-bell"></i>
Benachrichtigungen
</h3>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{% for setting in notification_settings %}
<div class="mb-3">
<label for="setting_{{ setting.key }}" class="form-label">
<strong>{{ setting.display_name }}</strong>
</label>
{% if setting.description %}
<div class="form-text mb-1">{{ setting.description }}</div>
{% endif %}
<input type="text"
class="form-control"
id="setting_{{ setting.key }}"
name="setting_{{ setting.key }}"
value="{{ setting.value }}"
placeholder="z.B. vorstand@vhtv-stiftung.de">
</div>
{% endfor %}
<hr>
<div class="d-flex gap-2">
<button type="submit" name="action" value="save" class="btn btn-primary">
<i class="fas fa-save me-1"></i> Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Info sidebar -->
<div class="col-lg-4">
<div class="card">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-info-circle"></i> Hinweise
<i class="fas fa-info-circle"></i> IMAP-Hinweise
</div>
<div class="card-body" style="font-size: 0.85rem;">
<p>Konfigurieren Sie hier die IMAP-Verbindung zum E-Mail-Server. Eingehende E-Mails werden automatisch alle <strong>15 Minuten</strong> abgerufen und den Destinatären zugeordnet.</p>
@@ -122,6 +261,23 @@
<p class="mb-0"><i class="fas fa-shield-alt text-success me-1"></i> Das Passwort wird in der Datenbank gespeichert. Umgebungsvariablen (<code>IMAP_HOST</code>, etc.) werden als Fallback verwendet, wenn hier keine Werte gesetzt sind.</p>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-info-circle"></i> SMTP-Hinweise
</div>
<div class="card-body" style="font-size: 0.85rem;">
<p>SMTP wird für den <strong>ausgehenden</strong> E-Mail-Versand verwendet (Nachweis-Aufforderungen, Erinnerungen, Onboarding-Einladungen).</p>
<hr>
<p class="mb-1"><strong>IONOS-Einstellungen:</strong></p>
<ul class="mb-0" style="font-size: 0.8rem;">
<li>Server: <code>smtp.ionos.de</code></li>
<li>SSL/TLS: Port <code>465</code></li>
<li>STARTTLS: Port <code>587</code></li>
</ul>
<hr>
<p class="mb-0"><i class="fas fa-envelope text-primary me-1"></i> Die Absenderadresse <code>buero@vhtv-stiftung.de</code> muss mit dem SMTP-Benutzernamen übereinstimmen.</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -13,6 +13,13 @@
Nachweis-Board {{ jahr_filter }}
</h1>
<div class="d-flex gap-2">
<form method="post" action="{% url 'stiftung:batch_nachweis_aufforderung_senden' %}">
{% csrf_token %}
<input type="hidden" name="jahr" value="{{ jahr_filter }}">
<button type="submit" class="btn btn-primary" onclick="return confirm('Nachweis-Aufforderungs-E-Mails für alle offenen Nachweise {{ jahr_filter }} versenden?')">
<i class="fas fa-paper-plane me-2"></i>Aufforderungen senden
</button>
</form>
{% if overdue_count > 0 %}
<form method="post" action="{% url 'stiftung:batch_erinnerung_senden' %}">
{% csrf_token %}
@@ -126,6 +133,14 @@
{{ nachweis.get_completion_percentage }}%
</a>
</div>
{% if nachweis.status == 'offen' or nachweis.status == 'teilweise' or nachweis.status == 'nachbesserung' %}{% if row.destinataer.email %}
<form method="post" action="{% url 'stiftung:nachweis_aufforderung_senden' nachweis_pk=nachweis.pk %}" class="mt-1">
{% csrf_token %}
<button type="submit" class="btn btn-outline-primary btn-xs" style="font-size:0.65rem;padding:1px 5px;" title="Upload-Link per E-Mail senden" onclick="return confirm('Upload-Aufforderung an {{ row.destinataer.email }} senden?')">
<i class="fas fa-paper-plane"></i>
</button>
</form>
{% endif %}{% endif %}
{% else %}
<span class="text-muted small"></span>
{% endif %}

View File

@@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block title %}Onboarding-Einladungen{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Onboarding-Einladungen</h1>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#einladungModal">
+ Neue Einladung
</button>
</div>
<!-- Neue Einladung Modal -->
<div class="modal fade" id="einladungModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="{% url 'stiftung:onboarding_einladung_senden' %}">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">Neue Onboarding-Einladung senden</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="email" class="form-label">E-Mail-Adresse <span class="text-danger">*</span></label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="row g-2">
<div class="col-6">
<label for="vorname" class="form-label">Vorname (optional)</label>
<input type="text" class="form-control" id="vorname" name="vorname">
</div>
<div class="col-6">
<label for="nachname" class="form-label">Nachname (optional)</label>
<input type="text" class="form-control" id="nachname" name="nachname">
</div>
</div>
<div class="alert alert-info mt-3 mb-0 small">
Der eingeladene Kandidat erhält einen Einmal-Link per E-Mail (gültig 30 Tage), über den er das mehrstufige Onboarding-Formular ausfüllen kann.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Einladung senden</button>
</div>
</form>
</div>
</div>
</div>
<!-- Tabelle -->
<div class="card">
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>E-Mail</th>
<th>Name</th>
<th>Status</th>
<th>Gültig bis</th>
<th>Eingeladen von</th>
<th>Abgeschlossen</th>
<th>Destinatär</th>
<th></th>
</tr>
</thead>
<tbody>
{% for e in einladungen %}
<tr>
<td>{{ e.email }}</td>
<td>{{ e.vorname }} {{ e.nachname }}</td>
<td>
{% if e.status == "offen" %}
<span class="badge bg-success">Offen</span>
{% elif e.status == "abgeschlossen" %}
<span class="badge bg-primary">Abgeschlossen</span>
{% elif e.status == "abgelaufen" %}
<span class="badge bg-secondary">Abgelaufen</span>
{% elif e.status == "widerrufen" %}
<span class="badge bg-danger">Widerrufen</span>
{% endif %}
</td>
<td>{{ e.gueltig_bis|date:"d.m.Y" }}</td>
<td>{{ e.eingeladen_von.get_full_name|default:e.eingeladen_von.username|default:"" }}</td>
<td>{{ e.abgeschlossen_am|date:"d.m.Y H:i"|default:"" }}</td>
<td>
{% if e.destinataer %}
<a href="{% url 'stiftung:destinataer_detail' pk=e.destinataer.id %}">
{{ e.destinataer.vorname }} {{ e.destinataer.nachname }}
{% if not e.destinataer.unterstuetzung_bestaetigt %}
<span class="badge bg-warning text-dark ms-1">Freigabe ausstehend</span>
{% endif %}
</a>
{% else %}{% endif %}
</td>
<td>
{% if e.status == "offen" %}
<form method="post" action="{% url 'stiftung:onboarding_einladung_widerrufen' pk=e.id %}"
onsubmit="return confirm('Einladung für {{ e.email }} widerrufen?');">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger">Widerrufen</button>
</form>
{% endif %}
</td>
</tr>
{% empty %}
<tr><td colspan="8" class="text-center text-muted py-4">Keine Onboarding-Einladungen vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}Einladung widerrufen{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center py-4">
<h4>Einladung widerrufen?</h4>
<p class="text-muted">Die Onboarding-Einladung für <strong>{{ einladung.email }}</strong> wird widerrufen. Der Link wird ungültig.</p>
<form method="post">
{% csrf_token %}
<a href="{% url 'stiftung:onboarding_einladung_liste' %}" class="btn btn-secondary me-2">Abbrechen</a>
<button type="submit" class="btn btn-danger">Ja, widerrufen</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,311 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ vorlage.bezeichnung }} Vorlage bearbeiten{% endblock %}
{% block extra_css %}
<!-- Summernote WYSIWYG (lokal) -->
<link rel="stylesheet" href="{% static 'stiftung/vendor/summernote/summernote-bs5.min.css' %}">
<style>
.preview-frame {
width: 100%;
height: 580px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #fff;
}
.var-list {
max-height: 400px;
overflow-y: auto;
}
.var-item {
cursor: pointer;
font-family: monospace;
font-size: 12px;
}
.var-item:hover {
background-color: #e9ecef;
}
.note-editor.note-frame {
border-radius: 4px;
}
.code-editor-textarea {
width: 100%;
height: 520px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px;
line-height: 1.5;
padding: 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
resize: vertical;
tab-size: 4;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
background: #f8f9fa;
}
.code-editor-textarea:focus {
outline: none;
border-color: #86b7fe;
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
}
</style>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-sm-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-file-code me-2"></i>{{ vorlage.bezeichnung }}
</h1>
<small class="text-muted"><code>{{ vorlage.schluessel }}</code> &nbsp;·&nbsp;
Kategorie: {{ vorlage.get_kategorie_display }}</small>
</div>
<div class="d-flex gap-2">
<a href="{% url 'stiftung:vorlagen_liste' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Zurück zur Liste
</a>
{% if hat_original %}
<form method="post" action="{% url 'stiftung:vorlage_zuruecksetzen' pk=vorlage.pk %}"
onsubmit="return confirm('Alle Änderungen verwerfen und auf Original zurücksetzen?')">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-warning">
<i class="fas fa-undo me-1"></i>Original wiederherstellen
</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<!-- Editor (links) -->
<div class="col-lg-8">
<form method="post" id="editor-form">
{% csrf_token %}
<div class="d-flex gap-2 mb-2 justify-content-end">
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-preview">
<i class="fas fa-eye me-1"></i>Vorschau
</button>
<button type="submit" class="btn btn-sm btn-primary">
<i class="fas fa-save me-1"></i>Speichern
</button>
</div>
<textarea name="html_inhalt" id="code-editor"{% if use_code_editor %} class="code-editor-textarea"{% endif %}>{{ vorlage.html_inhalt }}</textarea>
<script type="application/json" id="vorlage-html-inhalt">{{ html_inhalt_json }}</script>
</form>
<!-- Vorschau-Bereich (initial versteckt) -->
<div id="preview-area" class="mt-3" style="display:none">
<div class="d-flex align-items-center justify-content-between mb-2">
<strong>Vorschau <span class="badge bg-secondary">Beispieldaten</span></strong>
<button type="button" class="btn btn-sm btn-outline-secondary" id="btn-close-preview">
<i class="fas fa-times me-1"></i>Vorschau schließen
</button>
</div>
<iframe id="preview-frame" class="preview-frame" title="Vorschau"></iframe>
</div>
</div>
<!-- Seitenleiste (rechts) -->
<div class="col-lg-4">
{% if variablen %}
<div class="card shadow mb-3">
<div class="card-header py-2">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-code me-1"></i>Verfügbare Variablen
</h6>
<small class="text-muted">Klick zum Einfügen</small>
</div>
<div class="card-body p-0">
<div class="var-list">
<table class="table table-sm mb-0">
<tbody>
{% for var, beschreibung in variablen.items %}
<tr class="var-item" data-var="{{ var }}">
<td class="ps-3 py-1">
<code>{% templatetag openvariable %} {{ var }} {% templatetag closevariable %}</code>
</td>
<td class="pe-3 py-1 text-muted small">{{ beschreibung }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="card shadow">
<div class="card-header py-2">
<h6 class="m-0 font-weight-bold text-secondary">
<i class="fas fa-info-circle me-1"></i>Info
</h6>
</div>
<div class="card-body small">
<dl class="mb-0">
<dt>Zuletzt bearbeitet</dt>
<dd class="text-muted">{{ vorlage.zuletzt_bearbeitet_am|date:"d.m.Y H:i" }}</dd>
{% if vorlage.zuletzt_bearbeitet_von %}
<dt>Bearbeitet von</dt>
<dd class="text-muted">{{ vorlage.zuletzt_bearbeitet_von.get_full_name|default:vorlage.zuletzt_bearbeitet_von.username }}</dd>
{% endif %}
<dt>Erstellt am</dt>
<dd class="text-muted mb-0">{{ vorlage.erstellt_am|date:"d.m.Y" }}</dd>
</dl>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<!-- jQuery (lokal) -->
<script src="{% static 'stiftung/vendor/jquery/jquery.min.js' %}"></script>
<!-- Summernote WYSIWYG (lokal) -->
<script src="{% static 'stiftung/vendor/summernote/summernote-bs5.min.js' %}"></script>
<script src="{% static 'stiftung/vendor/summernote/summernote-de-DE.min.js' %}"></script>
<script>
(function() {
var initialContent;
try {
initialContent = JSON.parse(document.getElementById('vorlage-html-inhalt').textContent);
} catch(e) {
initialContent = null;
}
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
var editor = document.getElementById('code-editor');
// Code-Editor-Modus: Plain textarea fuer vollstaendige HTML-Dokumente
// (Serienbrief-Vorlagen mit DOCTYPE, Template-Tags usw.)
if (useCodeEditor) {
if (initialContent) {
editor.value = initialContent;
}
// Tab-Taste einfügen statt Fokus wechseln
editor.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
});
// Variablen einfügen bei Klick
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
var start = editor.selectionStart;
editor.value = editor.value.substring(0, start) + placeholder + editor.value.substring(editor.selectionEnd);
editor.selectionStart = editor.selectionEnd = start + placeholder.length;
editor.focus();
});
});
// Vorschau
var previewArea = document.getElementById('preview-area');
var previewFrame = document.getElementById('preview-frame');
document.getElementById('btn-preview').addEventListener('click', function() {
var formData = new FormData();
formData.append('html_inhalt', editor.value);
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', { method: 'POST', body: formData })
.then(function(r) { return r.text(); })
.then(function(html) {
previewFrame.srcdoc = html;
previewArea.style.display = 'block';
previewArea.scrollIntoView({behavior: 'smooth'});
})
.catch(function(err) { alert('Vorschau fehlgeschlagen: ' + err); });
});
document.getElementById('btn-close-preview').addEventListener('click', function() {
previewArea.style.display = 'none';
});
return; // Skip Summernote initialization
}
if (typeof $ === 'undefined' || typeof $.fn.summernote === 'undefined') {
// Fallback: Summernote nicht geladen — Textarea sichtbar lassen
if (editor) { editor.style.height = '520px'; editor.style.fontFamily = 'monospace'; editor.style.fontSize = '13px'; }
return;
}
// Summernote initialisieren (für HTML-Fragment-Vorlagen: E-Mail, PDF-Fragmente)
$('#code-editor').summernote({
lang: 'de-DE',
height: 520,
toolbar: [
['style', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
['para', ['style', 'ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'hr']],
['view', ['fullscreen', 'codeview', 'undo', 'redo']],
],
callbacks: {
onInit: function() {
if (initialContent) {
$('#code-editor').summernote('code', initialContent);
}
}
}
});
// Variablen einfügen bei Klick
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
const varName = this.getAttribute('data-var');
const placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
$('#code-editor').summernote('focus');
$('#code-editor').summernote('insertText', placeholder);
});
});
// Vorschau
const previewArea = document.getElementById('preview-area');
const previewFrame = document.getElementById('preview-frame');
const btnPreview = document.getElementById('btn-preview');
const btnClosePreview = document.getElementById('btn-close-preview');
btnPreview.addEventListener('click', function() {
const content = $('#code-editor').summernote('code');
const formData = new FormData();
formData.append('html_inhalt', content);
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
fetch('{% url "stiftung:vorlage_vorschau" pk=vorlage.pk %}', {
method: 'POST',
body: formData,
})
.then(r => r.text())
.then(html => {
previewFrame.srcdoc = html;
previewArea.style.display = 'block';
previewArea.scrollIntoView({behavior: 'smooth'});
})
.catch(err => alert('Vorschau fehlgeschlagen: ' + err));
});
btnClosePreview.addEventListener('click', function() {
previewArea.style.display = 'none';
});
// Formular-Submit: Summernote-Inhalt in Textarea schreiben
document.getElementById('editor-form').addEventListener('submit', function() {
// Summernote schreibt den Inhalt automatisch in die Textarea beim Submit
// Sicherheitshalber explizit synchronisieren:
const content = $('#code-editor').summernote('code');
document.querySelector('textarea[name=html_inhalt]').value = content;
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,100 @@
{% extends 'base.html' %}
{% block title %}Dokument-Vorlagen Administration{% 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-file-code me-2"></i>Dokument-Vorlagen
</h1>
<div class="d-flex gap-2">
<form method="post" action="{% url 'stiftung:vorlagen_alle_zuruecksetzen' %}"
onsubmit="return confirm('ALLE Vorlagen auf die Original-Dateien zurücksetzen? Individuelle Änderungen gehen verloren.')">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-warning">
<i class="fas fa-undo me-1"></i>Alle zurücksetzen
</button>
</form>
<a href="{% url 'stiftung:administration' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Administration
</a>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Hier können Sie alle Vorlagen für generierte Dokumente (PDF-Briefe, E-Mails) direkt bearbeiten.
Änderungen werden sofort aktiv. Mit „Zurücksetzen" können Sie jederzeit auf die Original-Datei-Vorlage zurückkehren.
</div>
</div>
</div>
{% for kategorie, vlist in kategorien.items %}
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
{% if kategorie == 'pdf' %}<i class="fas fa-file-pdf me-2"></i>PDF-Dokumente
{% elif kategorie == 'email' %}<i class="fas fa-envelope me-2"></i>E-Mail-Vorlagen
{% elif kategorie == 'bericht' %}<i class="fas fa-chart-bar me-2"></i>Berichte
{% elif kategorie == 'serienbrief' %}<i class="fas fa-mail-bulk me-2"></i>Serienbriefe
{% else %}{{ kategorie_labels|dictsort:kategorie }}
{% endif %}
<span class="badge bg-secondary ms-2">{{ vlist|length }}</span>
</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Bezeichnung</th>
<th>Schlüssel</th>
<th>Zuletzt bearbeitet</th>
<th>Bearbeitet von</th>
<th></th>
</tr>
</thead>
<tbody>
{% for vorlage in vlist %}
<tr>
<td>
<strong>{{ vorlage.bezeichnung }}</strong>
</td>
<td>
<code class="small">{{ vorlage.schluessel }}</code>
</td>
<td class="text-muted small">
{{ vorlage.zuletzt_bearbeitet_am|date:"d.m.Y H:i" }}
</td>
<td class="text-muted small">
{% if vorlage.zuletzt_bearbeitet_von %}
{{ vorlage.zuletzt_bearbeitet_von.get_full_name|default:vorlage.zuletzt_bearbeitet_von.username }}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="text-end">
<a href="{% url 'stiftung:vorlage_editor' pk=vorlage.pk %}"
class="btn btn-sm btn-primary">
<i class="fas fa-edit me-1"></i>Bearbeiten
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% empty %}
<div class="alert alert-warning">
Keine Vorlagen gefunden. Bitte führen Sie die Datenbank-Migration aus.
</div>
{% endfor %}
{% endblock %}