// 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) }