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:
12
CLAUDE.md
12
CLAUDE.md
@@ -640,9 +640,15 @@ sein — für E-Mails, die an die eigenen Mitarbeiter einer Firma gehen,
|
|||||||
durch den Mandanten selbst; für E-Mails, die die Plattform an
|
durch den Mandanten selbst; für E-Mails, die die Plattform an
|
||||||
Mandanten-Admins schickt, durch den Betreiber. Zweistufig wie der
|
Mandanten-Admins schickt, durch den Betreiber. Zweistufig wie der
|
||||||
Werkzeugkatalog: `email_vorlage.account_id` NULL = plattformweiter
|
Werkzeugkatalog: `email_vorlage.account_id` NULL = plattformweiter
|
||||||
Standard (`GET/POST /betreiber/email-vorlagen`, nur Betreiber), gesetzt
|
Standard (`/betreiber/email-vorlagen`, nur Betreiber), gesetzt =
|
||||||
= mandantenspezifische Übersteuerung (`GET/POST /verwaltung/email-
|
mandantenspezifische Übersteuerung (`/verwaltung/email-vorlagen`, nur
|
||||||
vorlagen`, nur Admin). `ResolveEmailVorlage` liefert die eigene
|
Admin) — jeweils ein eigener Nav-Punkt (nicht als weiterer Button auf
|
||||||
|
einer bestehenden Seite), mit Liste + eigener Bearbeiten-Seite je Typ
|
||||||
|
(`GET .../email-vorlagen` Liste, `GET/POST .../email-vorlagen/{typ}`
|
||||||
|
Bearbeiten) statt eines einzigen, mit allen Formularen gestapelten
|
||||||
|
Screens — auf ausdrücklichen Nutzerwunsch, da mit der Zeit weitere
|
||||||
|
Benachrichtigungstypen dazukommen sollen und eine gestapelte Liste dann
|
||||||
|
unübersichtlich würde. `ResolveEmailVorlage` liefert die eigene
|
||||||
Übersteuerung, falls vorhanden, sonst den Plattform-Standard — ein
|
Übersteuerung, falls vorhanden, sonst den Plattform-Standard — ein
|
||||||
Mandant, der nie etwas anpasst, bekommt automatisch jede künftige
|
Mandant, der nie etwas anpasst, bekommt automatisch jede künftige
|
||||||
Änderung des Plattform-Standards. `emailVorlageTypen` in
|
Änderung des Plattform-Standards. `emailVorlageTypen` in
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
// (/betreiber/email-vorlagen), jeder Mandant kann ihn für sich
|
// (/betreiber/email-vorlagen), jeder Mandant kann ihn für sich
|
||||||
// übersteuern (/verwaltung/email-vorlagen). emailVorlageTypen ist die
|
// übersteuern (/verwaltung/email-vorlagen). emailVorlageTypen ist die
|
||||||
// feste, bekannte Liste der vom System versendeten E-Mails — aktuell
|
// 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
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -28,13 +31,13 @@ var emailVorlageTypen = []emailVorlageTypInfo{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func istGueltigerEmailVorlageTyp(typ string) bool {
|
func findEmailVorlageTyp(typ string) (emailVorlageTypInfo, bool) {
|
||||||
for _, t := range emailVorlageTypen {
|
for _, t := range emailVorlageTypen {
|
||||||
if t.Typ == typ {
|
if t.Typ == typ {
|
||||||
return true
|
return t, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return emailVorlageTypInfo{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderEmailVorlage ersetzt die bekannten Platzhalter im Text einer
|
// renderEmailVorlage ersetzt die bekannten Platzhalter im Text einer
|
||||||
@@ -48,9 +51,6 @@ func renderEmailVorlage(text, link string) string {
|
|||||||
type emailVorlageZeile struct {
|
type emailVorlageZeile struct {
|
||||||
Typ string
|
Typ string
|
||||||
Label string
|
Label string
|
||||||
Platzhalter string
|
|
||||||
Betreff string
|
|
||||||
Text string
|
|
||||||
IstUebersteuert bool // eigene Vorlage vorhanden statt Plattform-Standard geerbt
|
IstUebersteuert bool // eigene Vorlage vorhanden statt Plattform-Standard geerbt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,44 +58,67 @@ type emailVorlagenListeData struct {
|
|||||||
Title string
|
Title string
|
||||||
Nav navData
|
Nav navData
|
||||||
Zeilen []emailVorlageZeile
|
Zeilen []emailVorlageZeile
|
||||||
Error string
|
|
||||||
Gespeichert bool
|
|
||||||
ActionBase string // unterscheidet Mandant- von Betreiber-Ziel-URL im gemeinsamen Template
|
ActionBase string // unterscheidet Mandant- von Betreiber-Ziel-URL im gemeinsamen Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
|
||||||
accountID := currentUser(r).AccountID
|
accountID := currentUser(r).AccountID
|
||||||
data := emailVorlagenListeData{
|
data := emailVorlagenListeData{Title: "E-Mail-Vorlagen", Nav: navFor(r), ActionBase: "/verwaltung/email-vorlagen"}
|
||||||
Title: "E-Mail-Vorlagen", Nav: navFor(r),
|
|
||||||
Gespeichert: r.URL.Query().Get("gespeichert") == "1",
|
|
||||||
ActionBase: "/verwaltung/email-vorlagen",
|
|
||||||
}
|
|
||||||
for _, t := range emailVorlageTypen {
|
for _, t := range emailVorlageTypen {
|
||||||
resolved, err := s.store.ResolveEmailVorlage(r.Context(), accountID, t.Typ)
|
_, err := s.store.GetEmailVorlage(r.Context(), &accountID, t.Typ)
|
||||||
if err != nil {
|
data.Zeilen = append(data.Zeilen, emailVorlageZeile{Typ: t.Typ, Label: t.Label, IstUebersteuert: 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
|
||||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
|
type emailVorlageBearbeitenData struct {
|
||||||
if err := r.ParseForm(); err != nil {
|
Title string
|
||||||
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
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
|
return
|
||||||
}
|
}
|
||||||
typ := r.FormValue("typ")
|
accountID := currentUser(r).AccountID
|
||||||
if !istGueltigerEmailVorlageTyp(typ) {
|
resolved, err := s.store.ResolveEmailVorlage(r.Context(), accountID, typ)
|
||||||
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
|
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
|
return
|
||||||
}
|
}
|
||||||
accountID := currentUser(r).AccountID
|
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)
|
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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) {
|
func (s *Server) handleEmailVorlageZuruecksetzen(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
typ := r.PathValue("typ")
|
||||||
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
if _, ok := findEmailVorlageTyp(typ); !ok {
|
||||||
return
|
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
|
||||||
}
|
|
||||||
typ := r.FormValue("typ")
|
|
||||||
if !istGueltigerEmailVorlageTyp(typ) {
|
|
||||||
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
accountID := currentUser(r).AccountID
|
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)
|
http.Error(w, "Zurücksetzen fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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 ─────────────────────
|
// ─── Betreiber (Ebene 5): plattformweiter Standard ─────────────────────
|
||||||
|
|
||||||
func (s *Server) handleBetreiberEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleBetreiberEmailVorlagenListe(w http.ResponseWriter, r *http.Request) {
|
||||||
data := emailVorlagenListeData{
|
data := emailVorlagenListeData{Title: "E-Mail-Vorlagen (Plattform-Standard)", Nav: navFor(r), ActionBase: "/betreiber/email-vorlagen"}
|
||||||
Title: "E-Mail-Vorlagen (Plattform-Standard)", Nav: navFor(r),
|
|
||||||
Gespeichert: r.URL.Query().Get("gespeichert") == "1",
|
|
||||||
ActionBase: "/betreiber/email-vorlagen",
|
|
||||||
}
|
|
||||||
for _, t := range emailVorlageTypen {
|
for _, t := range emailVorlageTypen {
|
||||||
v, err := s.store.GetEmailVorlage(r.Context(), nil, t.Typ)
|
data.Zeilen = append(data.Zeilen, emailVorlageZeile{Typ: t.Typ, Label: t.Label})
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
|
if err := s.templates.ExecuteTemplate(w, "email-vorlagen-liste", data); err != nil {
|
||||||
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleBetreiberEmailVorlageSpeichern(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleBetreiberEmailVorlageBearbeitenForm(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.ParseForm(); err != nil {
|
typ := r.PathValue("typ")
|
||||||
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
|
info, ok := findEmailVorlageTyp(typ)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
typ := r.FormValue("typ")
|
v, err := s.store.GetEmailVorlage(r.Context(), nil, typ)
|
||||||
if !istGueltigerEmailVorlageTyp(typ) {
|
if err != nil {
|
||||||
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusBadRequest)
|
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
|
return
|
||||||
}
|
}
|
||||||
betreff := strings.TrimSpace(r.FormValue("betreff"))
|
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)
|
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/betreiber/email-vorlagen?gespeichert=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/betreiber/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,22 +13,30 @@ func TestAdminKannEigeneEmailVorlageSpeichernUndZuruecksetzen(t *testing.T) {
|
|||||||
s := newServer(t, fs)
|
s := newServer(t, fs)
|
||||||
cookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
cookie := seedAccountWithRole(t, fs, "Test-Mandant", "admin@example.com", "admin")
|
||||||
|
|
||||||
getResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen")
|
listResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen")
|
||||||
|
if listResp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("Liste-Status = %d, want 200", listResp.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(listResp.Body.String(), "/verwaltung/email-vorlagen/passwort_zuruecksetzen") {
|
||||||
|
t.Fatalf("erwartet Link zur Bearbeiten-Seite, body: %s", listResp.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
getResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen")
|
||||||
if getResp.Code != http.StatusOK {
|
if getResp.Code != http.StatusOK {
|
||||||
t.Fatalf("GET status = %d, want 200", getResp.Code)
|
t.Fatalf("GET Bearbeiten-Seite status = %d, want 200", getResp.Code)
|
||||||
}
|
}
|
||||||
if !strings.Contains(getResp.Body.String(), "Plattform-Standard, nicht angepasst") {
|
if !strings.Contains(getResp.Body.String(), "Plattform-Standard, nicht angepasst") {
|
||||||
t.Fatalf("erwartet Hinweis auf geerbten Plattform-Standard, body: %s", getResp.Body.String())
|
t.Fatalf("erwartet Hinweis auf geerbten Plattform-Standard, body: %s", getResp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
saveResp := postForm(t, s, cookie, "/verwaltung/email-vorlagen", url.Values{
|
saveResp := postForm(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen", url.Values{
|
||||||
"typ": {"passwort_zuruecksetzen"}, "betreff": {"Mein eigener Betreff"}, "text": {"Mein eigener Text {{link}}"},
|
"betreff": {"Mein eigener Betreff"}, "text": {"Mein eigener Text {{link}}"},
|
||||||
})
|
})
|
||||||
if saveResp.Code != http.StatusSeeOther {
|
if saveResp.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("save status = %d, want 303, body: %s", saveResp.Code, saveResp.Body.String())
|
t.Fatalf("save status = %d, want 303, body: %s", saveResp.Code, saveResp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
afterResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen")
|
afterResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen")
|
||||||
body := afterResp.Body.String()
|
body := afterResp.Body.String()
|
||||||
if !strings.Contains(body, "Mein eigener Betreff") {
|
if !strings.Contains(body, "Mein eigener Betreff") {
|
||||||
t.Fatalf("gespeicherter Betreff fehlt, body: %s", body)
|
t.Fatalf("gespeicherter Betreff fehlt, body: %s", body)
|
||||||
@@ -37,11 +45,11 @@ func TestAdminKannEigeneEmailVorlageSpeichernUndZuruecksetzen(t *testing.T) {
|
|||||||
t.Fatalf("erwartet Hinweis auf eigene Vorlage, body: %s", body)
|
t.Fatalf("erwartet Hinweis auf eigene Vorlage, body: %s", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
resetResp := postForm(t, s, cookie, "/verwaltung/email-vorlagen/zuruecksetzen", url.Values{"typ": {"passwort_zuruecksetzen"}})
|
resetResp := postForm(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen/zuruecksetzen", url.Values{})
|
||||||
if resetResp.Code != http.StatusSeeOther {
|
if resetResp.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("reset status = %d, want 303", resetResp.Code)
|
t.Fatalf("reset status = %d, want 303", resetResp.Code)
|
||||||
}
|
}
|
||||||
finalResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen")
|
finalResp := getWithCookie(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen")
|
||||||
if strings.Contains(finalResp.Body.String(), "Mein eigener Betreff") {
|
if strings.Contains(finalResp.Body.String(), "Mein eigener Betreff") {
|
||||||
t.Fatal("nach Zurücksetzen ist die eigene Vorlage immer noch aktiv")
|
t.Fatal("nach Zurücksetzen ist die eigene Vorlage immer noch aktiv")
|
||||||
}
|
}
|
||||||
@@ -52,14 +60,14 @@ func TestBetreiberKannPlattformStandardBearbeiten(t *testing.T) {
|
|||||||
s := newServer(t, fs)
|
s := newServer(t, fs)
|
||||||
cookie := seedAccountWithRole(t, fs, "Betreiber-Firma", "betreiber@example.com", "betreiber")
|
cookie := seedAccountWithRole(t, fs, "Betreiber-Firma", "betreiber@example.com", "betreiber")
|
||||||
|
|
||||||
saveResp := postForm(t, s, cookie, "/betreiber/email-vorlagen", url.Values{
|
saveResp := postForm(t, s, cookie, "/betreiber/email-vorlagen/passwort_zuruecksetzen", url.Values{
|
||||||
"typ": {"passwort_zuruecksetzen"}, "betreff": {"Neuer Plattform-Betreff"}, "text": {"Neuer Plattform-Text {{link}}"},
|
"betreff": {"Neuer Plattform-Betreff"}, "text": {"Neuer Plattform-Text {{link}}"},
|
||||||
})
|
})
|
||||||
if saveResp.Code != http.StatusSeeOther {
|
if saveResp.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("save status = %d, want 303, body: %s", saveResp.Code, saveResp.Body.String())
|
t.Fatalf("save status = %d, want 303, body: %s", saveResp.Code, saveResp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
getResp := getWithCookie(t, s, cookie, "/betreiber/email-vorlagen")
|
getResp := getWithCookie(t, s, cookie, "/betreiber/email-vorlagen/passwort_zuruecksetzen")
|
||||||
if !strings.Contains(getResp.Body.String(), "Neuer Plattform-Betreff") {
|
if !strings.Contains(getResp.Body.String(), "Neuer Plattform-Betreff") {
|
||||||
t.Fatalf("neuer Plattform-Standard fehlt, body: %s", getResp.Body.String())
|
t.Fatalf("neuer Plattform-Standard fehlt, body: %s", getResp.Body.String())
|
||||||
}
|
}
|
||||||
@@ -81,8 +89,8 @@ func TestPasswortVergessenNutztMandantenspezifischeVorlage(t *testing.T) {
|
|||||||
s, fm := newServerWithMailer(t, fs)
|
s, fm := newServerWithMailer(t, fs)
|
||||||
cookie := seedAccountWithRole(t, fs, "Vorlage-Firma", "vorlage-admin@example.com", "admin")
|
cookie := seedAccountWithRole(t, fs, "Vorlage-Firma", "vorlage-admin@example.com", "admin")
|
||||||
|
|
||||||
if resp := postForm(t, s, cookie, "/verwaltung/email-vorlagen", url.Values{
|
if resp := postForm(t, s, cookie, "/verwaltung/email-vorlagen/passwort_zuruecksetzen", url.Values{
|
||||||
"typ": {"passwort_zuruecksetzen"}, "betreff": {"Firmenspezifischer Betreff"}, "text": {"Hallo! Link: {{link}} Ende."},
|
"betreff": {"Firmenspezifischer Betreff"}, "text": {"Hallo! Link: {{link}} Ende."},
|
||||||
}); resp.Code != http.StatusSeeOther {
|
}); resp.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("save status = %d, want 303", resp.Code)
|
t.Fatalf("save status = %d, want 303", resp.Code)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,10 +167,12 @@ func NewServer(st Store, regelwerk Regelwerk, mailer mail.Mailer) (*Server, erro
|
|||||||
mux.HandleFunc("GET /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenListe))
|
mux.HandleFunc("GET /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenListe))
|
||||||
mux.HandleFunc("POST /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenSpeichern))
|
mux.HandleFunc("POST /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenSpeichern))
|
||||||
mux.HandleFunc("GET /verwaltung/email-vorlagen", s.requireAdmin(s.handleEmailVorlagenListe))
|
mux.HandleFunc("GET /verwaltung/email-vorlagen", s.requireAdmin(s.handleEmailVorlagenListe))
|
||||||
mux.HandleFunc("POST /verwaltung/email-vorlagen", s.requireAdmin(s.handleEmailVorlageSpeichern))
|
mux.HandleFunc("GET /verwaltung/email-vorlagen/{typ}", s.requireAdmin(s.handleEmailVorlageBearbeitenForm))
|
||||||
mux.HandleFunc("POST /verwaltung/email-vorlagen/zuruecksetzen", s.requireAdmin(s.handleEmailVorlageZuruecksetzen))
|
mux.HandleFunc("POST /verwaltung/email-vorlagen/{typ}", s.requireAdmin(s.handleEmailVorlageSpeichern))
|
||||||
|
mux.HandleFunc("POST /verwaltung/email-vorlagen/{typ}/zuruecksetzen", s.requireAdmin(s.handleEmailVorlageZuruecksetzen))
|
||||||
mux.HandleFunc("GET /betreiber/email-vorlagen", s.requireBetreiber(s.handleBetreiberEmailVorlagenListe))
|
mux.HandleFunc("GET /betreiber/email-vorlagen", s.requireBetreiber(s.handleBetreiberEmailVorlagenListe))
|
||||||
mux.HandleFunc("POST /betreiber/email-vorlagen", s.requireBetreiber(s.handleBetreiberEmailVorlageSpeichern))
|
mux.HandleFunc("GET /betreiber/email-vorlagen/{typ}", s.requireBetreiber(s.handleBetreiberEmailVorlageBearbeitenForm))
|
||||||
|
mux.HandleFunc("POST /betreiber/email-vorlagen/{typ}", s.requireBetreiber(s.handleBetreiberEmailVorlageSpeichern))
|
||||||
mux.HandleFunc("GET /passwort-vergessen", s.handlePasswortVergessenForm)
|
mux.HandleFunc("GET /passwort-vergessen", s.handlePasswortVergessenForm)
|
||||||
mux.HandleFunc("POST /passwort-vergessen", s.handlePasswortVergessen)
|
mux.HandleFunc("POST /passwort-vergessen", s.handlePasswortVergessen)
|
||||||
mux.HandleFunc("GET /passwort-zuruecksetzen/{token}", s.handlePasswortZuruecksetzenForm)
|
mux.HandleFunc("GET /passwort-zuruecksetzen/{token}", s.handlePasswortZuruecksetzenForm)
|
||||||
|
|||||||
49
internal/web/templates/email_vorlage_bearbeiten.html
Normal file
49
internal/web/templates/email_vorlage_bearbeiten.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{{define "email-vorlage-bearbeiten"}}<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>{{template "head" .}}</head>
|
||||||
|
<body>
|
||||||
|
{{template "nav" .Nav}}
|
||||||
|
<div class="page page-wide">
|
||||||
|
<p><a href="{{.ActionBase}}">← E-Mail-Vorlagen</a></p>
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>{{.Label}}</h1>
|
||||||
|
<p class="hinweis">
|
||||||
|
Platzhalter in geschweiften Klammern werden beim Versand ersetzt.
|
||||||
|
Verfügbar: {{.Platzhalter}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
|
||||||
|
{{if .Gespeichert}}<p class="hinweis">Gespeichert.</p>{{end}}
|
||||||
|
<div class="form-card">
|
||||||
|
<form method="post" action="{{.ActionBase}}/{{.Typ}}">
|
||||||
|
<fieldset class="form-section">
|
||||||
|
<legend>{{.Label}}{{if .IstUebersteuert}} <span class="hinweis">(eigene Vorlage)</span>{{else if eq .ActionBase "/verwaltung/email-vorlagen"}} <span class="hinweis">(Plattform-Standard, nicht angepasst)</span>{{end}}</legend>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-full">
|
||||||
|
<label for="betreff">Betreff</label>
|
||||||
|
<input type="text" id="betreff" name="betreff" value="{{.Betreff}}" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-full">
|
||||||
|
<label for="text">Text</label>
|
||||||
|
<textarea id="text" name="text" rows="10" required>{{.Text}}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{if and .IstUebersteuert (eq .ActionBase "/verwaltung/email-vorlagen")}}
|
||||||
|
<div class="form-gefahrenzone">
|
||||||
|
<form method="post" action="/verwaltung/email-vorlagen/{{.Typ}}/zuruecksetzen" onsubmit="return confirm('Eigene Vorlage löschen und zum Plattform-Standard zurückkehren?')">
|
||||||
|
<button type="submit" class="entfernen">Auf Plattform-Standard zurücksetzen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
@@ -7,46 +7,22 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>{{.Title}}</h1>
|
<h1>{{.Title}}</h1>
|
||||||
<p class="hinweis">
|
<p class="hinweis">Betreff und Text der vom System versendeten E-Mails.</p>
|
||||||
Betreff und Text der vom System versendeten E-Mails. Platzhalter
|
|
||||||
in geschweiften Klammern (z. B. <code>{{"{{link}}"}}</code>)
|
|
||||||
werden beim Versand automatisch ersetzt.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
|
{{if not .Zeilen}}
|
||||||
{{if .Gespeichert}}<p class="hinweis">Gespeichert.</p>{{end}}
|
<p class="hinweis">Es gibt noch keine vom System versendeten E-Mails.</p>
|
||||||
{{range .Zeilen}}
|
{{else}}
|
||||||
<div class="form-card">
|
<ul class="beitraege-liste">
|
||||||
<form method="post" action="{{$.ActionBase}}">
|
{{range .Zeilen}}
|
||||||
<input type="hidden" name="typ" value="{{.Typ}}">
|
<li>
|
||||||
<fieldset class="form-section">
|
<a href="{{$.ActionBase}}/{{.Typ}}">
|
||||||
<legend>{{.Label}}{{if .IstUebersteuert}} <span class="hinweis">(eigene Vorlage)</span>{{else if eq $.ActionBase "/verwaltung/email-vorlagen"}} <span class="hinweis">(Plattform-Standard, nicht angepasst)</span>{{end}}</legend>
|
<strong>{{.Label}}</strong>
|
||||||
<p class="hinweis">Verfügbare Platzhalter: {{.Platzhalter}}</p>
|
{{if .IstUebersteuert}}<span class="status status-published">eigene Vorlage</span>{{else}}<span class="status status-mittel">Plattform-Standard</span>{{end}}
|
||||||
<div class="form-grid">
|
</a>
|
||||||
<div class="form-full">
|
</li>
|
||||||
<label for="betreff-{{.Typ}}">Betreff</label>
|
{{end}}
|
||||||
<input type="text" id="betreff-{{.Typ}}" name="betreff" value="{{.Betreff}}" required>
|
</ul>
|
||||||
</div>
|
|
||||||
<div class="form-full">
|
|
||||||
<label for="text-{{.Typ}}">Text</label>
|
|
||||||
<textarea id="text-{{.Typ}}" name="text" rows="8" required>{{.Text}}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{if and .IstUebersteuert (eq $.ActionBase "/verwaltung/email-vorlagen")}}
|
|
||||||
<div class="form-gefahrenzone">
|
|
||||||
<form method="post" action="/verwaltung/email-vorlagen/zuruecksetzen" onsubmit="return confirm('Eigene Vorlage löschen und zum Plattform-Standard zurückkehren?')">
|
|
||||||
<input type="hidden" name="typ" value="{{.Typ}}">
|
|
||||||
<button type="submit" class="entfernen">Auf Plattform-Standard zurücksetzen</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user