refactor: E-Mail-Vorlagen als Liste + eigene Bearbeiten-Seite statt Formular-Stapel

Auf Nutzerfeedback: die erste Version zeigte alle bekannten Vorlagen als
gestapelte Formulare auf einer Seite. Jetzt wie Werkzeugkatalog/
Nutzerverwaltung: eine Liste (GET .../email-vorlagen) mit Klick auf eine
eigene Bearbeiten-Seite je Typ (GET/POST .../email-vorlagen/{typ}) -
skaliert sauber, wenn mit der Zeit weitere Benachrichtigungstypen
dazukommen, statt eine immer länger werdende Formular-Stapel-Seite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-01 11:10:46 +02:00
parent d3b171f721
commit fa5e68c892
6 changed files with 191 additions and 121 deletions

View File

@@ -3,7 +3,10 @@
// (/betreiber/email-vorlagen), jeder Mandant kann ihn für sich
// übersteuern (/verwaltung/email-vorlagen). emailVorlageTypen ist die
// feste, bekannte Liste der vom System versendeten E-Mails — aktuell
// nur Passwort-Zurücksetzen, die einzige E-Mail, die es bisher gibt.
// nur Passwort-Zurücksetzen, die einzige E-Mail, die es bisher gibt,
// aber bewusst als Liste-mit-Bearbeiten-Seite gebaut (wie Werkzeugkatalog/
// Nutzerverwaltung), nicht als eine Seite mit allen Formularen gestapelt
// — mit der Zeit kommen weitere Benachrichtigungstypen dazu.
package web
import (
@@ -28,13 +31,13 @@ var emailVorlageTypen = []emailVorlageTypInfo{
},
}
func istGueltigerEmailVorlageTyp(typ string) bool {
func findEmailVorlageTyp(typ string) (emailVorlageTypInfo, bool) {
for _, t := range emailVorlageTypen {
if t.Typ == typ {
return true
return t, true
}
}
return false
return emailVorlageTypInfo{}, false
}
// renderEmailVorlage ersetzt die bekannten Platzhalter im Text einer
@@ -48,54 +51,74 @@ func renderEmailVorlage(text, link string) string {
type emailVorlageZeile struct {
Typ string
Label string
Platzhalter string
Betreff string
Text string
IstUebersteuert bool // eigene Vorlage vorhanden statt Plattform-Standard geerbt
}
type emailVorlagenListeData struct {
Title string
Nav navData
Zeilen []emailVorlageZeile
Error string
Gespeichert bool
ActionBase string // unterscheidet Mandant- von Betreiber-Ziel-URL im gemeinsamen Template
Title string
Nav navData
Zeilen []emailVorlageZeile
ActionBase string // unterscheidet Mandant- von Betreiber-Ziel-URL im gemeinsamen Template
}
func (s *Server) handleEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
accountID := currentUser(r).AccountID
data := emailVorlagenListeData{
Title: "E-Mail-Vorlagen", Nav: navFor(r),
Gespeichert: r.URL.Query().Get("gespeichert") == "1",
ActionBase: "/verwaltung/email-vorlagen",
}
data := emailVorlagenListeData{Title: "E-Mail-Vorlagen", Nav: navFor(r), ActionBase: "/verwaltung/email-vorlagen"}
for _, t := range emailVorlageTypen {
resolved, err := s.store.ResolveEmailVorlage(r.Context(), accountID, t.Typ)
if err != nil {
http.Error(w, "E-Mail-Vorlagen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
eigene, err := s.store.GetEmailVorlage(r.Context(), &accountID, t.Typ)
istUebersteuert := err == nil && eigene.ID == resolved.ID
data.Zeilen = append(data.Zeilen, emailVorlageZeile{
Typ: t.Typ, Label: t.Label, Platzhalter: t.Platzhalter,
Betreff: resolved.Betreff, Text: resolved.Text, IstUebersteuert: istUebersteuert,
})
_, err := s.store.GetEmailVorlage(r.Context(), &accountID, t.Typ)
data.Zeilen = append(data.Zeilen, emailVorlageZeile{Typ: t.Typ, Label: t.Label, IstUebersteuert: err == nil})
}
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
}
}
func (s *Server) handleEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
type emailVorlageBearbeitenData struct {
Title string
Nav navData
Typ string
Label string
Platzhalter string
Betreff string
Text string
IstUebersteuert bool
ActionBase string
Error string
Gespeichert bool
}
func (s *Server) handleEmailVorlageBearbeitenForm(w http.ResponseWriter, r *http.Request) {
typ := r.PathValue("typ")
info, ok := findEmailVorlageTyp(typ)
if !ok {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
return
}
typ := r.FormValue("typ")
if !istGueltigerEmailVorlageTyp(typ) {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
accountID := currentUser(r).AccountID
resolved, err := s.store.ResolveEmailVorlage(r.Context(), accountID, typ)
if err != nil {
http.Error(w, "E-Mail-Vorlage konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
_, uebersteuert := s.store.GetEmailVorlage(r.Context(), &accountID, typ)
data := emailVorlageBearbeitenData{
Title: info.Label, Nav: navFor(r), Typ: typ, Label: info.Label, Platzhalter: info.Platzhalter,
Betreff: resolved.Betreff, Text: resolved.Text, IstUebersteuert: uebersteuert == nil,
ActionBase: "/verwaltung/email-vorlagen", Gespeichert: r.URL.Query().Get("gespeichert") == "1",
}
if err := s.templates.ExecuteTemplate(w, "email-vorlage-bearbeiten", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
}
}
func (s *Server) handleEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
typ := r.PathValue("typ")
if _, ok := findEmailVorlageTyp(typ); !ok {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
return
}
accountID := currentUser(r).AccountID
@@ -109,17 +132,13 @@ func (s *Server) handleEmailVorlageSpeichern(w http.ResponseWriter, r *http.Requ
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/verwaltung/email-vorlagen?gespeichert=1", http.StatusSeeOther)
http.Redirect(w, r, "/verwaltung/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
}
func (s *Server) handleEmailVorlageZuruecksetzen(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
return
}
typ := r.FormValue("typ")
if !istGueltigerEmailVorlageTyp(typ) {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
typ := r.PathValue("typ")
if _, ok := findEmailVorlageTyp(typ); !ok {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
return
}
accountID := currentUser(r).AccountID
@@ -127,41 +146,51 @@ func (s *Server) handleEmailVorlageZuruecksetzen(w http.ResponseWriter, r *http.
http.Error(w, "Zurücksetzen fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/verwaltung/email-vorlagen?gespeichert=1", http.StatusSeeOther)
http.Redirect(w, r, "/verwaltung/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
}
// ─── Betreiber (Ebene 5): plattformweiter Standard ─────────────────────
func (s *Server) handleBetreiberEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
data := emailVorlagenListeData{
Title: "E-Mail-Vorlagen (Plattform-Standard)", Nav: navFor(r),
Gespeichert: r.URL.Query().Get("gespeichert") == "1",
ActionBase: "/betreiber/email-vorlagen",
}
data := emailVorlagenListeData{Title: "E-Mail-Vorlagen (Plattform-Standard)", Nav: navFor(r), ActionBase: "/betreiber/email-vorlagen"}
for _, t := range emailVorlageTypen {
v, err := s.store.GetEmailVorlage(r.Context(), nil, t.Typ)
if err != nil {
http.Error(w, "E-Mail-Vorlagen konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data.Zeilen = append(data.Zeilen, emailVorlageZeile{
Typ: t.Typ, Label: t.Label, Platzhalter: t.Platzhalter,
Betreff: v.Betreff, Text: v.Text,
})
data.Zeilen = append(data.Zeilen, emailVorlageZeile{Typ: t.Typ, Label: t.Label})
}
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
}
}
func (s *Server) handleBetreiberEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
func (s *Server) handleBetreiberEmailVorlageBearbeitenForm(w http.ResponseWriter, r *http.Request) {
typ := r.PathValue("typ")
info, ok := findEmailVorlageTyp(typ)
if !ok {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
return
}
typ := r.FormValue("typ")
if !istGueltigerEmailVorlageTyp(typ) {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
v, err := s.store.GetEmailVorlage(r.Context(), nil, typ)
if err != nil {
http.Error(w, "E-Mail-Vorlage konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
return
}
data := emailVorlageBearbeitenData{
Title: info.Label, Nav: navFor(r), Typ: typ, Label: info.Label, Platzhalter: info.Platzhalter,
Betreff: v.Betreff, Text: v.Text, ActionBase: "/betreiber/email-vorlagen",
Gespeichert: r.URL.Query().Get("gespeichert") == "1",
}
if err := s.templates.ExecuteTemplate(w, "email-vorlage-bearbeiten", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
}
}
func (s *Server) handleBetreiberEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
typ := r.PathValue("typ")
if _, ok := findEmailVorlageTyp(typ); !ok {
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
return
}
betreff := strings.TrimSpace(r.FormValue("betreff"))
@@ -174,5 +203,5 @@ func (s *Server) handleBetreiberEmailVorlageSpeichern(w http.ResponseWriter, r *
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/betreiber/email-vorlagen?gespeichert=1", http.StatusSeeOther)
http.Redirect(w, r, "/betreiber/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
}