feat(admin): eigene Vorlage fuer die Willkommensmail mit Platzhaltern, Vorschau und Testmail
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 1m22s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m10s

Administrator -> Willkommensmail: Betreff, Ueberschrift, Einleitung, Abschluss je
Mandant (Tabelle WelcomeMailTemplate, RLS je Mandant, Migration 20260930150000);
Platzhalter {{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}},
unbekannte -> 400 bzw. Hinweis beim Tippen; Werte escaped, Vorlage reiner Text.
Live-Vorschau per API gerendert, Testmail an die eigene Adresse ohne Token,
Zuruecksetzen auf Standard. Feste Bausteine (Kopf, Zugangsdaten, Anmeldehinweis,
Knoepfe, Fusszeile) bleiben immer drin.
Kopf: Wellenzelle dunkel statt weiss, Streifen 600x40, Inhalt 24 px naeher –
keine weisse Luecke, wenn OWA das CID-Bild nicht zeigt.
Lokal nachgewiesen: Hinweis/Sperre bei {{xyz}}, Speichern, Testmail (Link nur
/login), echte Mail mit eigener Vorlage und 7-Tage-Link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:49:17 +02:00
parent 31d514b7ca
commit e10da76259
27 changed files with 2471 additions and 82 deletions
+77
View File
@@ -288,3 +288,80 @@ export const CUSTOM_MODULE_CATEGORY = "custom-modules" as const;
export const CUSTOM_MODULE_CATEGORIES = [...MODULE_CATEGORIES, CUSTOM_MODULE_CATEGORY] as const;
export type CustomModuleCategory = (typeof CUSTOM_MODULE_CATEGORIES)[number];
/**
* Eigene Vorlage der Willkommensmail (Administrator → Willkommensmail).
*
* Eine Stelle fuer API und Oberflaeche: welche Platzhalter es gibt, wie lang
* die vier Textfelder sein duerfen, wie die Standardtexte lauten (Vorbelegung
* im Formular, "Auf Standard zuruecksetzen", Versand ohne eigene Vorlage) und
* wie unbekannte Platzhalter erkannt werden. Die API lehnt eine Vorlage mit
* unbekanntem Platzhalter mit 400 ab; das Formular benennt ihn schon vorher.
*
* Die festen Bausteine der Mail (Kopf, Kasten Adresse/Benutzername,
* Anmeldehinweis mit "Passwort festlegen", Knopf "Zu Tessera", Fusszeile)
* sind KEIN Teil der Vorlage — so kann eine eigene Vorlage die Anmeldung
* nicht kaputt machen.
*/
export const WELCOME_MAIL_PLACEHOLDERS = [
'name',
'vorname',
'benutzername',
'email',
'adresse',
'firma',
] as const;
export type WelcomeMailPlaceholder = (typeof WELCOME_MAIL_PLACEHOLDERS)[number];
/** Die vier bearbeitbaren Texte der Willkommensmail. */
export interface WelcomeMailTexts {
subject: string;
heading: string;
intro: string;
closing: string;
}
/** Hoechstlaengen je Feld (Zeichen). */
export const WELCOME_MAIL_LIMITS: Readonly<Record<keyof WelcomeMailTexts, number>> = {
subject: 200,
heading: 200,
intro: 4000,
closing: 4000,
};
/**
* Standardtexte — entsprechen der Willkommensmail vor der eigenen Vorlage.
* Leerzeile = neuer Absatz, einfacher Zeilenumbruch = Umbruch im Absatz.
*/
export const DEFAULT_WELCOME_MAIL_TEXTS: Readonly<WelcomeMailTexts> = {
subject: 'Willkommen bei Tessera',
heading: 'Willkommen bei Tessera, {{name}}!',
intro:
'Für Sie wurde ein Zugang zu Tessera eingerichtet – Ihrer zentralen Plattform für Werkzeuge und Abläufe im Unternehmen. Alles, was Sie für Ihre tägliche Arbeit brauchen, finden Sie dort an einem Ort.',
closing:
'Tipp: Tessera gibt es auch als Desktop-App – den Download finden Sie auf der Anmeldeseite.\n\nViel Erfolg mit Tessera!',
};
/** Findet `{{ … }}`; der Name ohne Leerraum steht in Gruppe 1. */
export const WELCOME_MAIL_PLACEHOLDER_RE = /\{\{\s*([^{}]*?)\s*\}\}/g;
/** Ob `name` (Gross-/Kleinschreibung egal) ein bekannter Platzhalter ist. */
export function isWelcomeMailPlaceholder(name: string): name is WelcomeMailPlaceholder {
return (WELCOME_MAIL_PLACEHOLDERS as readonly string[]).includes(name.toLowerCase());
}
/**
* Alle unbekannten Platzhalter eines Textes in der Form `{{xyz}}`, ohne
* Doppelte, in Reihenfolge des ersten Auftretens.
*/
export function findUnknownWelcomeMailPlaceholders(text: string): string[] {
const unknown: string[] = [];
for (const match of text.matchAll(WELCOME_MAIL_PLACEHOLDER_RE)) {
const token = `{{${match[1]}}}`;
if (!isWelcomeMailPlaceholder(match[1]) && !unknown.includes(token)) {
unknown.push(token);
}
}
return unknown;
}