// 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. 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 istGueltigerEmailVorlageTyp(typ string) bool { for _, t := range emailVorlageTypen { if t.Typ == typ { return true } } return 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 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 } 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", } 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, }) } 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) return } typ := r.FormValue("typ") if !istGueltigerEmailVorlageTyp(typ) { http.Error(w, "unbekannter Vorlagen-Typ", 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?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) 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?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", } 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, }) } 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) return } typ := r.FormValue("typ") if !istGueltigerEmailVorlageTyp(typ) { http.Error(w, "unbekannter Vorlagen-Typ", 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?gespeichert=1", http.StatusSeeOther) }