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>
208 lines
7.7 KiB
Go
208 lines
7.7 KiB
Go
// E-Mail-Vorlagen (Migration 0022) — zweistufig wie der Werkzeugkatalog:
|
|
// der Betreiber pflegt den plattformweiten Standard
|
|
// (/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,
|
|
// 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 (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
type emailVorlageTypInfo struct {
|
|
Typ string
|
|
Label string
|
|
Platzhalter string
|
|
}
|
|
|
|
var emailVorlageTypen = []emailVorlageTypInfo{
|
|
{
|
|
Typ: "passwort_zuruecksetzen",
|
|
Label: "Passwort zurücksetzen",
|
|
Platzhalter: "{{link}} — der Zurücksetzen-Link, wird beim Versand eingesetzt.",
|
|
},
|
|
}
|
|
|
|
func findEmailVorlageTyp(typ string) (emailVorlageTypInfo, bool) {
|
|
for _, t := range emailVorlageTypen {
|
|
if t.Typ == typ {
|
|
return t, true
|
|
}
|
|
}
|
|
return emailVorlageTypInfo{}, false
|
|
}
|
|
|
|
// renderEmailVorlage ersetzt die bekannten Platzhalter im Text einer
|
|
// Vorlage — aktuell nur {{link}} (Passwort-Zurücksetzen-Link).
|
|
func renderEmailVorlage(text, link string) string {
|
|
return strings.ReplaceAll(text, "{{link}}", link)
|
|
}
|
|
|
|
// ─── Mandant (Ebene 4, admin-only): eigene Übersteuerung ───────────────
|
|
|
|
type emailVorlageZeile struct {
|
|
Typ string
|
|
Label string
|
|
IstUebersteuert bool // eigene Vorlage vorhanden statt Plattform-Standard geerbt
|
|
}
|
|
|
|
type emailVorlagenListeData struct {
|
|
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), ActionBase: "/verwaltung/email-vorlagen"}
|
|
for _, t := range emailVorlageTypen {
|
|
_, 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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
betreff := strings.TrimSpace(r.FormValue("betreff"))
|
|
text := strings.TrimSpace(r.FormValue("text"))
|
|
if betreff == "" || text == "" {
|
|
http.Error(w, "Betreff und Text sind Pflicht", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := s.store.UpsertEmailVorlage(r.Context(), &accountID, typ, betreff, text); err != nil {
|
|
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/verwaltung/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
|
|
}
|
|
|
|
func (s *Server) handleEmailVorlageZuruecksetzen(w http.ResponseWriter, r *http.Request) {
|
|
typ := r.PathValue("typ")
|
|
if _, ok := findEmailVorlageTyp(typ); !ok {
|
|
http.Error(w, "unbekannter Vorlagen-Typ", http.StatusNotFound)
|
|
return
|
|
}
|
|
accountID := currentUser(r).AccountID
|
|
if err := s.store.DeleteEmailVorlage(r.Context(), &accountID, typ); err != nil && !errors.Is(err, store.ErrNotFound) {
|
|
http.Error(w, "Zurücksetzen fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
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), ActionBase: "/betreiber/email-vorlagen"}
|
|
for _, t := range emailVorlageTypen {
|
|
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) 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
|
|
}
|
|
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"))
|
|
text := strings.TrimSpace(r.FormValue("text"))
|
|
if betreff == "" || text == "" {
|
|
http.Error(w, "Betreff und Text sind Pflicht", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := s.store.UpsertEmailVorlage(r.Context(), nil, typ, betreff, text); err != nil {
|
|
http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
http.Redirect(w, r, "/betreiber/email-vorlagen/"+typ+"?gespeichert=1", http.StatusSeeOther)
|
|
}
|