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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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}} + + + + + + + + + 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]*>Datenschutz { + 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}`) + } +})