Baseline für Vision 2026: Veranstaltungsmodul + ausstehende Änderungen
Alle bestehenden, nicht commiteten Änderungen als Ausgangsbasis für den vision-2026 Branch übernommen (Veranstaltungsmodul, Serienbrief, etc.). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -177,7 +177,7 @@ if not DEBUG:
|
||||
|
||||
# django-otp settings
|
||||
OTP_TOTP_ISSUER = 'Stiftung Management System'
|
||||
OTP_LOGIN_URL = '/two-factor/login/'
|
||||
OTP_LOGIN_URL = '/auth/2fa/verify/'
|
||||
|
||||
# Optional: Hide sensitive data in admin when not verified
|
||||
OTP_ADMIN_HIDE_SENSITIVE_DATA = True
|
||||
|
||||
250
app/static/stiftung/js/briefvorlage_editor.js
Normal file
250
app/static/stiftung/js/briefvorlage_editor.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Briefvorlage-Editor: Minimal-WYSIWYG + Vorschau-Panel + Vorlagen-Loader
|
||||
* für Django Admin – keine externen Abhängigkeiten.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Warte auf DOM
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var textareas = document.querySelectorAll("textarea.briefvorlage-textarea");
|
||||
textareas.forEach(function (textarea) {
|
||||
initEditor(textarea);
|
||||
});
|
||||
});
|
||||
|
||||
function initEditor(textarea) {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.style.cssText = "border:1px solid #ccc;border-radius:4px;overflow:hidden;margin-top:4px;";
|
||||
|
||||
// ---- Toolbar ----
|
||||
var toolbar = document.createElement("div");
|
||||
toolbar.style.cssText = "background:#f5f5f5;border-bottom:1px solid #ccc;padding:5px 8px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;";
|
||||
|
||||
var buttons = [
|
||||
{ label: "B", cmd: "bold", title: "Fett (Strg+B)", style: "font-weight:bold;" },
|
||||
{ label: "I", cmd: "italic", title: "Kursiv (Strg+I)", style: "font-style:italic;" },
|
||||
{ label: "U", cmd: "underline", title: "Unterstrichen (Strg+U)", style: "text-decoration:underline;" },
|
||||
{ label: "¶", cmd: "insertParagraph", title: "Absatz einfügen" },
|
||||
{ label: "• Liste", cmd: "insertUnorderedList", title: "Aufzählung" },
|
||||
{ label: "1. Liste", cmd: "insertOrderedList", title: "Nummerierte Liste" },
|
||||
];
|
||||
|
||||
buttons.forEach(function (b) {
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.title = b.title;
|
||||
btn.innerHTML = b.label;
|
||||
btn.style.cssText = "padding:3px 8px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;font-size:13px;" + (b.style || "");
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
editor.focus();
|
||||
document.execCommand(b.cmd, false, null);
|
||||
syncToTextarea();
|
||||
});
|
||||
toolbar.appendChild(btn);
|
||||
});
|
||||
|
||||
// Trennlinie
|
||||
var sep = document.createElement("span");
|
||||
sep.style.cssText = "border-left:1px solid #ccc;height:20px;margin:0 4px;";
|
||||
toolbar.appendChild(sep);
|
||||
|
||||
// Tab-Buttons: Editor / HTML / Vorschau
|
||||
var tabEditor = createTabBtn("Editor", true);
|
||||
var tabHtml = createTabBtn("HTML", false);
|
||||
var tabVorschau = createTabBtn("Vorschau", false);
|
||||
toolbar.appendChild(tabEditor);
|
||||
toolbar.appendChild(tabHtml);
|
||||
toolbar.appendChild(tabVorschau);
|
||||
|
||||
// Vorlage-Loader (nur wenn BriefVorlage-API verfügbar)
|
||||
var sep2 = document.createElement("span");
|
||||
sep2.style.cssText = "border-left:1px solid #ccc;height:20px;margin:0 4px;";
|
||||
toolbar.appendChild(sep2);
|
||||
|
||||
var vorlagenSelect = document.createElement("select");
|
||||
vorlagenSelect.style.cssText = "font-size:12px;padding:2px 6px;border:1px solid #ccc;border-radius:3px;max-width:200px;";
|
||||
var defaultOption = document.createElement("option");
|
||||
defaultOption.value = "";
|
||||
defaultOption.textContent = "– Vorlage laden –";
|
||||
vorlagenSelect.appendChild(defaultOption);
|
||||
toolbar.appendChild(vorlagenSelect);
|
||||
|
||||
// Vorlagen asynchron laden
|
||||
loadVorlagen(vorlagenSelect);
|
||||
|
||||
var ladeBtn = document.createElement("button");
|
||||
ladeBtn.type = "button";
|
||||
ladeBtn.textContent = "Laden";
|
||||
ladeBtn.title = "Ausgewählte Vorlage in den Editor laden";
|
||||
ladeBtn.style.cssText = "padding:3px 8px;cursor:pointer;border:1px solid #0d6efd;border-radius:3px;background:#0d6efd;color:#fff;font-size:12px;";
|
||||
ladeBtn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
var val = vorlagenSelect.value;
|
||||
if (!val) return;
|
||||
var opt = vorlagenSelect.querySelector("option[value='" + val + "']");
|
||||
if (!opt) return;
|
||||
var html = opt.dataset.briefvorlage || "";
|
||||
var betreff = opt.dataset.betreff || "";
|
||||
if (confirm("Vorlage \"" + opt.textContent + "\" laden?\nDer aktuelle Brieftext wird überschrieben.")) {
|
||||
editor.innerHTML = html;
|
||||
textarea.value = html;
|
||||
// Betreff-Feld befüllen falls vorhanden und nicht leer
|
||||
if (betreff) {
|
||||
var betreffField = document.getElementById("id_betreff");
|
||||
if (betreffField && !betreffField.value) {
|
||||
betreffField.value = betreff;
|
||||
}
|
||||
}
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
toolbar.appendChild(ladeBtn);
|
||||
|
||||
// ---- Editor-Div (WYSIWYG) ----
|
||||
var editor = document.createElement("div");
|
||||
editor.contentEditable = "true";
|
||||
editor.style.cssText = "min-height:300px;padding:12px;font-family:Times New Roman,serif;font-size:11pt;line-height:1.4;outline:none;background:#fff;";
|
||||
editor.innerHTML = textarea.value;
|
||||
editor.addEventListener("input", syncToTextarea);
|
||||
editor.addEventListener("keyup", syncToTextarea);
|
||||
|
||||
// ---- HTML-Textarea (Quelltext) ----
|
||||
textarea.style.cssText += "display:none;width:100%;box-sizing:border-box;border:none;padding:12px;font-family:monospace;font-size:13px;";
|
||||
textarea.addEventListener("input", function () {
|
||||
editor.innerHTML = textarea.value;
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// ---- Vorschau-Panel ----
|
||||
var preview = document.createElement("div");
|
||||
preview.style.cssText = "display:none;min-height:300px;padding:12px;background:#fff;font-family:'Times New Roman',serif;font-size:11pt;line-height:1.4;";
|
||||
|
||||
// Tab-Logik
|
||||
function showTab(which) {
|
||||
editor.style.display = "none";
|
||||
textarea.style.display = "none";
|
||||
preview.style.display = "none";
|
||||
tabEditor.style.background = "#f5f5f5";
|
||||
tabHtml.style.background = "#f5f5f5";
|
||||
tabVorschau.style.background = "#f5f5f5";
|
||||
if (which === "editor") {
|
||||
editor.style.display = "block";
|
||||
tabEditor.style.background = "#fff";
|
||||
tabEditor.style.fontWeight = "bold";
|
||||
} else if (which === "html") {
|
||||
textarea.style.display = "block";
|
||||
tabHtml.style.background = "#fff";
|
||||
tabHtml.style.fontWeight = "bold";
|
||||
} else {
|
||||
preview.style.display = "block";
|
||||
tabVorschau.style.background = "#fff";
|
||||
tabVorschau.style.fontWeight = "bold";
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
tabEditor.addEventListener("click", function (e) { e.preventDefault(); showTab("editor"); });
|
||||
tabHtml.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
syncToTextarea();
|
||||
showTab("html");
|
||||
});
|
||||
tabVorschau.addEventListener("click", function (e) { e.preventDefault(); showTab("vorschau"); });
|
||||
|
||||
// Zusammenbauen
|
||||
wrapper.appendChild(toolbar);
|
||||
wrapper.appendChild(editor);
|
||||
wrapper.appendChild(preview);
|
||||
|
||||
// Textarea hinter Editor platzieren
|
||||
textarea.parentNode.insertBefore(wrapper, textarea);
|
||||
wrapper.appendChild(textarea);
|
||||
|
||||
// Initial: Editor-Tab aktiv
|
||||
showTab("editor");
|
||||
|
||||
// ---- Hilfsfunktionen ----
|
||||
function syncToTextarea() {
|
||||
textarea.value = editor.innerHTML;
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
// Platzhalter durch Beispielwerte ersetzen für Vorschau
|
||||
var html = textarea.value;
|
||||
var replacements = {
|
||||
"{{ anrede }}": "Frau",
|
||||
"{{ vorname }}": "Maria",
|
||||
"{{ nachname }}": "Mustermann",
|
||||
"{{ strasse }}": "Musterstraße 12",
|
||||
"{{ plz }}": "46499",
|
||||
"{{ ort }}": "Hamminkeln",
|
||||
"{{ datum }}": "Freitag, 17. April 2026",
|
||||
"{{ uhrzeit }}": "19:00 Uhr",
|
||||
"{{ veranstaltungsort }}": "Marienthaler Gasthof",
|
||||
"{{ gasthaus_adresse }}": "Pastor-Winkelmann-Str. 2, 46499 Hamminkeln",
|
||||
};
|
||||
for (var key in replacements) {
|
||||
html = html.split(key).join(replacements[key]);
|
||||
}
|
||||
preview.innerHTML = html || "<em style='color:#999;'>Kein Brieftext eingegeben.</em>";
|
||||
}
|
||||
|
||||
function createTabBtn(label, active) {
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
btn.style.cssText = "padding:3px 10px;cursor:pointer;border:1px solid #ccc;border-radius:3px;font-size:12px;background:" + (active ? "#fff" : "#f5f5f5") + ";";
|
||||
if (active) btn.style.fontWeight = "bold";
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
|
||||
function loadVorlagen(selectEl) {
|
||||
// Lese Vorlagen über einfachen Admin-API-Aufruf
|
||||
fetch("/admin/stiftung/briefvorlage/?format=json", {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!data || !data.results) return;
|
||||
data.results.forEach(function (v) {
|
||||
var opt = document.createElement("option");
|
||||
opt.value = v.id || v.pk;
|
||||
opt.textContent = v.name || v.fields && v.fields.name;
|
||||
opt.dataset.briefvorlage = v.briefvorlage || v.fields && v.fields.briefvorlage || "";
|
||||
opt.dataset.betreff = v.betreff || v.fields && v.fields.betreff || "";
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
// Kein API-Endpunkt – Vorlage-Loader deaktivieren
|
||||
});
|
||||
|
||||
// Alternativ: REST-API
|
||||
fetch("/api/v1/briefvorlagen/", {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||
})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (data) {
|
||||
if (!data) return;
|
||||
var results = Array.isArray(data) ? data : data.results;
|
||||
if (!results) return;
|
||||
// Bereits vorhandene Optionen nicht doppeln
|
||||
var existing = Array.from(selectEl.options).map(function (o) { return o.value; });
|
||||
results.forEach(function (v) {
|
||||
var id = String(v.id || v.pk || "");
|
||||
if (existing.includes(id)) return;
|
||||
var opt = document.createElement("option");
|
||||
opt.value = id;
|
||||
opt.textContent = v.name;
|
||||
opt.dataset.briefvorlage = v.briefvorlage || "";
|
||||
opt.dataset.betreff = v.betreff || "";
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
})
|
||||
.catch(function () { /* kein REST-Endpunkt */ });
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,3 +1,4 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db.models import Count, Sum
|
||||
from django.urls import reverse
|
||||
@@ -7,7 +8,7 @@ from django.utils.safestring import mark_safe
|
||||
|
||||
from . import models
|
||||
from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
CSVImport, Destinataer, DestinataerEmailEingang,
|
||||
BriefVorlage, CSVImport, Destinataer, DestinataerEmailEingang,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
|
||||
@@ -1238,8 +1239,31 @@ class VeranstaltungsteilnehmerInline(admin.TabularInline):
|
||||
]
|
||||
|
||||
|
||||
class BriefVorlageWidget(forms.Textarea):
|
||||
"""Erweitertes Textarea-Widget für HTML-Briefvorlagen mit Editor-Panel und Platzhalter-Hilfe."""
|
||||
|
||||
class Media:
|
||||
js = ["stiftung/js/briefvorlage_editor.js"]
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
default_attrs = {"rows": 18, "cols": 80, "class": "briefvorlage-textarea", "style": "font-family: monospace; font-size: 13px;"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(attrs=default_attrs)
|
||||
|
||||
|
||||
class VeranstaltungAdminForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Veranstaltung
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
"briefvorlage": BriefVorlageWidget(),
|
||||
}
|
||||
|
||||
|
||||
@admin.register(Veranstaltung)
|
||||
class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
form = VeranstaltungAdminForm
|
||||
list_display = [
|
||||
"titel", "datum", "uhrzeit", "ort", "status",
|
||||
"get_teilnehmer_count", "get_zugesagte_count", "budget_pro_person",
|
||||
@@ -1247,7 +1271,7 @@ class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
list_filter = ["status", "datum"]
|
||||
search_fields = ["titel", "ort", "beschreibung"]
|
||||
ordering = ["-datum"]
|
||||
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_link"]
|
||||
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_aktionen", "platzhalter_dokumentation"]
|
||||
inlines = [VeranstaltungsteilnehmerInline]
|
||||
|
||||
fieldsets = (
|
||||
@@ -1255,19 +1279,22 @@ class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
("Veranstaltungsort", {"fields": ("ort", "adresse")}),
|
||||
("Details", {"fields": ("beschreibung", "budget_pro_person")}),
|
||||
(
|
||||
"Serienbrief",
|
||||
"Serienbrief – Vorlage",
|
||||
{
|
||||
"fields": (
|
||||
"platzhalter_dokumentation",
|
||||
"betreff",
|
||||
"briefvorlage",
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Serienbrief – Unterschriften & Aktionen",
|
||||
{
|
||||
"fields": (
|
||||
"betreff", "briefvorlage",
|
||||
"unterschrift_1_name", "unterschrift_1_titel",
|
||||
"unterschrift_2_name", "unterschrift_2_titel",
|
||||
"serienbrief_link",
|
||||
),
|
||||
"description": (
|
||||
"Betreff leer = 'Einladung zum [Titel]'. "
|
||||
"Platzhalter in der Vorlage: {{ anrede }}, {{ vorname }}, "
|
||||
"{{ nachname }}, {{ strasse }}, {{ plz }}, {{ ort_teilnehmer }}, "
|
||||
"{{ datum }}, {{ uhrzeit }}, {{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
"serienbrief_aktionen",
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -1282,15 +1309,43 @@ class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
return obj.get_zugesagte_count()
|
||||
get_zugesagte_count.short_description = "Zugesagt"
|
||||
|
||||
def serienbrief_link(self, obj):
|
||||
def platzhalter_dokumentation(self, obj):
|
||||
return format_html(
|
||||
"""<div class="help" style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;padding:10px 14px;margin-bottom:4px;">
|
||||
<strong>Verfügbare Platzhalter im Brieftext:</strong><br>
|
||||
<table style="margin-top:6px;border-collapse:collapse;font-size:13px;">
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ anrede }}}}</td><td>Anredetitel (Herr / Frau)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ vorname }}}}</td><td>Vorname des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ nachname }}}}</td><td>Nachname des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ strasse }}}}</td><td>Straße und Hausnummer</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ plz }}}}</td><td>Postleitzahl</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ ort }}}}</td><td>Wohnort des Empfängers</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ datum }}}}</td><td>Datum der Veranstaltung (z.B. Freitag, 17. April 2026)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ uhrzeit }}}}</td><td>Uhrzeit der Veranstaltung (z.B. 19:00 Uhr)</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ veranstaltungsort }}}}</td><td>Name des Veranstaltungsorts / Gasthaus</td></tr>
|
||||
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ gasthaus_adresse }}}}</td><td>Adresse des Gasthauses</td></tr>
|
||||
</table>
|
||||
<div style="margin-top:8px;font-size:12px;color:#6c757d;">
|
||||
Platzhalter werden beim PDF-Export automatisch mit den Empfänger- und Veranstaltungsdaten befüllt.
|
||||
Tipp: Vorlagen unter <a href="/admin/stiftung/briefvorlage/" target="_blank">Verwaltung → Briefvorlagen</a> speichern und wiederverwenden.
|
||||
</div>
|
||||
</div>"""
|
||||
)
|
||||
platzhalter_dokumentation.short_description = "Platzhalter-Dokumentation"
|
||||
platzhalter_dokumentation.allow_tags = True
|
||||
|
||||
def serienbrief_aktionen(self, obj):
|
||||
if obj.pk:
|
||||
from django.urls import reverse as url_reverse
|
||||
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
|
||||
pdf_url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
|
||||
vorschau_url = url_reverse("stiftung:veranstaltung_serienbrief_vorschau", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a href="{}" target="_blank" class="button">Serienbrief-PDF generieren</a>', url
|
||||
'<a href="{}" target="_blank" class="button" style="margin-right:8px;">Serienbrief-PDF generieren</a>'
|
||||
'<a href="{}" target="_blank" class="button default">Vorschau im Browser</a>',
|
||||
pdf_url, vorschau_url,
|
||||
)
|
||||
return "–"
|
||||
serienbrief_link.short_description = "Serienbrief"
|
||||
serienbrief_aktionen.short_description = "Aktionen"
|
||||
|
||||
actions = ["generate_serienbrief"]
|
||||
|
||||
@@ -1310,6 +1365,34 @@ class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
generate_serienbrief.short_description = "Serienbrief-PDF generieren"
|
||||
|
||||
|
||||
@admin.register(BriefVorlage)
|
||||
class BriefVorlageAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "beschreibung_kurz", "erstellt_am", "aktualisiert_am"]
|
||||
search_fields = ["name", "beschreibung"]
|
||||
ordering = ["name"]
|
||||
readonly_fields = ["erstellt_am", "aktualisiert_am"]
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("name", "beschreibung")}),
|
||||
(
|
||||
"Briefinhalt",
|
||||
{
|
||||
"fields": ("betreff", "briefvorlage"),
|
||||
"description": (
|
||||
"Verfügbare Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, "
|
||||
"{{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
},
|
||||
),
|
||||
("System", {"fields": ("erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
|
||||
def beschreibung_kurz(self, obj):
|
||||
return obj.beschreibung[:80] + "…" if len(obj.beschreibung) > 80 else obj.beschreibung
|
||||
beschreibung_kurz.short_description = "Beschreibung"
|
||||
|
||||
|
||||
@admin.register(Veranstaltungsteilnehmer)
|
||||
class VeranstaltungsteilnehmerAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
|
||||
@@ -8,6 +8,7 @@ from .models import (BankTransaction, Destinataer, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
||||
LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend,
|
||||
Veranstaltung, Veranstaltungsteilnehmer,
|
||||
Verwaltungskosten, VierteljahresNachweis)
|
||||
|
||||
|
||||
@@ -1717,3 +1718,59 @@ class GeschichteBildForm(forms.ModelForm):
|
||||
'alt_text': 'Wichtig für Barrierefreiheit',
|
||||
'sortierung': 'Reihenfolge in der Bildergalerie'
|
||||
}
|
||||
|
||||
|
||||
class VeranstaltungForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Veranstaltungen inkl. Serienbrief-Felder"""
|
||||
|
||||
class Meta:
|
||||
model = Veranstaltung
|
||||
fields = [
|
||||
"titel", "datum", "uhrzeit", "ort", "adresse",
|
||||
"beschreibung", "status", "budget_pro_person",
|
||||
"betreff", "briefvorlage",
|
||||
"unterschrift_1_name", "unterschrift_1_titel",
|
||||
"unterschrift_2_name", "unterschrift_2_titel",
|
||||
]
|
||||
widgets = {
|
||||
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
||||
"uhrzeit": forms.TimeInput(attrs={"class": "form-control", "type": "time"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"adresse": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"beschreibung": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
||||
"status": forms.Select(attrs={"class": "form-select"}),
|
||||
"budget_pro_person": forms.NumberInput(attrs={"class": "form-control", "step": "0.01"}),
|
||||
"betreff": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"briefvorlage": forms.Textarea(attrs={"class": "form-control", "rows": 12}),
|
||||
"unterschrift_1_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_1_titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_2_name": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"unterschrift_2_titel": forms.TextInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
|
||||
class VeranstaltungsteilnehmerForm(forms.ModelForm):
|
||||
"""Form für das Erstellen und Bearbeiten von Veranstaltungsteilnehmern"""
|
||||
|
||||
class Meta:
|
||||
model = Veranstaltungsteilnehmer
|
||||
fields = [
|
||||
"anrede", "vorname", "nachname",
|
||||
"strasse", "plz", "ort", "email",
|
||||
"rsvp_status", "bemerkungen",
|
||||
"paechter", "destinataer",
|
||||
]
|
||||
widgets = {
|
||||
"anrede": forms.Select(attrs={"class": "form-select"}),
|
||||
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
||||
"rsvp_status": forms.Select(attrs={"class": "form-select"}),
|
||||
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||
"paechter": forms.Select(attrs={"class": "form-select"}),
|
||||
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
||||
}
|
||||
|
||||
30
app/stiftung/migrations/0046_briefvorlage_model.py
Normal file
30
app/stiftung/migrations/0046_briefvorlage_model.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.0.6 on 2026-03-10 22:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0045_add_serienbrief_editable_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BriefVorlage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='Vorlagenname')),
|
||||
('beschreibung', models.TextField(blank=True, help_text='Kurze Beschreibung des Verwendungszwecks dieser Vorlage.', verbose_name='Beschreibung')),
|
||||
('briefvorlage', models.TextField(help_text='HTML-Text des Briefs. Verfügbare Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, {{ veranstaltungsort }}, {{ gasthaus_adresse }}', verbose_name='Brieftext (HTML)')),
|
||||
('betreff', models.CharField(blank=True, help_text='Wird beim Laden der Vorlage in die Veranstaltung übernommen. Leer = unveränderter Betreff.', max_length=300, verbose_name='Standard-Betreff')),
|
||||
('erstellt_am', models.DateTimeField(auto_now_add=True)),
|
||||
('aktualisiert_am', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Briefvorlage',
|
||||
'verbose_name_plural': 'Briefvorlagen',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2154,6 +2154,9 @@ class ApplicationPermission(models.Model):
|
||||
("manage_backups", "Kann Backups erstellen und verwalten"),
|
||||
("manage_users", "Kann Benutzer verwalten"),
|
||||
("manage_permissions", "Kann Berechtigungen verwalten"),
|
||||
# Veranstaltungen Permissions
|
||||
("manage_veranstaltungen", "Kann Veranstaltungen verwalten"),
|
||||
("view_veranstaltungen", "Kann Veranstaltungen anzeigen"),
|
||||
# Import/Export Permissions
|
||||
("import_data", "Kann Daten importieren"),
|
||||
("export_data", "Kann Daten exportieren"),
|
||||
@@ -3281,6 +3284,43 @@ class DestinataerEmailEingang(models.Model):
|
||||
]
|
||||
|
||||
|
||||
class BriefVorlage(models.Model):
|
||||
"""Wiederverwendbare Briefvorlagen für Serienbriefe (Veranstaltungseinladungen u.ä.)"""
|
||||
|
||||
name = models.CharField(max_length=100, verbose_name="Vorlagenname")
|
||||
beschreibung = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Beschreibung",
|
||||
help_text="Kurze Beschreibung des Verwendungszwecks dieser Vorlage.",
|
||||
)
|
||||
briefvorlage = models.TextField(
|
||||
verbose_name="Brieftext (HTML)",
|
||||
help_text=(
|
||||
"HTML-Text des Briefs. Verfügbare Platzhalter: "
|
||||
"{{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, "
|
||||
"{{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
)
|
||||
betreff = models.CharField(
|
||||
max_length=300,
|
||||
blank=True,
|
||||
verbose_name="Standard-Betreff",
|
||||
help_text="Wird beim Laden der Vorlage in die Veranstaltung übernommen. Leer = unveränderter Betreff.",
|
||||
)
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Briefvorlage"
|
||||
verbose_name_plural = "Briefvorlagen"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Veranstaltung(models.Model):
|
||||
"""Veranstaltungen der Stiftung, z.B. Stiftungsessen mit Rechnungslegung"""
|
||||
|
||||
|
||||
@@ -361,12 +361,36 @@ urlpatterns = [
|
||||
),
|
||||
# Veranstaltungsmodul
|
||||
path("veranstaltungen/", views.veranstaltung_list, name="veranstaltung_list"),
|
||||
path("veranstaltungen/neu/", views.veranstaltung_create, name="veranstaltung_create"),
|
||||
path("veranstaltungen/<uuid:pk>/", views.veranstaltung_detail, name="veranstaltung_detail"),
|
||||
path("veranstaltungen/<uuid:pk>/bearbeiten/", views.veranstaltung_update, name="veranstaltung_update"),
|
||||
path("veranstaltungen/<uuid:pk>/loeschen/", views.veranstaltung_delete, name="veranstaltung_delete"),
|
||||
path(
|
||||
"veranstaltungen/<uuid:pk>/serienbrief/",
|
||||
views.veranstaltung_serienbrief_pdf,
|
||||
name="veranstaltung_serienbrief_pdf",
|
||||
),
|
||||
path(
|
||||
"veranstaltungen/<uuid:pk>/serienbrief-vorschau/",
|
||||
views.veranstaltung_serienbrief_vorschau,
|
||||
name="veranstaltung_serienbrief_vorschau",
|
||||
),
|
||||
# Teilnehmer CRUD
|
||||
path(
|
||||
"veranstaltungen/<uuid:veranstaltung_pk>/teilnehmer/neu/",
|
||||
views.teilnehmer_create,
|
||||
name="teilnehmer_create",
|
||||
),
|
||||
path(
|
||||
"veranstaltungen/<uuid:veranstaltung_pk>/teilnehmer/<uuid:pk>/bearbeiten/",
|
||||
views.teilnehmer_update,
|
||||
name="teilnehmer_update",
|
||||
),
|
||||
path(
|
||||
"veranstaltungen/<uuid:veranstaltung_pk>/teilnehmer/<uuid:pk>/loeschen/",
|
||||
views.teilnehmer_delete,
|
||||
name="teilnehmer_delete",
|
||||
),
|
||||
# Gramps integration (probe)
|
||||
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
|
||||
path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
@@ -6430,13 +6431,20 @@ def user_login(request):
|
||||
|
||||
log_login(request, user)
|
||||
|
||||
messages.success(request, f"Willkommen zurück, {user.username}!")
|
||||
|
||||
# Redirect to safe next URL path or home
|
||||
# Determine redirect target
|
||||
next_param = request.GET.get("next") or request.POST.get("next")
|
||||
if next_param and next_param.startswith("/"):
|
||||
return redirect(next_param)
|
||||
return redirect("stiftung:home")
|
||||
if not next_param or not next_param.startswith("/"):
|
||||
next_param = reverse("stiftung:home")
|
||||
|
||||
# Check if user has 2FA enabled - redirect to verification first
|
||||
has_2fa = TOTPDevice.objects.filter(user=user, confirmed=True).exists()
|
||||
if has_2fa:
|
||||
from urllib.parse import urlencode
|
||||
verify_url = reverse("stiftung:two_factor_verify") + "?" + urlencode({"next": next_param})
|
||||
return redirect(verify_url)
|
||||
|
||||
messages.success(request, f"Willkommen zurück, {user.username}!")
|
||||
return redirect(next_param)
|
||||
else:
|
||||
messages.error(request, "Ungültige Anmeldedaten.")
|
||||
else:
|
||||
@@ -8685,3 +8693,153 @@ def veranstaltung_serienbrief_pdf(request, pk):
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_serienbrief_vorschau(request, pk):
|
||||
"""HTML-Vorschau des Serienbriefs im Browser (kein PDF-Download)"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||||
return render(
|
||||
request,
|
||||
"stiftung/veranstaltung/serienbrief_vorschau.html",
|
||||
{
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_create(request):
|
||||
"""Neue Veranstaltung erstellen"""
|
||||
from .forms import VeranstaltungForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST)
|
||||
if form.is_valid():
|
||||
veranstaltung = form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde erstellt.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"title": "Neue Veranstaltung erstellen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_update(request, pk):
|
||||
"""Veranstaltung bearbeiten (inkl. Serienbrief-Felder)"""
|
||||
from .forms import VeranstaltungForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST, instance=veranstaltung)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde aktualisiert.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm(instance=veranstaltung)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": f"Veranstaltung bearbeiten: {veranstaltung.titel}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_delete(request, pk):
|
||||
"""Veranstaltung löschen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
titel = veranstaltung.titel
|
||||
veranstaltung.delete()
|
||||
messages.success(request, f'Veranstaltung "{titel}" wurde gelöscht.')
|
||||
return redirect("stiftung:veranstaltung_list")
|
||||
|
||||
return render(request, "stiftung/veranstaltung/delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_create(request, veranstaltung_pk):
|
||||
"""Teilnehmer zu einer Veranstaltung hinzufügen"""
|
||||
from .forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST)
|
||||
if form.is_valid():
|
||||
teilnehmer = form.save(commit=False)
|
||||
teilnehmer.veranstaltung = veranstaltung
|
||||
teilnehmer.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde hinzugefügt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": "Teilnehmer hinzufügen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_update(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer bearbeiten"""
|
||||
from .forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST, instance=teilnehmer)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde aktualisiert.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm(instance=teilnehmer)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
"title": f"Teilnehmer bearbeiten: {teilnehmer.vorname} {teilnehmer.nachname}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_delete(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer aus Veranstaltung entfernen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
name = f"{teilnehmer.vorname} {teilnehmer.nachname}"
|
||||
teilnehmer.delete()
|
||||
messages.success(request, f"{name} wurde aus der Teilnehmerliste entfernt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
})
|
||||
|
||||
36
app/templates/stiftung/veranstaltung/delete.html
Normal file
36
app/templates/stiftung/veranstaltung/delete.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Veranstaltung löschen – Stiftungsverwaltung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Veranstaltung löschen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Möchten Sie die folgende Veranstaltung wirklich löschen?</p>
|
||||
<div class="alert alert-warning">
|
||||
<strong>{{ veranstaltung.titel }}</strong><br>
|
||||
{{ veranstaltung.datum|date:"d.m.Y" }} – {{ veranstaltung.ort }}<br>
|
||||
<small class="text-muted">{{ veranstaltung.get_teilnehmer_count }} Teilnehmer werden ebenfalls entfernt.</small>
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'stiftung:veranstaltung_detail' veranstaltung.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-2"></i>Endgültig löschen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -20,7 +20,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'admin:stiftung_veranstaltung_change' veranstaltung.pk %}" class="btn btn-outline-secondary">
|
||||
<a href="{% url 'stiftung:veranstaltung_update' veranstaltung.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-edit me-1"></i>Bearbeiten
|
||||
</a>
|
||||
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' veranstaltung.pk %}" class="btn btn-success">
|
||||
@@ -125,11 +125,15 @@
|
||||
class="btn btn-success w-100">
|
||||
<i class="fas fa-file-pdf me-2"></i>Serienbrief-PDF (alle Teilnehmer)
|
||||
</a>
|
||||
<a href="{% url 'admin:stiftung_veranstaltungsteilnehmer_add' %}?veranstaltung={{ veranstaltung.pk }}"
|
||||
<a href="{% url 'stiftung:veranstaltung_serienbrief_vorschau' veranstaltung.pk %}"
|
||||
class="btn btn-outline-primary w-100" target="_blank">
|
||||
<i class="fas fa-eye me-2"></i>Serienbrief-Vorschau
|
||||
</a>
|
||||
<a href="{% url 'stiftung:teilnehmer_create' veranstaltung.pk %}"
|
||||
class="btn btn-outline-primary w-100">
|
||||
<i class="fas fa-user-plus me-2"></i>Teilnehmer hinzufügen
|
||||
</a>
|
||||
<a href="{% url 'admin:stiftung_veranstaltung_change' veranstaltung.pk %}"
|
||||
<a href="{% url 'stiftung:veranstaltung_update' veranstaltung.pk %}"
|
||||
class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-edit me-2"></i>Veranstaltung bearbeiten
|
||||
</a>
|
||||
@@ -142,6 +146,9 @@
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||
<span><i class="fas fa-users me-2"></i>Teilnehmerliste ({{ teilnehmer.count }})</span>
|
||||
<a href="{% url 'stiftung:teilnehmer_create' veranstaltung.pk %}" class="btn btn-sm btn-outline-light">
|
||||
<i class="fas fa-user-plus me-1"></i>Hinzufügen
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if teilnehmer %}
|
||||
@@ -153,6 +160,7 @@
|
||||
<th>E-Mail</th>
|
||||
<th>RSVP</th>
|
||||
<th>Bemerkungen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -179,6 +187,14 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ t.bemerkungen|default:"–" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'stiftung:teilnehmer_update' veranstaltung.pk t.pk %}" class="btn btn-sm btn-outline-secondary me-1" title="Bearbeiten">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'stiftung:teilnehmer_delete' veranstaltung.pk t.pk %}" class="btn btn-sm btn-outline-danger" title="Entfernen">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -187,7 +203,7 @@
|
||||
<div class="p-4 text-center text-muted">
|
||||
<i class="fas fa-users fa-2x mb-2"></i>
|
||||
<p>Noch keine Teilnehmer eingetragen.</p>
|
||||
<a href="{% url 'admin:stiftung_veranstaltungsteilnehmer_add' %}?veranstaltung={{ veranstaltung.pk }}"
|
||||
<a href="{% url 'stiftung:teilnehmer_create' veranstaltung.pk %}"
|
||||
class="btn btn-primary">
|
||||
<i class="fas fa-user-plus me-1"></i>Ersten Teilnehmer hinzufügen
|
||||
</a>
|
||||
|
||||
200
app/templates/stiftung/veranstaltung/form.html
Normal file
200
app/templates/stiftung/veranstaltung/form.html
Normal file
@@ -0,0 +1,200 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3">
|
||||
<i class="fas fa-calendar-alt text-primary me-2"></i>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<a href="{% url 'stiftung:veranstaltung_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Zurück zur Liste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<!-- Grunddaten -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-info-circle me-2"></i>Grunddaten
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="{{ form.titel.id_for_label }}" class="form-label">{{ form.titel.label }} *</label>
|
||||
{{ form.titel }}
|
||||
{% if form.titel.errors %}<div class="invalid-feedback d-block">{{ form.titel.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.datum.id_for_label }}" class="form-label">{{ form.datum.label }} *</label>
|
||||
{{ form.datum }}
|
||||
{% if form.datum.errors %}<div class="invalid-feedback d-block">{{ form.datum.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.uhrzeit.id_for_label }}" class="form-label">{{ form.uhrzeit.label }}</label>
|
||||
{{ form.uhrzeit }}
|
||||
{% if form.uhrzeit.errors %}<div class="invalid-feedback d-block">{{ form.uhrzeit.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.status.id_for_label }}" class="form-label">{{ form.status.label }}</label>
|
||||
{{ form.status }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.ort.id_for_label }}" class="form-label">{{ form.ort.label }} *</label>
|
||||
{{ form.ort }}
|
||||
{% if form.ort.errors %}<div class="invalid-feedback d-block">{{ form.ort.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.budget_pro_person.id_for_label }}" class="form-label">{{ form.budget_pro_person.label }}</label>
|
||||
{{ form.budget_pro_person }}
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label for="{{ form.adresse.id_for_label }}" class="form-label">{{ form.adresse.label }}</label>
|
||||
{{ form.adresse }}
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label for="{{ form.beschreibung.id_for_label }}" class="form-label">{{ form.beschreibung.label }}</label>
|
||||
{{ form.beschreibung }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Serienbrief-Vorlage -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-envelope-open-text me-2"></i>Serienbrief – Vorlage
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="col-12 mb-3">
|
||||
<label for="{{ form.betreff.id_for_label }}" class="form-label">{{ form.betreff.label }}</label>
|
||||
{{ form.betreff }}
|
||||
<div class="form-text">{{ form.betreff.help_text }}</div>
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label for="{{ form.briefvorlage.id_for_label }}" class="form-label">{{ form.briefvorlage.label }}</label>
|
||||
{{ form.briefvorlage }}
|
||||
{% if form.briefvorlage.errors %}<div class="invalid-feedback d-block">{{ form.briefvorlage.errors.0 }}</div>{% endif %}
|
||||
<div class="form-text">{{ form.briefvorlage.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unterschriften -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-signature me-2"></i>Serienbrief – Unterschriften
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.unterschrift_1_name.id_for_label }}" class="form-label">{{ form.unterschrift_1_name.label }}</label>
|
||||
{{ form.unterschrift_1_name }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.unterschrift_1_titel.id_for_label }}" class="form-label">{{ form.unterschrift_1_titel.label }}</label>
|
||||
{{ form.unterschrift_1_titel }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.unterschrift_2_name.id_for_label }}" class="form-label">{{ form.unterschrift_2_name.label }}</label>
|
||||
{{ form.unterschrift_2_name }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.unterschrift_2_titel.id_for_label }}" class="form-label">{{ form.unterschrift_2_titel.label }}</label>
|
||||
{{ form.unterschrift_2_titel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-question-circle me-2"></i>Platzhalter-Hilfe
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted mb-2">Verfügbare Platzhalter für die Briefvorlage:</p>
|
||||
<table class="table table-sm small">
|
||||
<tbody>
|
||||
<tr><td class="font-monospace text-danger">{{ anrede }}</td><td>Herr / Frau</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ vorname }}</td><td>Vorname</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ nachname }}</td><td>Nachname</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ strasse }}</td><td>Straße + Nr.</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ plz }}</td><td>PLZ</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ ort }}</td><td>Wohnort</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ datum }}</td><td>Veranstaltungsdatum</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ uhrzeit }}</td><td>Uhrzeit</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ veranstaltungsort }}</td><td>Gasthaus / Ort</td></tr>
|
||||
<tr><td class="font-monospace text-danger">{{ gasthaus_adresse }}</td><td>Adresse Gasthaus</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="small text-muted mb-0">Platzhalter werden beim PDF-Export automatisch befüllt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if veranstaltung %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-tools me-2"></i>Aktionen
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<a href="{% url 'stiftung:veranstaltung_serienbrief_vorschau' veranstaltung.pk %}"
|
||||
class="btn btn-outline-primary w-100" target="_blank">
|
||||
<i class="fas fa-eye me-2"></i>Serienbrief-Vorschau
|
||||
</a>
|
||||
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' veranstaltung.pk %}"
|
||||
class="btn btn-outline-success w-100">
|
||||
<i class="fas fa-file-pdf me-2"></i>Serienbrief-PDF
|
||||
</a>
|
||||
<hr>
|
||||
<a href="{% url 'stiftung:veranstaltung_delete' veranstaltung.pk %}"
|
||||
class="btn btn-outline-danger w-100">
|
||||
<i class="fas fa-trash me-2"></i>Veranstaltung löschen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-lg-8">
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'stiftung:veranstaltung_list' %}" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-2"></i>Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save me-2"></i>
|
||||
{% if veranstaltung %}Aktualisieren{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Veranstaltungen
|
||||
</h1>
|
||||
<a href="{% url 'admin:stiftung_veranstaltung_add' %}" class="btn btn-primary">
|
||||
<a href="{% url 'stiftung:veranstaltung_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Neue Veranstaltung
|
||||
</a>
|
||||
</div>
|
||||
@@ -68,7 +68,7 @@
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>Noch keine Veranstaltungen angelegt.
|
||||
<a href="{% url 'admin:stiftung_veranstaltung_add' %}">Jetzt erste Veranstaltung erstellen.</a>
|
||||
<a href="{% url 'stiftung:veranstaltung_create' %}">Jetzt erste Veranstaltung erstellen.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
134
app/templates/stiftung/veranstaltung/serienbrief_vorschau.html
Normal file
134
app/templates/stiftung/veranstaltung/serienbrief_vorschau.html
Normal file
@@ -0,0 +1,134 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Serienbrief-Vorschau – {{ veranstaltung.titel }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-3">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>
|
||||
Serienbrief-Vorschau
|
||||
<small class="text-muted fs-6">{{ veranstaltung.titel }} ({{ veranstaltung.datum|date:"j. F Y" }})</small>
|
||||
</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' veranstaltung.pk %}"
|
||||
class="btn btn-primary">
|
||||
PDF generieren
|
||||
</a>
|
||||
<a href="{% url 'stiftung:veranstaltung_detail' veranstaltung.pk %}"
|
||||
class="btn btn-outline-secondary">
|
||||
Zurück zur Veranstaltung
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not teilnehmer %}
|
||||
<div class="alert alert-warning">
|
||||
Diese Veranstaltung hat noch keine Teilnehmer. Bitte zuerst Teilnehmer anlegen.
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="alert alert-info d-flex align-items-start gap-2 mb-3">
|
||||
<span>ℹ️</span>
|
||||
<div>
|
||||
<strong>{{ teilnehmer.count }} Brief{% if teilnehmer.count != 1 %}e{% endif %}</strong> werden generiert.
|
||||
Die Vorschau zeigt jeden Brief auf einer separaten Seite.
|
||||
Platzhalter wie <code>{% verbatim %}{{ vorname }}{% endverbatim %}</code> sind hier bereits durch Beispieldaten ersetzt.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation zwischen Briefen -->
|
||||
<div class="mb-3 d-flex gap-2 align-items-center flex-wrap">
|
||||
<strong>Empfänger:</strong>
|
||||
{% for t in teilnehmer %}
|
||||
<a href="#brief-{{ forloop.counter }}"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
{{ t.nachname }}, {{ t.vorname }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Einzelne Briefe -->
|
||||
{% for t in teilnehmer %}
|
||||
<div id="brief-{{ forloop.counter }}"
|
||||
style="max-width:210mm;margin:0 auto 40px;padding:20mm 25mm;border:1px solid #dee2e6;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:'Times New Roman',Times,serif;font-size:11pt;line-height:1.4;">
|
||||
|
||||
<!-- Stiftungskopf -->
|
||||
<div style="font-size:12pt;font-weight:bold;margin-bottom:2mm;">van Hees-Theyssen-Vogel'sche Stiftung</div>
|
||||
<div style="font-size:8.5pt;color:#444;margin-bottom:5mm;">
|
||||
Raesfelder Str. 3 · 46499 Hamminkeln
|
||||
</div>
|
||||
|
||||
<!-- Empfänger -->
|
||||
<div style="min-height:35mm;margin-bottom:5mm;">
|
||||
<div style="font-size:7.5pt;border-bottom:1px solid #000;margin-bottom:3pt;padding-bottom:1pt;color:#444;">
|
||||
van Hees-Theyssen-Vogel'sche Stiftung · Raesfelder Str. 3 · 46499 Hamminkeln
|
||||
</div>
|
||||
<p style="margin:0;line-height:1.3;">{{ t.anrede }} {{ t.vorname }} {{ t.nachname }}</p>
|
||||
{% if t.strasse %}<p style="margin:0;line-height:1.3;">{{ t.strasse }}</p>{% endif %}
|
||||
{% if t.plz or t.ort %}<p style="margin:0;line-height:1.3;">{{ t.plz }} {{ t.ort }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Datum -->
|
||||
<div style="text-align:right;margin-bottom:4mm;">
|
||||
Hamminkeln, den {{ veranstaltung.datum|date:"j. F Y" }}
|
||||
</div>
|
||||
|
||||
<!-- Betreff -->
|
||||
<div style="font-weight:bold;margin-bottom:4mm;">
|
||||
{% if veranstaltung.betreff %}{{ veranstaltung.betreff }}{% else %}Einladung zum {{ veranstaltung.titel }}{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Anrede -->
|
||||
<div style="margin-bottom:3mm;">
|
||||
Sehr geehrte{% if t.anrede == "Herr" %}r Herr{% elif t.anrede == "Frau" %} Frau{% else %}
|
||||
{{ t.anrede }}{% endif %} {{ t.nachname }},
|
||||
</div>
|
||||
|
||||
<!-- Brieftext -->
|
||||
<div class="brieftext">
|
||||
{% if veranstaltung.briefvorlage %}
|
||||
{{ veranstaltung.briefvorlage|safe }}
|
||||
{% else %}
|
||||
<p>wir laden Sie herzlich ein, an der jährlichen Vorstellung der Rechnungslegung
|
||||
der van Hees-Theyssen-Vogel'schen Stiftung teilzunehmen.</p>
|
||||
<p>Die Veranstaltung findet statt am:</p>
|
||||
<div style="margin:4mm 0 4mm 10mm;font-weight:bold;">
|
||||
{{ veranstaltung.datum|date:"l, j. F Y" }}{% if veranstaltung.uhrzeit %}, {{ veranstaltung.uhrzeit|time:"H:i" }} Uhr{% endif %}<br>
|
||||
{{ veranstaltung.ort }}<br>
|
||||
{% if veranstaltung.adresse %}{{ veranstaltung.adresse }}{% endif %}
|
||||
</div>
|
||||
<p>Bitte teilen Sie uns Ihre Teilnahme bis zum <strong>4. April 2026</strong> mit.</p>
|
||||
<p>Wir freuen uns auf Ihr Kommen.</p>
|
||||
{% endif %}
|
||||
<p>Mit freundlichen Grüßen</p>
|
||||
</div>
|
||||
|
||||
<!-- Unterschriften -->
|
||||
<div style="margin-top:10mm;">
|
||||
<div style="display:inline-block;width:45%;vertical-align:top;">
|
||||
{% if veranstaltung.unterschrift_1_name %}
|
||||
<div style="border-top:1px solid #000;margin-bottom:2mm;width:80%;"></div>
|
||||
{{ veranstaltung.unterschrift_1_name }}<br>
|
||||
{{ veranstaltung.unterschrift_1_titel }}<br>
|
||||
van Hees-Theyssen-Vogel'sche Stiftung
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:inline-block;width:45%;vertical-align:top;">
|
||||
{% if veranstaltung.unterschrift_2_name %}
|
||||
<div style="border-top:1px solid #000;margin-bottom:2mm;width:80%;"></div>
|
||||
{{ veranstaltung.unterschrift_2_name }}<br>
|
||||
{{ veranstaltung.unterschrift_2_titel }}<br>
|
||||
van Hees-Theyssen-Vogel'sche Stiftung
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:right;margin-top:12mm;font-size:9pt;color:#999;">
|
||||
Brief {{ forloop.counter }} von {{ teilnehmer.count }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
35
app/templates/stiftung/veranstaltung/teilnehmer_delete.html
Normal file
35
app/templates/stiftung/veranstaltung/teilnehmer_delete.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Teilnehmer entfernen – Stiftungsverwaltung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Teilnehmer entfernen
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Möchten Sie den folgenden Teilnehmer wirklich aus der Veranstaltung entfernen?</p>
|
||||
<div class="alert alert-warning">
|
||||
<strong>{{ teilnehmer.anrede }} {{ teilnehmer.vorname }} {{ teilnehmer.nachname }}</strong><br>
|
||||
<small class="text-muted">Veranstaltung: {{ veranstaltung.titel }}</small>
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'stiftung:veranstaltung_detail' veranstaltung.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-2"></i>Entfernen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
119
app/templates/stiftung/veranstaltung/teilnehmer_form.html
Normal file
119
app/templates/stiftung/veranstaltung/teilnehmer_form.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ title }} – Stiftungsverwaltung{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'stiftung:veranstaltung_list' %}">Veranstaltungen</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'stiftung:veranstaltung_detail' veranstaltung.pk %}">{{ veranstaltung.titel }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-4">
|
||||
<i class="fas fa-user-plus text-primary me-2"></i>{{ title }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-user me-2"></i>Teilnehmerdaten
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<ul class="mb-0">
|
||||
{% for error in form.non_field_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.anrede.id_for_label }}" class="form-label">{{ form.anrede.label }}</label>
|
||||
{{ form.anrede }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.vorname.id_for_label }}" class="form-label">{{ form.vorname.label }} *</label>
|
||||
{{ form.vorname }}
|
||||
{% if form.vorname.errors %}<div class="invalid-feedback d-block">{{ form.vorname.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.nachname.id_for_label }}" class="form-label">{{ form.nachname.label }} *</label>
|
||||
{{ form.nachname }}
|
||||
{% if form.nachname.errors %}<div class="invalid-feedback d-block">{{ form.nachname.errors.0 }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.strasse.id_for_label }}" class="form-label">{{ form.strasse.label }}</label>
|
||||
{{ form.strasse }}
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label for="{{ form.plz.id_for_label }}" class="form-label">{{ form.plz.label }}</label>
|
||||
{{ form.plz }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="{{ form.ort.id_for_label }}" class="form-label">{{ form.ort.label }}</label>
|
||||
{{ form.ort }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.email.id_for_label }}" class="form-label">{{ form.email.label }}</label>
|
||||
{{ form.email }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.rsvp_status.id_for_label }}" class="form-label">{{ form.rsvp_status.label }}</label>
|
||||
{{ form.rsvp_status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.paechter.id_for_label }}" class="form-label">{{ form.paechter.label }}</label>
|
||||
{{ form.paechter }}
|
||||
<div class="form-text">Optional: Verknüpfung mit bestehendem Pächter</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.destinataer.id_for_label }}" class="form-label">{{ form.destinataer.label }}</label>
|
||||
{{ form.destinataer }}
|
||||
<div class="form-text">Optional: Verknüpfung mit bestehendem Destinatär</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.bemerkungen.id_for_label }}" class="form-label">{{ form.bemerkungen.label }}</label>
|
||||
{{ form.bemerkungen }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{% url 'stiftung:veranstaltung_detail' veranstaltung.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-2"></i>Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save me-2"></i>
|
||||
{% if teilnehmer %}Aktualisieren{% else %}Hinzufügen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user