Terminplaner: Team-Mail-Vorlage T20 Betriebsmeldung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 10:26:08 +00:00
1 parent 43b3caddde
commit 3a3319ec14
2 files changed
+69 -4

No files matched your search

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
T20 · Terminplaner Betriebsmeldung
Betreff: [Terminplaner] {{vorfall}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: das Team (kenso@webklar.com, andrej@webklar.com)
Platzhalter: {{vorfall}}, {{details}}, {{zeit}}
vorfall ist die kurze Überschrift der Meldung; details ist Text (Zeilenumbrüche bleiben); zeit steht als TT.MM.JJJJ, HH:MM (Berliner Zeit). Die Vorlage hat keine optionalen Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>[Terminplaner] {{vorfall}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{vorfall}} · {{zeit}}&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Intern · Terminplaner</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#fdebd3;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#8a4b00;">Betriebsmeldung</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">{{vorfall}}</td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Zeit</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{zeit}}</td></tr></table></td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Details</div><div class="mut" style="color:#4a4843;">{{details}}</div></td></tr></table></td></tr>
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
</td></tr>
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https://ticket.webklar.com/customers" style="color:#4a4843;text-decoration:underline;">Ticketsystem</a></td></tr>
</table>
</td></tr>
</table>
</body>
</html>
+21 -4
View File
@@ -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(/<title>([\s\S]*?)<\/title>/)[1]
assert.equal(title, tpl.subject, 'Betreff im Kopf und <title> 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<br>Zeile &lt;2&gt;/, '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 <br>', () => {
const mail = renderMail('T01', { kunde_name: 'Max <b>&"\'', nachricht: 'Zeile 1\nZeile <2>' })
assert.match(mail.html, /Max &lt;b&gt;&amp;&quot;&#39; hat im Kundenportal geschrieben/)