Add Vorlagen editor, upload portal, onboarding, and participant import command
- 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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
114
app/templates/stiftung/onboarding_einladung_liste.html
Normal file
114
app/templates/stiftung/onboarding_einladung_liste.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
311
app/templates/stiftung/vorlage_editor.html
Normal file
311
app/templates/stiftung/vorlage_editor.html
Normal 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> ·
|
||||
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 %}
|
||||
100
app/templates/stiftung/vorlagen_liste.html
Normal file
100
app/templates/stiftung/vorlagen_liste.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user