diff --git a/server/mail-templates/T20 Terminplaner Betriebsmeldung.html b/server/mail-templates/T20 Terminplaner Betriebsmeldung.html
new file mode 100644
index 0000000..cf94965
--- /dev/null
+++ b/server/mail-templates/T20 Terminplaner Betriebsmeldung.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+[Terminplaner] {{vorfall}}
+
+
+
+
+{{vorfall}} · {{zeit}} ͏ ͏ ͏
+
+
+
+
+ | WEBklar |
|
+Intern · Terminplaner |
+
|
+|
+
+ |
+| Automatische Benachrichtigung aus dem WEBklar Kundenportal · Ticketsystem |
+
+ |
+
+
+
diff --git a/test/mailTemplates.test.js b/test/mailTemplates.test.js
index 80d3232..6957c82 100644
--- a/test/mailTemplates.test.js
+++ b/test/mailTemplates.test.js
@@ -6,9 +6,9 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
const DIR = new URL('../server/mail-templates/', import.meta.url)
// 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)
+// 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); T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweis; T19 „Erstattung fällig“ folgt in Block B)
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 EXPECTED = [...ids('G', 8), ...ids('K', 21), ...ids('T', 18), 'T20']
const GUEST = ids('G', 8)
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
@@ -24,8 +24,8 @@ for (const id of EXPECTED) {
const source = readFileSync(new URL(tpl.file, DIR), 'utf8')
const title = source.match(/([\s\S]*?)<\/title>/)[1]
assert.equal(title, tpl.subject, 'Betreff im Kopf und sind gleich')
- // F34: Team-Hinweise tragen [Abo], [Speicher] (Zusatzspeicher-Aufgaben), [Datenschutz] (Loeschfristen), [Zeit] (Push-Zeitmail) oder [Finanzen] (Bankzugang)
- assert.equal(id.startsWith('T'), /^\[(Abo|Speicher|Datenschutz|Zeit|Finanzen)\]/.test(tpl.subject) || id === 'T01' || id === 'T16' || id === 'T17', 'Team-Hinweise ab T02 mit [Abo], [Speicher], [Datenschutz], [Zeit] oder [Finanzen] (T16 Funktion bestellt und T17 Repo zuordnen ohne Kennung)')
+ // F34: Team-Hinweise tragen [Abo], [Speicher] (Zusatzspeicher-Aufgaben), [Datenschutz] (Loeschfristen), [Zeit] (Push-Zeitmail), [Finanzen] (Bankzugang) oder [Terminplaner] (Betriebsmeldung)
+ assert.equal(id.startsWith('T'), /^\[(Abo|Speicher|Datenschutz|Zeit|Finanzen|Terminplaner)\]/.test(tpl.subject) || id === 'T01' || id === 'T16' || id === 'T17', 'Team-Hinweise ab T02 mit [Abo], [Speicher], [Datenschutz], [Zeit], [Finanzen] oder [Terminplaner] (T16 Funktion bestellt und T17 Repo zuordnen ohne Kennung)')
const used = new Set([...tpl.body.matchAll(/\{\{(\w+)\}\}/g), ...tpl.subject.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]))
assert.deepEqual([...used].sort(), [...tpl.placeholders].sort(), 'deklarierte = verwendete Platzhalter')
@@ -53,6 +53,23 @@ for (const id of EXPECTED) {
})
}
+// Terminplaner TP2 (A10a1): Betriebsmeldung ans Team; Versand ab A10a2 ueber sendAdminNotice
+test('T20: Betreff [Terminplaner], Platzhalter vorfall/details/zeit, keine Blöcke', () => {
+ const tpl = loadTemplate('T20')
+ assert.equal(tpl.file, 'T20 Terminplaner Betriebsmeldung.html')
+ assert.equal(tpl.subject, '[Terminplaner] {{vorfall}}')
+ assert.deepEqual(tpl.placeholders, ['vorfall', 'details', 'zeit'])
+ assert.deepEqual(tpl.blocks, [])
+ // jeder deklarierte Platzhalter steht im Text, vorfall zusaetzlich im Betreff
+ for (const key of tpl.placeholders) assert.ok(tpl.body.includes(`{{${key}}}`), `{{${key}}} im Text`)
+ const mail = renderMail('T20', { vorfall: 'Mailkontingent erreicht', details: 'Zeile 1\nZeile <2>', zeit: '04.10.2026, 10:00' })
+ assert.deepEqual(mail.problems, [])
+ assert.equal(mail.subject, '[Terminplaner] Mailkontingent erreicht')
+ assert.match(mail.html, /Zeile 1
Zeile <2>/, 'details: HTML-sicher, Zeilenumbrüche bleiben')
+ assert.doesNotMatch(mail.html, /Zeile <2>/)
+ for (const line of ['Mailkontingent erreicht', 'Zeile 1\nZeile <2>', '04.10.2026, 10:00']) assert.ok(mail.text.includes(line), `Text enthält: ${line}\n---\n${mail.text}`)
+})
+
test('Werte werden HTML-sicher eingesetzt, Zeilenumbrüche werden
', () => {
const mail = renderMail('T01', { kunde_name: 'Max &"\'', nachricht: 'Zeile 1\nZeile <2>' })
assert.match(mail.html, /Max <b>&"' hat im Kundenportal geschrieben/)