diff --git a/server/mail-templates/G01 Termin bestaetigen.html b/server/mail-templates/G01 Termin bestaetigen.html
new file mode 100644
index 0000000..3653085
--- /dev/null
+++ b/server/mail-templates/G01 Termin bestaetigen.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Bitte bestätigen Sie Ihren TerminBitte bestätige deinen Termin |
+Guten Tag, bei {{betrieb_name}} wurde mit dieser E-Mail-Adresse ein Termin ausgewählt. Bitte bestätigen Sie ihn mit dem Knopf unten. Der Termin ist nur kurze Zeit für Sie reserviert.Hallo, bei {{betrieb_name}} wurde mit dieser E-Mail-Adresse ein Termin ausgewählt. Bitte bestätige ihn mit dem Knopf unten. Der Termin ist nur kurze Zeit für dich reserviert. |
+
+| Wichtiger Hinweis: Zu dieser E-Mail-Adresse gibt es bei {{betrieb_name}} bereits die höchstmögliche Zahl offener Termine. Der Termin unten wurde deshalb nicht für Sie reserviert, den Knopf können Sie ignorieren. Bitte sagen Sie einen Ihrer offenen Termine ab (Link in der jeweiligen Bestätigungs-E-Mail) oder rufen Sie uns an: {{betrieb_telefon}}.Wichtiger Hinweis: Zu dieser E-Mail-Adresse gibt es bei {{betrieb_name}} bereits die höchstmögliche Zahl offener Termine. Der Termin unten wurde deshalb nicht für dich reserviert, den Knopf kannst du ignorieren. Bitte sage einen deiner offenen Termine ab (Link in der jeweiligen Bestätigungs-E-Mail) oder ruf uns an: {{betrieb_telefon}}. |
|
+
+
+| Leistung | {{leistung}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+ |
+ |
+| Der Knopf öffnet die Bestätigungsseite. Dort sehen Sie noch einmal alle Angaben.Der Knopf öffnet die Bestätigungsseite. Dort siehst du noch einmal alle Angaben. |
+| Das waren Sie nicht? Dann ignorieren Sie diese E-Mail.Das warst du nicht? Dann ignoriere diese E-Mail. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G02 Termin bestaetigt.html b/server/mail-templates/G02 Termin bestaetigt.html
new file mode 100644
index 0000000..9ec8274
--- /dev/null
+++ b/server/mail-templates/G02 Termin bestaetigt.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Ihr Termin ist bestätigtDein Termin ist bestätigt |
+Guten Tag {{gast_name}},
+{{betrieb_name}} hat Ihre Terminanfrage angenommen. Ihr Termin ist bestätigt. Hier die Zusammenfassung:Hallo {{gast_name}},
+{{betrieb_name}} hat deine Terminanfrage angenommen. Dein Termin ist bestätigt. Hier die Zusammenfassung: |
+
+| Leistung | {{leistung}} |
+
+| Bei | {{person}} |
+
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| Dauer | {{dauer}} |
+
+| Preis | {{preis}} |
+| Zahlung | {{zahlung}} |
+
+| Ort | {{ort}} |
+| Buchungsnummer | {{nummer}} |
+ |
+
+| Stornobedingungen |
+| {{storno_text}} |
+
+ |
+| Hier können Sie Ihren Termin ansehen, absagen oder verschieben, solange es die Bedingungen des Betriebs erlauben. Ein Kalendereintrag liegt dieser E-Mail bei.Hier kannst du deinen Termin ansehen, absagen oder verschieben, solange es die Bedingungen des Betriebs erlauben. Ein Kalendereintrag liegt dieser E-Mail bei. |
+
+| Widerrufsrecht. Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür können Sie jederzeit den Knopf „Vertrag widerrufen“ nutzen. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular finden Sie weiter unten in dieser E-Mail.Widerrufsrecht. Wenn du Verbraucher bist, kannst du diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür kannst du jederzeit den Knopf „Vertrag widerrufen“ nutzen. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular findest du weiter unten in dieser E-Mail. |
+ |
+| Widerrufsbelehrung und Muster-Widerrufsformular |
+| Rechtlich vorgeschriebener Wortlaut: |
+ |
+
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G03 Anfrage eingegangen.html b/server/mail-templates/G03 Anfrage eingegangen.html
new file mode 100644
index 0000000..46906c1
--- /dev/null
+++ b/server/mail-templates/G03 Anfrage eingegangen.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Ihre Terminanfrage ist eingegangenDeine Terminanfrage ist eingegangen |
+Guten Tag {{gast_name}}, vielen Dank für Ihre Anfrage. {{betrieb_name}} prüft sie und meldet sich bei Ihnen. Erst wenn der Betrieb den Termin bestätigt hat, ist er verbindlich.Hallo {{gast_name}}, danke für deine Anfrage. {{betrieb_name}} prüft sie und meldet sich bei dir. Erst wenn der Betrieb den Termin bestätigt hat, ist er verbindlich. |
+
+| Leistung | {{leistung}} |
+| Wunschtermin | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+ |
+ |
+| Möchten Sie die Anfrage zurückziehen, können Sie sie hier absagen.Willst du die Anfrage zurückziehen, kannst du sie hier absagen. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G04 Anfrage abgelehnt.html b/server/mail-templates/G04 Anfrage abgelehnt.html
new file mode 100644
index 0000000..31a31d7
--- /dev/null
+++ b/server/mail-templates/G04 Anfrage abgelehnt.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Ihre Terminanfrage wurde abgelehntDeine Terminanfrage wurde abgelehnt |
+Guten Tag {{gast_name}}, leider kann {{betrieb_name}} Ihre Terminanfrage nicht annehmen.Hallo {{gast_name}}, leider kann {{betrieb_name}} deine Terminanfrage nicht annehmen. |
+
+| Leistung | {{leistung}} |
+| Wunschtermin | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+ |
+
+| Nachricht von {{betrieb_name}} |
+ |
+
+| Bei Fragen erreichen Sie uns unter {{betrieb_telefon}}.Bei Fragen erreichst du uns unter {{betrieb_telefon}}. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G05 Erinnerung.html b/server/mail-templates/G05 Erinnerung.html
new file mode 100644
index 0000000..0d23c32
--- /dev/null
+++ b/server/mail-templates/G05 Erinnerung.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Erinnerung an Ihren TerminErinnerung an deinen Termin |
+Guten Tag {{gast_name}}, wir erinnern Sie an Ihren Termin bei {{betrieb_name}}. Sie haben diese Erinnerung bei der Buchung gewünscht.Hallo {{gast_name}}, wir erinnern dich an deinen Termin bei {{betrieb_name}}. Du hast diese Erinnerung bei der Buchung gewünscht. |
+
+| Leistung | {{leistung}} |
+
+| Bei | {{person}} |
+
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+ |
+ |
+| Können Sie nicht kommen? Dann sagen Sie den Termin bitte rechtzeitig ab.Kannst du nicht kommen? Dann sage den Termin bitte rechtzeitig ab. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G06 Termin abgesagt.html b/server/mail-templates/G06 Termin abgesagt.html
new file mode 100644
index 0000000..d7e6a2d
--- /dev/null
+++ b/server/mail-templates/G06 Termin abgesagt.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Ihr Termin wurde abgesagtDein Termin wurde abgesagt |
+Guten Tag {{gast_name}}, Ihr Termin bei {{betrieb_name}} wurde abgesagt.Hallo {{gast_name}}, dein Termin bei {{betrieb_name}} wurde abgesagt. |
+
+| Leistung | {{leistung}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| Abgesagt von | {{absage_von}} |
+ |
+
+| Grund und Nachricht |
+ |
+
+| Die beigefügte Kalenderdatei entfernt den Termin aus Ihrem Kalender.Die beigefügte Kalenderdatei entfernt den Termin aus deinem Kalender. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G07 Termin verschoben.html b/server/mail-templates/G07 Termin verschoben.html
new file mode 100644
index 0000000..3f89540
--- /dev/null
+++ b/server/mail-templates/G07 Termin verschoben.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Ihr Termin wurde verschobenDein Termin wurde verschoben |
+Guten Tag {{gast_name}}, Ihr Termin bei {{betrieb_name}} hat eine neue Zeit.Hallo {{gast_name}}, dein Termin bei {{betrieb_name}} hat eine neue Zeit. |
+
+| Leistung | {{leistung}} |
+| Bisher | {{alt_datum}}, {{alt_zeit}} Uhr |
+| Neu | {{datum}}, {{zeit}} Uhr |
+ |
+ |
+| Die beigefügte Kalenderdatei aktualisiert den Termin in Ihrem Kalender. Hier können Sie Ihren Termin auch ansehen und absagen.Die beigefügte Kalenderdatei aktualisiert den Termin in deinem Kalender. Hier kannst du deinen Termin auch ansehen und absagen. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/G08 Eingang Widerruf.html b/server/mail-templates/G08 Eingang Widerruf.html
new file mode 100644
index 0000000..e0e800c
--- /dev/null
+++ b/server/mail-templates/G08 Eingang Widerruf.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+ |
+
+
+| Eingangsbestätigung Ihres WiderrufsEingangsbestätigung deines Widerrufs |
+Guten Tag, wir bestätigen den Eingang Ihrer Widerrufserklärung: Sie haben erklärt, einen Vertrag über einen Termin bei {{betrieb_name}} zu widerrufen.Hallo, wir bestätigen den Eingang deiner Widerrufserklärung: Du hast erklärt, einen Vertrag über einen Termin bei {{betrieb_name}} zu widerrufen. |
+
+| Eingegangen am | {{eingang}} |
+ |
+
+| Ihre Erklärung betrifft die Buchung {{nummer}}. Der Termin ist abgesagt.Deine Erklärung betrifft die Buchung {{nummer}}. Der Termin ist abgesagt. |
+
+
+| Hinweis: Die Widerrufsfrist war bei Eingang Ihrer Erklärung bereits abgelaufen. {{betrieb_name}} prüft, wie es weitergeht, und meldet sich bei Ihnen.Hinweis: Die Widerrufsfrist war bei Eingang deiner Erklärung bereits abgelaufen. {{betrieb_name}} prüft, wie es weitergeht, und meldet sich bei dir. |
+
+| Falls wir Ihre Erklärung keiner Buchung zuordnen konnten, prüft {{betrieb_name}} sie und meldet sich bei Ihnen.Falls wir deine Erklärung keiner Buchung zuordnen konnten, prüft {{betrieb_name}} sie und meldet sich bei dir. |
+| Sie haben diese E-Mail erhalten, weil mit Ihrer E-Mail-Adresse ein Widerruf erklärt wurde. Waren Sie das nicht, können Sie diese Nachricht ignorieren.Du hast diese E-Mail erhalten, weil mit deiner E-Mail-Adresse ein Widerruf erklärt wurde. Warst du das nicht, kannst du diese Nachricht ignorieren. |
+Viele Grüße {{betrieb_name}} |
+
+ |
+{{betrieb_name}} {{betrieb_adresse}} {{betrieb_telefon}} |
+| Impressum · Datenschutz |
+| Diese E-Mail wurde im Auftrag von {{betrieb_name}} über den Terminplaner von WEBklar versendet. Antworten gehen direkt an {{betrieb_name}}. |
+
+ |
+
+
+
diff --git a/server/mail-templates/K20 Neue Buchung.html b/server/mail-templates/K20 Neue Buchung.html
new file mode 100644
index 0000000..90691a2
--- /dev/null
+++ b/server/mail-templates/K20 Neue Buchung.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+
+ | WEBklar |
|
+Terminplaner |
+
|
+
+
+| {{betreff}} |
+Hallo {{kunde_name}}, über Deinen Terminplaner ist ein neuer Termin eingegangen. |
+
+| Diese Anfrage ist noch nicht bestätigt. Bitte nimm sie im Portal an oder lehne sie ab. Dein Gast wartet auf Deine Antwort. |
|
+
+
+| Leistung | {{leistung}} |
+| Person oder Platz | {{person}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| Gast | {{gast_name}} |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+| Nachricht | {{gast_nachricht}} |
+ |
+ |
+Viele Grüße Dein WEBklar-Team |
+
+ |
+| Kundenportal |
+WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler Telefon 0176 23726355 · support@webklar.com Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein |
+
+ |
+
+
+
diff --git a/server/mail-templates/K21 Termin geaendert.html b/server/mail-templates/K21 Termin geaendert.html
new file mode 100644
index 0000000..df38b24
--- /dev/null
+++ b/server/mail-templates/K21 Termin geaendert.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+{{betreff}}
+
+
+
+
+
+
+
+
+ | WEBklar |
|
+Terminplaner |
+
|
+
+
+| {{betreff}} |
+| Hallo {{kunde_name}}, |
+
+| {{gast_name}} hat den Termin abgesagt. Der Zeitraum ist wieder frei. |
+
+| Leistung | {{leistung}} |
+| Person oder Platz | {{person}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+ |
+
+
+| {{gast_name}} hat den Termin verschoben. Der alte Zeitraum ist wieder frei. |
+
+| Leistung | {{leistung}} |
+| Person oder Platz | {{person}} |
+| Bisher | {{alt_datum}}, {{alt_zeit}} Uhr |
+| Neu | {{datum}}, {{zeit}} Uhr |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+ |
+
+
+| {{gast_name}} hat den Vertrag widerrufen. Der Termin ist abgesagt und der Zeitraum wieder frei. Bitte prüfe, ob schon etwas bezahlt wurde, das erstattet werden muss. |
+
+| Leistung | {{leistung}} |
+| Person oder Platz | {{person}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| Eingegangen am | {{eingang}} |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+ |
+
+
+| {{gast_name}} hat einen Widerruf erklärt. Die Widerrufsfrist war bei Eingang bereits abgelaufen. Der Termin ist unverändert, wir haben nichts abgesagt. Bitte prüfe die Erklärung und melde Dich bei Deinem Gast. |
+
+| Leistung | {{leistung}} |
+| Person oder Platz | {{person}} |
+| Datum | {{datum}} |
+| Uhrzeit | {{zeit}} Uhr |
+| Eingegangen am | {{eingang}} |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+ |
+
+
+| Über das Widerrufsformular ist eine Erklärung eingegangen, die wir keiner Buchung zuordnen konnten. Bitte prüfe die Angaben und melde Dich beim Absender. |
+
+| Eingegangen am | {{eingang}} |
+| Name | {{gast_name}} |
+| E-Mail | {{gast_email}} |
+| Telefon | {{gast_telefon}} |
+| Weitere Angaben | {{gast_nachricht}} |
+ |
+
+ |
+Viele Grüße Dein WEBklar-Team |
+
+ |
+| Kundenportal |
+WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler Telefon 0176 23726355 · support@webklar.com Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein |
+
+ |
+
+
+
diff --git a/test/mailTemplates.test.js b/test/mailTemplates.test.js
index 766f38d..e196491 100644
--- a/test/mailTemplates.test.js
+++ b/test/mailTemplates.test.js
@@ -5,27 +5,11 @@ import { escapeHtml, htmlToText, loadTemplate, renderMail, templateIds } from '.
import { contractAttachmentFor, termsDocFor } from '../server/services/products.js'
const DIR = new URL('../server/mail-templates/', import.meta.url)
-// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1);
-// Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen)
-const EXPECTED = [
- ...Array.from({ length: 11 }, (_, i) => `K${String(i + 1).padStart(2, '0')}`),
- 'K12',
- 'K13',
- 'K14',
- 'K15',
- 'K16',
- 'K17',
- 'K18',
- 'K19',
- ...Array.from({ length: 11 }, (_, i) => `T${String(i + 1).padStart(2, '0')}`),
- 'T12',
- 'T13',
- 'T14',
- 'T15',
- 'T16',
- 'T17',
- 'T18',
-]
+// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
+// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; T18 Bankzugang erneuern (Finanzen)
+const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
+const EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 18)]
+const GUEST = ids('G', 8)
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
const allBlocks = (tpl, on) => Object.fromEntries(tpl.blocks.map((b) => [b, on]))
@@ -525,3 +509,250 @@ test('T01: Knopf antwortet im Ticketsystem beim Kunden (team_link)', () => {
assert.deepEqual(mail.problems, [])
assert.match(mail.html, /]*>Im Ticketsystem antworten →<\/a>/)
})
+
+// ---------------------------------------------------------------------------
+// Terminplaner (Task 9): G01–G08 an Gäste, K20 und K21 an den Betrieb
+// ---------------------------------------------------------------------------
+
+const sourceOf = (id) => readFileSync(new URL(loadTemplate(id).file, DIR), 'utf8')
+const FOOTER_SENTENCE = 'Diese E-Mail wurde im Auftrag von Wert-betrieb_name über den Terminplaner von WEBklar versendet.'
+// Sie-Formen und Du-Formen (der Rechtstext der Widerrufsbelehrung ist ein Platzhalterwert und kommt hier nicht vor)
+const SIE_FORM = /(^|[^\p{L}])(Sie|Ihr\p{L}*|Ihnen)(?![\p{L}-])/u
+const DU_FORM = /(^|[^\p{L}])(du|dein\p{L}*|dir|dich)(?![\p{L}-])/iu
+const anrede = (tpl, sie, extra = {}) => ({ ...allBlocks(tpl, true), ...extra, sie, du: !sie })
+
+test('G-Vorlagen: Betreff ist {{betreff}}, Blöcke sie und du, Platzhalter für Farbe und Betriebsfuß', () => {
+ for (const id of GUEST) {
+ const tpl = loadTemplate(id)
+ assert.equal(tpl.subject, '{{betreff}}', id)
+ for (const block of ['sie', 'du']) assert.ok(tpl.blocks.includes(block), `${id}: Block ${block}`)
+ for (const p of ['betreff', 'farbe', 'betrieb_name', 'betrieb_adresse', 'betrieb_telefon', 'betrieb_email', 'impressum_url', 'datenschutz_url']) {
+ assert.ok(tpl.placeholders.includes(p), `${id}: ${p}`)
+ }
+ }
+})
+
+test('G-Vorlagen: neutrales Layout, Kopf in der Betriebsfarbe, Fuß mit Betrieb, Links und Satz über den Terminplaner', () => {
+ for (const id of GUEST) {
+ const tpl = loadTemplate(id)
+ const source = sourceOf(id)
+ assert.match(source, /]*style="[^"]*background:\{\{farbe\}\}/, `${id}: Kopfbalken in {{farbe}}`)
+ assert.doesNotMatch(source, / ]*>Impressum, id)
+ assert.match(mail.html, /href="Wert-datenschutz_url"[^>]*>Datenschutz, id)
+ assert.match(mail.html, /background:Wert-farbe/, id)
+ }
+ }
+})
+
+test('G-Vorlagen: jeder Anredetext steht in sie und du, Du-Mail ohne Sie-Formen und Sie-Mail ohne Du-Formen', () => {
+ for (const id of GUEST) {
+ const tpl = loadTemplate(id)
+ const du = renderMail(id, sampleValues(tpl), anrede(tpl, false))
+ const sie = renderMail(id, sampleValues(tpl), anrede(tpl, true))
+ assert.deepEqual([du.problems, sie.problems], [[], []], id)
+ assert.doesNotMatch(du.text, SIE_FORM, `${id}: Du-Fassung enthält Sie-Form\n${du.text}`)
+ assert.doesNotMatch(du.text, / Sie | Ihr /, id)
+ assert.doesNotMatch(sie.text, DU_FORM, `${id}: Sie-Fassung enthält Du-Form\n${sie.text}`)
+ assert.match(sie.text, SIE_FORM, `${id}: Sie-Fassung spricht den Gast an`)
+ assert.match(du.text, DU_FORM, `${id}: Du-Fassung spricht den Gast an`)
+ assert.notEqual(du.text, sie.text, id)
+ // ohne gesetzte Anrede erscheint kein Anredetext (der Server setzt genau einen der beiden Blöcke)
+ const neither = renderMail(id, sampleValues(tpl), { ...allBlocks(tpl, true), sie: false, du: false })
+ assert.doesNotMatch(neither.text, SIE_FORM, id)
+ assert.doesNotMatch(neither.text, DU_FORM, id)
+ }
+})
+
+test('G02: Du-Mail enthält im Text kein „ Sie “ und kein „Ihr “', () => {
+ const tpl = loadTemplate('G02')
+ const mail = renderMail('G02', sampleValues(tpl), { ...allBlocks(tpl, true), sie: false, du: true })
+ assert.deepEqual(mail.problems, [])
+ assert.doesNotMatch(mail.text, / Sie |Ihr /)
+ assert.doesNotMatch(mail.html, / Sie | Ihr /)
+})
+
+test('G01 und G08 enthalten keinen vom Gast eingegebenen Text (kein Platzhalter gast_name)', () => {
+ for (const id of ['G01', 'G08']) {
+ assert.ok(!loadTemplate(id).placeholders.includes('gast_name'), `${id}: Platzhalter`)
+ assert.ok(!sourceOf(id).includes('gast_name'), `${id}: Quelltext`)
+ }
+ // ... die anderen Gästemails nennen den Gast beim Namen
+ for (const id of ['G02', 'G03', 'G04', 'G05', 'G06', 'G07']) assert.ok(loadTemplate(id).placeholders.includes('gast_name'), id)
+})
+
+test('G-Vorlagen: Platzhalter und Blöcke wie im Plan', () => {
+ const common = ['betreff', 'farbe', 'betrieb_name', 'betrieb_adresse', 'betrieb_telefon', 'betrieb_email', 'impressum_url', 'datenschutz_url']
+ const expected = {
+ G01: { p: ['leistung', 'datum', 'zeit', 'link'], b: ['zu_viele'] },
+ G02: { p: ['gast_name', 'leistung', 'person', 'datum', 'zeit', 'dauer', 'preis', 'zahlung', 'nummer', 'link', 'ort', 'storno_text', 'widerrufsbelehrung', 'widerruf_link'], b: ['preis', 'person', 'storno', 'widerruf', 'anfrage_angenommen'] },
+ G03: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'link'], b: [] },
+ G04: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'nachricht'], b: ['nachricht'] },
+ G05: { p: ['gast_name', 'leistung', 'person', 'datum', 'zeit', 'link'], b: ['person'] },
+ G06: { p: ['gast_name', 'leistung', 'datum', 'zeit', 'absage_von', 'grund'], b: ['nachricht'] },
+ G07: { p: ['gast_name', 'leistung', 'alt_datum', 'alt_zeit', 'datum', 'zeit', 'link'], b: [] },
+ G08: { p: ['eingang', 'nummer'], b: ['zugeordnet', 'nach_frist'] },
+ }
+ for (const [id, { p, b }] of Object.entries(expected)) {
+ const tpl = loadTemplate(id)
+ assert.deepEqual([...tpl.placeholders].sort(), [...common, ...p].sort(), `${id}: Platzhalter`)
+ assert.deepEqual([...tpl.blocks].sort(), ['du', 'sie', ...b].sort(), `${id}: Blöcke`)
+ }
+})
+
+test('G01: Knopf zur Bestätigung, Hinweis für Dritte; Block zu_viele nennt Telefon und Absagen', () => {
+ const tpl = loadTemplate('G01')
+ const values = { ...sampleValues(tpl), betrieb_name: 'Salon Muster', betrieb_telefon: '06301 1234', leistung: 'Herrenschnitt', datum: 'Montag, 12.10.2026', zeit: '10:00', link: 'https://project.webklar.com/termin.html#v=abc' }
+ const sie = renderMail('G01', values, { sie: true })
+ assert.deepEqual(sie.problems, [])
+ assert.match(sie.text, /Leistung: Herrenschnitt/)
+ assert.match(sie.text, /Datum: Montag, 12\.10\.2026/)
+ assert.match(sie.text, /Uhrzeit: 10:00 Uhr/)
+ assert.match(sie.text, /Termin bestätigen: https:\/\/project\.webklar\.com\/termin\.html#v=abc/)
+ assert.match(sie.text, /Das waren Sie nicht\? Dann ignorieren Sie diese E-Mail\./)
+ assert.doesNotMatch(sie.text, /höchstmögliche Zahl/)
+ const viele = renderMail('G01', values, { sie: true, zu_viele: true })
+ assert.match(viele.text, /bereits die höchstmögliche Zahl offener Termine/)
+ assert.match(viele.text, /sagen Sie einen Ihrer offenen Termine ab/)
+ assert.match(viele.text, /rufen Sie uns an: 06301 1234/)
+ assert.match(renderMail('G01', values, { du: true }).text, /Das warst du nicht\? Dann ignoriere diese E-Mail\./)
+})
+
+test('G02: Zusammenfassung, Verwaltungslink, Preis, Person, Storno und Widerruf nur mit ihren Blöcken', () => {
+ const tpl = loadTemplate('G02')
+ const belehrung = 'Widerrufsbelehrung\n\nWiderrufsrecht: 14 Tage ohne Gründe.\nZweite Zeile & mehr'
+ const values = {
+ ...sampleValues(tpl),
+ gast_name: 'Erika Muster', leistung: 'Herrenschnitt', person: 'Anna', datum: 'Montag, 12.10.2026', zeit: '10:00', dauer: '30 Minuten',
+ preis: '28,00 € inkl. USt', zahlung: 'Bezahlung vor Ort', nummer: 'AB12CD34', link: 'https://project.webklar.com/termin.html#m=tok', ort: 'Hauptstr. 1, 67722 Winnweiler',
+ storno_text: 'Absage bis 24 Stunden vorher kostenlos.', widerrufsbelehrung: belehrung, widerruf_link: 'https://project.webklar.com/widerrufen.html#m=tok',
+ }
+ const basis = renderMail('G02', values, { sie: true })
+ assert.deepEqual(basis.problems, [])
+ for (const zeile of ['Guten Tag Erika Muster,', 'Leistung: Herrenschnitt', 'Datum: Montag, 12.10.2026', 'Uhrzeit: 10:00 Uhr', 'Dauer: 30 Minuten', 'Ort: Hauptstr. 1, 67722 Winnweiler', 'Buchungsnummer: AB12CD34']) {
+ assert.ok(basis.text.split('\n').includes(zeile), `Zeile fehlt: ${zeile}\n${basis.text}`)
+ }
+ assert.match(basis.text, /Termin verwalten: https:\/\/project\.webklar\.com\/termin\.html#m=tok/)
+ assert.match(basis.text, /Ihr Termin ist bestätigt/)
+ assert.doesNotMatch(basis.text, /28,00|Bezahlung vor Ort|Anna|Absage bis 24 Stunden|Widerruf|angenommen/)
+
+ const voll = renderMail('G02', values, { sie: true, preis: true, person: true, storno: true, widerruf: true, anfrage_angenommen: true })
+ assert.deepEqual(voll.problems, [])
+ assert.match(voll.text, /Preis: 28,00 € inkl\. USt/)
+ assert.match(voll.text, /Zahlung: Bezahlung vor Ort/)
+ assert.match(voll.text, /Anna/)
+ assert.match(voll.text, /Absage bis 24 Stunden vorher kostenlos\./)
+ assert.match(voll.text, /Wert-betrieb_name hat Ihre Terminanfrage angenommen/)
+ assert.match(voll.text, /Vertrag widerrufen: https:\/\/project\.webklar\.com\/widerrufen\.html#m=tok/)
+ // Belehrung wörtlich, Zeilenumbrüche bleiben, Sonderzeichen sind maskiert
+ assert.match(voll.html, /Widerrufsrecht: 14 Tage <b>ohne<\/b> Gründe\. Zweite Zeile & mehr/)
+ assert.ok(voll.text.includes('Widerrufsbelehrung\n\nWiderrufsrecht: 14 Tage ohne Gründe.\nZweite Zeile & mehr'), voll.text)
+ // der Wortlaut der Belehrung steht in einem abgesetzten Kasten
+ assert.match(voll.html, /class="box"[^>]*> | ]*>Widerrufsbelehrung
Widerrufsrecht/)
+
+ const du = renderMail('G02', values, { du: true, anfrage_angenommen: true, widerruf: true })
+ assert.match(du.text, /Dein Termin ist bestätigt/)
+ assert.match(du.text, /hat deine Terminanfrage angenommen/)
+ assert.doesNotMatch(du.text, SIE_FORM)
+})
+
+test('G03, G04, G05, G06, G07: Inhalt je Mail', () => {
+ const g03 = renderMail('G03', { ...sampleValues(loadTemplate('G03')), gast_name: 'Erika', betrieb_name: 'Salon Muster', link: 'https://x.example/m' }, { sie: true })
+ assert.match(g03.text, /Salon Muster prüft sie und meldet sich bei Ihnen/)
+ assert.match(g03.text, /Anfrage absagen: https:\/\/x\.example\/m/)
+
+ const t04 = loadTemplate('G04')
+ const g04 = renderMail('G04', { ...sampleValues(t04), nachricht: 'Am Montag sind wir leider im Urlaub.' }, { sie: true })
+ assert.doesNotMatch(g04.text, /Urlaub/)
+ assert.match(g04.text, /leider kann Wert-betrieb_name Ihre Terminanfrage nicht annehmen/)
+ assert.match(renderMail('G04', { ...sampleValues(t04), nachricht: 'Am Montag sind wir leider im Urlaub.' }, { sie: true, nachricht: true }).text, /Am Montag sind wir leider im Urlaub\./)
+
+ const t05 = loadTemplate('G05')
+ const g05 = renderMail('G05', { ...sampleValues(t05), person: 'Anna', link: 'https://x.example/m' }, { du: true, person: true })
+ assert.match(g05.text, /Anna/)
+ assert.match(g05.text, /Termin verwalten: https:\/\/x\.example\/m/)
+ assert.match(g05.text, /Erinnerung/)
+ assert.doesNotMatch(renderMail('G05', { ...sampleValues(t05), person: 'Anna' }, { du: true }).text, /Anna/)
+
+ const t06 = loadTemplate('G06')
+ const g06 = renderMail('G06', { ...sampleValues(t06), absage_von: 'dem Betrieb', grund: 'Krankheit im Team' }, { sie: true })
+ assert.match(g06.text, /Abgesagt von: dem Betrieb/)
+ assert.doesNotMatch(g06.text, /Krankheit/)
+ assert.match(renderMail('G06', { ...sampleValues(t06), grund: 'Krankheit im Team' }, { sie: true, nachricht: true }).text, /Krankheit im Team/)
+
+ const t07 = loadTemplate('G07')
+ const g07 = renderMail('G07', { ...sampleValues(t07), alt_datum: '12.10.2026', alt_zeit: '10:00', datum: '13.10.2026', zeit: '11:30' }, { du: true })
+ assert.match(g07.text, /Bisher: 12\.10\.2026, 10:00 Uhr/)
+ assert.match(g07.text, /Neu: 13\.10\.2026, 11:30 Uhr/)
+})
+
+test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und nach_frist, nichts vom Gast', () => {
+ const tpl = loadTemplate('G08')
+ const values = { ...sampleValues(tpl), betrieb_name: 'Salon Muster', eingang: '02.10.2026, 10:07 Uhr', nummer: 'AB12CD34' }
+ const ohne = renderMail('G08', values, { sie: true })
+ assert.deepEqual(ohne.problems, [])
+ assert.match(ohne.text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
+ assert.match(ohne.text, /Widerruf/)
+ assert.doesNotMatch(ohne.text, /AB12CD34|bereits abgelaufen/)
+ const zugeordnet = renderMail('G08', values, { sie: true, zugeordnet: true })
+ assert.match(zugeordnet.text, /Buchung AB12CD34/)
+ assert.match(zugeordnet.text, /Der Termin ist abgesagt\./)
+ assert.doesNotMatch(zugeordnet.text, /bereits abgelaufen/)
+ const spaet = renderMail('G08', values, { sie: true, nach_frist: true })
+ assert.match(spaet.text, /Widerrufsfrist war bei Eingang Ihrer Erklärung bereits abgelaufen/)
+ assert.doesNotMatch(spaet.text, /Der Termin ist abgesagt|AB12CD34/)
+ // Hinweis für Dritte: wer nichts erklärt hat, kann die Mail ignorieren
+ assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
+})
+
+test('K20 und K21: WEBklar-Layout in Du-Form, Gastdaten maskiert, Blöcke je Fall', () => {
+ for (const id of ['K20', 'K21']) {
+ const tpl = loadTemplate(id)
+ assert.equal(tpl.subject, '{{betreff}}', id)
+ for (const p of ['betreff', 'kunde_name', 'leistung', 'person', 'datum', 'zeit', 'gast_name', 'gast_email', 'gast_telefon', 'gast_nachricht', 'portal_link']) assert.ok(tpl.placeholders.includes(p), `${id}: ${p}`)
+ assert.ok(!tpl.blocks.includes('sie') && !tpl.blocks.includes('du'), `${id}: Du-Form fest, keine Anredeblöcke`)
+ const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
+ assert.doesNotMatch(mail.text, SIE_FORM, `${id}: Du-Form\n${mail.text}`)
+ assert.match(mail.text, /Hallo Wert-kunde_name,/, id)
+ assert.match(mail.html, /Muster', gast_email: 'max@example.com', gast_telefon: '0170 1', gast_nachricht: 'Bitte\nFenster', portal_link: 'https://project.webklar.com/dashboard.html' }
+ const buchung = renderMail('K20', werte20, {})
+ assert.deepEqual(buchung.problems, [])
+ assert.match(buchung.html, /Max <b>Muster<\/b>/)
+ assert.match(buchung.html, /Bitte Fenster/)
+ assert.match(buchung.text, /E-Mail: max@example\.com/)
+ assert.match(buchung.text, /Im Portal ansehen: https:\/\/project\.webklar\.com\/dashboard\.html/)
+ assert.doesNotMatch(buchung.text, /noch nicht bestätigt/)
+ assert.match(renderMail('K20', werte20, { anfrage: true }).text, /Diese Anfrage ist noch nicht bestätigt/)
+
+ const t21 = loadTemplate('K21')
+ const werte21 = { ...sampleValues(t21), alt_datum: '11.10.2026', alt_zeit: '09:00', datum: '12.10.2026', zeit: '10:00', eingang: '02.10.2026, 10:07 Uhr' }
+ const fall = (block) => renderMail('K21', werte21, { [block]: true })
+ assert.match(fall('abgesagt').text, /hat den Termin abgesagt/)
+ assert.match(fall('verschoben').text, /Bisher: 11\.10\.2026, 09:00 Uhr/)
+ assert.match(fall('verschoben').text, /Neu: 12\.10\.2026, 10:00 Uhr/)
+ assert.match(fall('widerrufen').text, /Eingegangen am: 02\.10\.2026, 10:07 Uhr/)
+ assert.match(fall('widerrufen').text, /widerrufen/)
+ assert.match(fall('nach_frist').text, /Widerrufsfrist war bei Eingang bereits abgelaufen/)
+ assert.match(fall('nicht_zugeordnet').text, /keiner Buchung zuordnen/)
+ // je Fall erscheint nur der eigene Abschnitt
+ const SATZ = { abgesagt: 'hat den Termin abgesagt', verschoben: 'hat den Termin verschoben', widerrufen: 'hat den Vertrag widerrufen', nach_frist: 'Widerrufsfrist war bei Eingang bereits abgelaufen', nicht_zugeordnet: 'keiner Buchung zuordnen' }
+ for (const block of Object.keys(SATZ)) {
+ const mail = fall(block)
+ assert.deepEqual(mail.problems, [], block)
+ for (const [anderer, satz] of Object.entries(SATZ)) assert.equal(mail.text.includes(satz), anderer === block, `${block}: ${satz}`)
+ }
+})
|