Öffnungszeiten: Mails K24 bis K27
Neue Kundenmails (Du-Form, Skelett von K23):
- K24 eingeschaltet (Blöcke angestossen/manuell nach einbau, wieder nach der Abo-Rückkehr)
- K25 Feiertage prüfen (Blöcke pruefen/mehrere, ohne BCC)
- K26 Abo beendet (abo_ende/hosting/wechsel_widerrufen, Frist, einmalig 30 € behalten)
- K27 abgeschaltet (feste Zeiten, Eingaben bis zum Löschdatum, Wiedereinschalten)
server/services/zeiten/mails.js: Builder k24Mail bis k27Mail und sendeZeitenMail
(wirft nie, ohne Adresse { sent:false, reason:'keine_adresse' }, Log ohne Adresse und Namen).
K15 bekommt den Block behalten (Gegenstück neu für den Einrichtungstext; bestellMail
setzt neu = nicht behalten, Standard unverändert), T16 die Blöcke abo und team
(sendT16 führt sie als Vorgabe aus). Pins in mailTemplates- und featureMails-Tests
angepasst; K26 darf „Wartungs-Abo“ im Betreff führen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d1f9176c99
commit
0042d8dd8f
11 files changed
+760
-22
No files matched your search
@@ -8,11 +8,12 @@
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{funktion}}, {{funktion_gen}}, {{funktion_akk}}, {{funktion_nom}}, {{funktion_pron}}, {{preis}}, {{preis_hinweis}}, {{zeit}}, {{danach}}, {{portal_url}}, {{rechnung_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): rechnung
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): rechnung, behalten, neu
|
||||
funktion = Name der Funktion; funktion_gen/_akk/_nom/_pron = ihre Formen im Satz (Genitiv, Akkusativ, Nominativ mit Artikel, Pronomen im Akkusativ), aus dem Register (mails.formen).
|
||||
preis = Preis der Einrichtung aus dem Katalog der Funktion; preis_hinweis = Zusatz in Klammern dahinter (mails.preisHinweis).
|
||||
danach = Satz, was der Kunde nach der Freischaltung tun kann (mails.danach). portal_url = Adresse der Funktion im Kundenportal.
|
||||
rechnung = Link zur Stripe-Rechnung (nur wenn Stripe die Rechnung schon geliefert hat).
|
||||
behalten = der Kunde behält die Funktion nach dem Ende seines Wartungs-Abos für einmalig 30 € (Öffnungszeiten live): nichts wird neu eingerichtet. neu = der Normalfall, die Einrichtung läuft jetzt los; genau einer der beiden ist an (bestellMail setzt neu = nicht behalten).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
@@ -38,9 +39,9 @@ a{color:#111111;}
|
||||
<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:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">✓ Bestellung eingegangen</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;">Deine Bestellung: {{funktion}}</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>vielen Dank für Deine Bestellung und Deine Zahlung. Wir richten {{funktion_akk}} jetzt für Dein Projekt ein.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{funktion}}</strong> – Einrichtung</td></tr><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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><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;">Einmalig bezahlt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{preis}}</strong> <span class="mut" style="color:#6b6964;">({{preis_hinweis}})</span></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Einrichtung</td><td class="val ink rowline" valign="top" style="padding:12px 0;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;">Wie geht es weiter?</div><div class="mut" style="color:#4a4843;">Sobald {{funktion_nom}} auf Deiner Website eingebaut ist, schalten wir {{funktion_pron}} im Kundenportal frei und Du bekommst eine weitere E-Mail. {{danach}}</div></td></tr></table></td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>vielen Dank für Deine Bestellung und Deine Zahlung.<!-- WENN neu --> Wir richten {{funktion_akk}} jetzt für Dein Projekt ein.<!-- ENDE neu --><!-- WENN behalten --> Du behältst {{funktion_akk}} für Dein Projekt, auch ohne Wartungs-Abo.<!-- ENDE behalten --></td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{funktion}}</strong><!-- WENN neu --> – Einrichtung<!-- ENDE neu --><!-- WENN behalten --> – behalten<!-- ENDE behalten --></td></tr><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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><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;">Einmalig bezahlt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{preis}}</strong> <span class="mut" style="color:#6b6964;">({{preis_hinweis}})</span></td></tr><!-- WENN neu --><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Einrichtung</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{zeit}}</td></tr><!-- ENDE neu --></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;">Wie geht es weiter?</div><div class="mut" style="color:#4a4843;"><!-- WENN neu -->Sobald {{funktion_nom}} auf Deiner Website eingebaut ist, schalten wir {{funktion_pron}} im Kundenportal frei und Du bekommst eine weitere E-Mail. {{danach}}<!-- ENDE neu --><!-- WENN behalten -->Deine Einstellungen und Daten bleiben unverändert, und es wird nichts neu eingebaut. Du musst nichts weiter tun.<!-- ENDE behalten --></div></td></tr></table></td></tr>
|
||||
<!-- WENN rechnung -->
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{rechnung_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Rechnung ansehen →</a></td></tr></table></td></tr>
|
||||
<!-- ENDE rechnung -->
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K24 · Zeiten eingeschaltet
|
||||
Betreff: Öffnungszeiten live ist eingeschaltet
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{ticket}}, {{portal_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angestossen, manuell, wieder
|
||||
Geht an Kunden, die Öffnungszeiten live im Wartungs-Abo selbst einschalten (Gratisweg), und, mit dem Block wieder, wenn die Funktion nach einem Abo-Ende wieder eingeschaltet wird.
|
||||
Genau einer der Blöcke angestossen (der Neubau der Website läuft, die Leisten erscheinen nach dem nächsten Veröffentlichen) oder manuell (das Team baut ein) ist an; wieder kommt dazu, wenn das Wartungs-Abo zurückgekehrt ist.
|
||||
ticket = Hinweis auf das Einbau-Ticket mit führendem Leerzeichen, zum Beispiel „ (Ticket #100123)“, oder leer.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und 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>Öffnungszeiten live ist eingeschaltet</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;">Öffnungszeiten live ist eingeschaltet. ͏ ͏ ͏</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;">Eingeschaltet</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:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">✓ Öffnungszeiten live</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;">Öffnungszeiten live ist eingeschaltet</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>Öffnungszeiten live ist für Dein Projekt {{projekt}} eingeschaltet. Die Funktion ist in Deinem Wartungs-Abo ohne Aufpreis enthalten.<!-- WENN wieder --> Dein Wartungs-Abo läuft wieder, deshalb haben wir sie automatisch und ohne Gebühr wieder eingeschaltet. Deine Zeiten und Einstellungen sind unverändert da.<!-- ENDE wieder --></td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Öffnungszeiten live</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{projekt}}</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;">Wie geht es weiter?</div><div class="mut" style="color:#4a4843;"><!-- WENN angestossen -->Die Hinweis-Leiste und die Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen Deiner Website, meist in wenigen Minuten. Die Zeiten-Tabelle setzt unser Team zusätzlich per Ticket an die passende Stelle Deiner Seite{{ticket}}.<!-- ENDE angestossen --><!-- WENN manuell -->Deine Website können wir nicht automatisch aktualisieren. Deshalb baut unser Team die Hinweis-Leiste, die Kontakt-Leiste und die Zeiten-Tabelle von Hand in Deine Seite ein{{ticket}}.<!-- ENDE manuell --></div><div class="mut" style="color:#4a4843;padding-top:8px;">Im Kundenportal trägst Du Deine Öffnungszeiten ein und änderst sie jederzeit. Ohne gespeicherte Zeiten kann die Tabelle nicht eingebaut werden.</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zeiten bearbeiten →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K25 · Zeiten Feiertage prüfen
|
||||
Betreff: Feiertage stehen an – stimmen Deine Zeiten?
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · ohne BCC
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{tage}}, {{portal_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): pruefen, mehrere
|
||||
Erinnerung 1 bis 7 Tage vor Feiertagen und „bitte prüfen“-Tagen (Heiligabend, Silvester, örtliche Feiertage), eine Mail für alle neuen Tage; Hinweis, die Zeiten auch bei Google einzutragen.
|
||||
tage = eine Zeile je Tag, zum Beispiel „Sa 03.10. – Tag der Deutschen Einheit“. pruefen = mindestens ein Tag ist ein „bitte prüfen“-Tag (Erklärung); mehrere = mehr als ein Tag.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und 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>Feiertage stehen an – stimmen Deine Zeiten?</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;">Stimmen Deine Zeiten an den nächsten Feiertagen? ͏ ͏ ͏</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;">Erinnerung</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:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Öffnungszeiten live</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;">Stimmen Deine Zeiten an diesen Tagen?</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>In den nächsten Tagen weicht der Kalender vom Alltag ab. Für Dein Projekt {{projekt}} gelten an diesen Tagen die Zeiten, die Du im Kundenportal eingetragen hast.<!-- WENN mehrere --> Es sind mehrere Tage, bitte prüfe jeden einzelnen.<!-- ENDE mehrere --></td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Betrifft</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{tage}}</td></tr></table></td></tr>
|
||||
<!-- WENN pruefen -->
|
||||
<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;">Bitte prüfen</div><div class="mut" style="color:#4a4843;">Bei Tagen mit dem Zusatz „bitte prüfen“ gilt der Feiertag nur in manchen Orten oder ist kein gesetzlicher Feiertag (zum Beispiel Heiligabend und Silvester). Wir haben dort Deine normalen Zeiten stehen lassen. Bitte trag ein, ob Du an diesen Tagen geöffnet hast.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE pruefen -->
|
||||
<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;">Auch bei Google eintragen</div><div class="mut" style="color:#4a4843;">Dein Google-Unternehmensprofil ändert sich nicht von allein. Trag abweichende Zeiten dort bitte ebenfalls ein; im Kundenportal kannst Du Dir Deine Zeiten dafür zum Kopieren ausgeben lassen.</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zeiten prüfen →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K26 · Zeiten Abo beendet
|
||||
Betreff: Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{frist}}, {{preis}}, {{portal_url}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): abo_ende, hosting, wechsel_widerrufen
|
||||
Geht an Kunden, die Öffnungszeiten live im Wartungs-Abo eingeschaltet hatten, wenn das Abo endet: Die Funktion läuft noch bis zur Frist, danach schalten wir sie ab, außer sie wird für einmalig 30 € behalten.
|
||||
Genau einer der Gründe abo_ende (Wartungs-Abo beendet), hosting (das Abo ist ein Hosting-Abo) oder wechsel_widerrufen (Wechsel ins Wartungs-Abo widerrufen) ist an.
|
||||
frist = Tag, an dem wir abschalten (30 Tage nach dem erkannten Abo-Ende), z. B. „04.11.2026“; preis = Preis zum Behalten bzw. Wiedereinschalten, z. B. „30 €“.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und 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>Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?</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;">Öffnungszeiten live läuft noch bis {{frist}}. ͏ ͏ ͏</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;">Abo beendet</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:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Öffnungszeiten live</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;">Möchtest Du Öffnungszeiten live behalten?</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br><!-- WENN abo_ende -->Dein Wartungs-Abo ist beendet. Öffnungszeiten live war darin ohne Aufpreis enthalten und endet deshalb mit dem Abo.<!-- ENDE abo_ende --><!-- WENN hosting -->Dein Abo ist ein Hosting-Abo. Öffnungszeiten live war im Wartungs-Abo ohne Aufpreis enthalten; ein Hosting-Abo reicht dafür nicht.<!-- ENDE hosting --><!-- WENN wechsel_widerrufen -->Du hast den Wechsel in das Wartungs-Abo widerrufen, Dein Abo bleibt ein Hosting-Abo. Öffnungszeiten live war im Wartungs-Abo ohne Aufpreis enthalten; ein Hosting-Abo reicht dafür nicht.<!-- ENDE wechsel_widerrufen --> Die Funktion läuft noch bis zum {{frist}} unverändert weiter. Wenn Du sie behalten möchtest, geht das für einmalig {{preis}}.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Öffnungszeiten live</strong></td></tr><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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><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;">Läuft weiter bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{frist}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Behalten</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">einmalig {{preis}}</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;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Bis zum {{frist}} bleibt die Funktion eingeschaltet, Deine Website zeigt Zeiten, Status und Leisten wie bisher.</div><div class="mut" style="color:#4a4843;padding-top:8px;"><strong>Behalten:</strong> Für einmalig {{preis}} behältst Du Öffnungszeiten live auch ohne Wartungs-Abo. Das geht im Kundenportal.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Ohne Zahlung schalten wir die Funktion am {{frist}} ab. Dann stehen auf Deiner Website wieder feste Zeiten, und Deine Eingaben bleiben noch 30 Tage gespeichert.</div><div class="mut" style="color:#4a4843;padding-top:8px;"><strong>Rückkehr:</strong> Läuft wieder ein Wartungs-Abo, solange Deine Eingaben noch gespeichert sind, schalten wir die Funktion ohne Gebühr wieder ein.</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Für {{preis}} behalten →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K27 · Zeiten abgeschaltet
|
||||
Betreff: Öffnungszeiten live ist abgeschaltet
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{loeschdatum}}, {{preis}}, {{portal_url}}
|
||||
Geht an Kunden, deren Wartungs-Abo geendet hat und die Öffnungszeiten live nicht behalten haben: Die Leisten sind weg, auf der Seite stehen wieder feste Zeiten; die Eingaben bleiben noch 30 Tage gespeichert, Wiedereinschalten für einmalig 30 €.
|
||||
loeschdatum = Tag, ab dem wir die Eingaben löschen (30 Tage nach dem Abschalten); preis = Preis zum Wiedereinschalten, z. B. „30 €“.
|
||||
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und 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>Öffnungszeiten live ist abgeschaltet</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;">Öffnungszeiten live ist abgeschaltet. ͏ ͏ ͏</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;">Abgeschaltet</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:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Öffnungszeiten live</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;">Öffnungszeiten live ist abgeschaltet</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>Öffnungszeiten live ist für Dein Projekt {{projekt}} abgeschaltet, weil das Wartungs-Abo beendet ist und die Frist zum Behalten abgelaufen ist. Die Hinweis-Leiste, die Kontakt-Leiste und der Live-Status sind von Deiner Website verschwunden; dort stehen ab jetzt wieder feste Zeiten.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 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;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Öffnungszeiten live</strong></td></tr><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;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Eingaben gespeichert bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{loeschdatum}}</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;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Deine Eingaben (Zeiten, Hinweise und Einstellungen) bleiben bis zum {{loeschdatum}} gespeichert. Danach löschen wir sie.</div><div class="mut" style="color:#4a4843;padding-top:8px;"><strong>Wieder einschalten:</strong> Bis dahin kannst Du Öffnungszeiten live im Kundenportal für einmalig {{preis}} wieder einschalten. Läuft wieder ein Wartungs-Abo, schalten wir die Funktion automatisch und ohne Gebühr wieder ein.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Nach der Löschung musst Du die Funktion neu einschalten und Deine Zeiten noch einmal eintragen.</div></td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,7 @@
|
||||
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
|
||||
Empfänger: kenso@webklar.com, andrej@webklar.com
|
||||
Platzhalter: {{kunde_name}}, {{email}}, {{projekt}}, {{funktion}}, {{woid}}, {{sitekey}}, {{sitzung}}, {{zustand}}, {{anleitung_url}}, {{gesundheit_text}}
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_fehlgeschlagen, ungeklaert, kein_abo, bereits_aktiv, anleitung, gesundheit
|
||||
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): zahlung_fehlgeschlagen, ungeklaert, kein_abo, bereits_aktiv, anleitung, gesundheit, abo, team
|
||||
funktion = Name der Funktion aus dem Register (zum Beispiel News-Pinnwand, Terminplaner); ohne bekannte Funktion steht „Funktion“.
|
||||
ungeklaert = eine bezahlte Stripe-Sitzung passt zu keiner offenen Bestellung (Betreff und Inhalt: von Hand pruefen); sitzung nennt Sitzungs-ID, Kunde und Projekt.
|
||||
zahlung_fehlgeschlagen = die Zahlung (SEPA) ist nicht eingegangen; es wurde nichts bestellt, woid und sitekey sind dann leer.
|
||||
@@ -16,6 +16,8 @@
|
||||
bereits_aktiv = die Zahlung ist da, aber die Funktion war schon in dem Zustand {{zustand}}; es wurde kein Ticket angelegt und nichts eingerichtet (Einbau nachholen oder erstatten); sitzung nennt die Stripe-Sitzung.
|
||||
anleitung = Knopf „Im Ticketsystem öffnen“ zur Einbauanleitung des Projekts (anleitung_url); nur bei Funktionen mit Anleitung und angelegtem Ticket.
|
||||
gesundheit = Warnzeile „Gesundheitsbereich prüfen“ mit gesundheit_text (Hinweise zur Branche des Betriebs).
|
||||
abo = die Funktion wurde ohne Zahlung eingeschaltet, weil sie im Wartungs-Abo des Kunden enthalten ist (Gratisweg, der Kunde hat selbst eingeschaltet); es gibt keine Rechnung.
|
||||
team = das Team hat die Funktion ohne Zahlung freigeschaltet („Aktivieren ohne Zahlung“); es gibt keine Rechnung.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
@@ -54,6 +56,12 @@ a{color:#111111;}
|
||||
<!-- WENN bereits_aktiv -->
|
||||
<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:#fdf0ef;border:1px solid #f1c9c5;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;">Bezahlt, aber schon vorhanden</div><div class="mut" style="color:#4a4843;">Zahlung eingegangen, aber die Funktion war schon {{zustand}}. Es wurde kein Ticket angelegt und nichts eingerichtet. Bitte prüfen: Einbau nachholen oder erstatten. Sitzung: {{sitzung}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE bereits_aktiv -->
|
||||
<!-- WENN abo -->
|
||||
<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;">Im Wartungs-Abo enthalten</div><div class="mut" style="color:#4a4843;">Der Kunde hat die Funktion selbst eingeschaltet. Sie ist in seinem Wartungs-Abo ohne Aufpreis enthalten; es gab keine Zahlung und es gibt keine Rechnung.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE abo -->
|
||||
<!-- WENN team -->
|
||||
<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;">Vom Team freigeschaltet</div><div class="mut" style="color:#4a4843;">Die Funktion wurde im Ticketsystem ohne Zahlung freigeschaltet. Es gab keine Zahlung und es gibt keine Rechnung.</div></td></tr></table></td></tr>
|
||||
<!-- ENDE team -->
|
||||
<!-- WENN gesundheit -->
|
||||
<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:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:16px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;">Gesundheitsbereich prüfen: {{gesundheit_text}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE gesundheit -->
|
||||
|
||||
@@ -621,8 +621,10 @@ function einrichtungsPreis(feature) {
|
||||
* K15 (Bestellung eingegangen) einer Funktion: Vorlage, Platzhalterwerte und Bloecke ausschliesslich aus dem Register-Eintrag
|
||||
* (mails.*, setupEstimate, returnHash) und dem Katalogpreis. Die Werte decken genau die Platzhalter der Vorlage ab.
|
||||
* Ohne Anhaenge: die Rechtsunterlagen der Termine-Bestellung haengt F8 direkt in onFeaturePaid an.
|
||||
* behalten: der Kunde behaelt die Funktion nach dem Abo-Ende fuer einmalig 30 EUR (Oeffnungszeiten live): die Mail sagt, dass nichts
|
||||
* neu eingerichtet wird. Die Vorlage kennt dafuer zwei Gegenstuecke, behalten und neu (Normalfall); genau eins ist an.
|
||||
*/
|
||||
export function bestellMail(feature, { kundeName, projekt, invoiceUrl }) {
|
||||
export function bestellMail(feature, { kundeName, projekt, invoiceUrl, behalten = false }) {
|
||||
const { funktion, formen, preisHinweis, danach } = feature.mails
|
||||
return {
|
||||
template: 'K15',
|
||||
@@ -641,7 +643,7 @@ export function bestellMail(feature, { kundeName, projekt, invoiceUrl }) {
|
||||
portal_url: portalUrl(feature),
|
||||
rechnung_url: invoiceUrl || '',
|
||||
},
|
||||
blocks: { rechnung: Boolean(invoiceUrl) },
|
||||
blocks: { rechnung: Boolean(invoiceUrl), behalten: Boolean(behalten), neu: !behalten },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +736,7 @@ function sendT16({ funktion, ...werte }, bloecke) {
|
||||
gesundheit_text: '',
|
||||
...werte,
|
||||
},
|
||||
blocks: { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false, ...bloecke },
|
||||
blocks: { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false, abo: false, team: false, ...bloecke },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { config } from '../../config.js'
|
||||
import { sendCustomerMail } from '../mailer.js'
|
||||
import { datumKurz, wochentag } from '../termine/zeit.js'
|
||||
|
||||
/**
|
||||
* Mails von Öffnungszeiten live an Kunden (Vorlagen server/mail-templates/K24 bis K27):
|
||||
* K24 eingeschaltet (Gratisweg im Wartungs-Abo; mit Block `wieder` nach der Rückkehr des Abos)
|
||||
* K25 Feiertags-Erinnerung (eine Mail mit allen neuen Tagen, ohne BCC)
|
||||
* K26 Wartungs-Abo beendet: Funktion läuft bis zur Frist, „für einmalig 30 € behalten“ (Varianten nach der Abo-Lage)
|
||||
* K27 abgeschaltet: feste Zeiten auf der Seite, Eingaben noch bis zum Löschdatum
|
||||
* Die Builder sind rein (kein Zugriff auf Appwrite, kein Versand) und liefern { template, values, blocks }, so wie
|
||||
* siteFeatures.bestellMail: die Werte decken genau die Platzhalter der Vorlage ab. sendeZeitenMail verschickt sie.
|
||||
*
|
||||
* Anrede: `kunde` ist der Name oder das Kundendokument; ohne Namen bleibt kunde_name leer (wie bei K22/K23 im Terminplaner).
|
||||
* Datumsangaben (`frist`, `loeschdatum`) sind Date-Objekte und stehen als TT.MM.JJJJ (Berliner Tag) in der Mail.
|
||||
*/
|
||||
|
||||
const WOCHENTAG_KURZ = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||
const TAG_FORM = /^\d{4}-\d{2}-\d{2}$/
|
||||
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
|
||||
|
||||
/** Adresse des Editors im Kundenportal. */
|
||||
const portalUrl = () => `${config.publicBaseUrl}/dashboard.html#zeiten`
|
||||
|
||||
/** Anrede-Wert: Name aus einem String oder einem Kundendokument, getrimmt; sonst '' (die Vorlage sagt dann „Hallo ,“, wie K22/K23). */
|
||||
function kundenName(kunde) {
|
||||
const name = typeof kunde === 'string' ? kunde : kunde?.name
|
||||
return typeof name === 'string' ? name.trim() : ''
|
||||
}
|
||||
|
||||
/** Date -> 'TT.MM.JJJJ' (Berliner Tag). Wirft bei etwas anderem als einem gültigen Date: ein leeres Datum in der Mail wäre falsch. */
|
||||
function datum(wert, feld) {
|
||||
if (!(wert instanceof Date) || Number.isNaN(wert.getTime())) throw new TypeError(`${feld}: gültiges Date erwartet`)
|
||||
return datumKurz(wert)
|
||||
}
|
||||
|
||||
/**
|
||||
* K24: Öffnungszeiten live ist eingeschaltet.
|
||||
* @param {{ kunde: string | { name?: string }, projekt: string, einbau?: string | null, wieder?: boolean, ticketNr?: string | number | null }} p
|
||||
* einbau: 'angestossen' (Neubau läuft: Leisten erscheinen nach dem nächsten Veröffentlichen) oder 'manuell' (das Team baut ein);
|
||||
* alles andere, auch ein fehlender Wert, sagt das Vorsichtige: das Team baut ein.
|
||||
* wieder: das Wartungs-Abo läuft wieder und die Funktion wurde automatisch wieder eingeschaltet.
|
||||
* ticketNr: Nummer des Einbau-Tickets; ohne sie steht kein Ticket in der Mail.
|
||||
*/
|
||||
export function k24Mail({ kunde, projekt, einbau, wieder = false, ticketNr = null }) {
|
||||
const nr = String(ticketNr ?? '').trim().replace(/^#/, '')
|
||||
const angestossen = einbau === 'angestossen'
|
||||
return {
|
||||
template: 'K24',
|
||||
values: {
|
||||
kunde_name: kundenName(kunde),
|
||||
projekt,
|
||||
// führendes Leerzeichen: steht direkt hinter einem Wort, ohne Ticket bleibt nichts übrig
|
||||
ticket: nr ? ` (Ticket #${nr})` : '',
|
||||
portal_url: portalUrl(),
|
||||
},
|
||||
blocks: { angestossen, manuell: !angestossen, wieder: Boolean(wieder) },
|
||||
}
|
||||
}
|
||||
|
||||
/** Eine Zeile je Erinnerungstag: 'Sa 03.10. – Tag der Deutschen Einheit', bei „bitte prüfen“ mit Zusatz. */
|
||||
function tagesZeile(eintrag) {
|
||||
if (typeof eintrag === 'string') return eintrag
|
||||
const { tag, name, art } = eintrag || {}
|
||||
const zeile = TAG_FORM.test(String(tag))
|
||||
? `${WOCHENTAG_KURZ[wochentag(tag) - 1]} ${tag.slice(8, 10)}.${tag.slice(5, 7)}.${name ? ` – ${name}` : ''}`
|
||||
: [tag, name].filter(Boolean).join(' – ')
|
||||
return art === 'pruefen' ? `${zeile} (bitte prüfen)` : zeile
|
||||
}
|
||||
|
||||
/**
|
||||
* K25: Feiertage stehen an (eine Mail für alle neuen Tage; Aufrufer: Erinnerungs-Job, ohne BCC ans Team).
|
||||
* @param {{ kunde: string | { name?: string }, projekt: string, tage: Array<{ tag: string, name?: string, art?: 'feiertag' | 'pruefen' }> }} p
|
||||
* tage: [{ tag: 'YYYY-MM-DD', name, art }] wie zeitlogik.erinnerungsTage(); Blöcke: pruefen (mindestens ein „bitte prüfen“-Tag), mehrere (> 1 Tag).
|
||||
*/
|
||||
export function k25Mail({ kunde, projekt, tage }) {
|
||||
const liste = Array.isArray(tage) ? tage : []
|
||||
return {
|
||||
template: 'K25',
|
||||
values: {
|
||||
kunde_name: kundenName(kunde),
|
||||
projekt,
|
||||
tage: liste.map(tagesZeile).join('\n'),
|
||||
portal_url: portalUrl(),
|
||||
},
|
||||
blocks: { pruefen: liste.some((e) => e?.art === 'pruefen'), mehrere: liste.length > 1 },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* K26: Das Wartungs-Abo ist beendet, die Funktion läuft noch bis zur Frist; für einmalig `preis` behalten.
|
||||
* @param {{ kunde: string | { name?: string }, projekt: string, art?: string, frist: Date, preis: string }} p
|
||||
* art: Abo-Lage aus aboStatus.aboLage(): 'hosting' (das Abo ist ein Hosting-Abo), 'wechsel_widerrufen' (Wechsel ins Wartungs-Abo
|
||||
* widerrufen), sonst (auch 'beendet' und Unbekanntes) 'abo_ende'. frist: Tag des Abschaltens (aboEndeAt + 30 Tage).
|
||||
*/
|
||||
export function k26Mail({ kunde, projekt, art, frist, preis }) {
|
||||
return {
|
||||
template: 'K26',
|
||||
values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), preis, portal_url: portalUrl() },
|
||||
blocks: { abo_ende: art !== 'hosting' && art !== 'wechsel_widerrufen', hosting: art === 'hosting', wechsel_widerrufen: art === 'wechsel_widerrufen' },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* K27: Öffnungszeiten live ist abgeschaltet (Wartungs-Abo beendet, Frist zum Behalten abgelaufen).
|
||||
* @param {{ kunde: string | { name?: string }, projekt: string, loeschdatum: Date, preis: string }} p
|
||||
* loeschdatum: Tag, ab dem die Eingaben gelöscht werden (30 Tage nach dem Abschalten); preis: Preis zum Wiedereinschalten.
|
||||
*/
|
||||
export function k27Mail({ kunde, projekt, loeschdatum, preis }) {
|
||||
return {
|
||||
template: 'K27',
|
||||
values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), preis, portal_url: portalUrl() },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fehlertext fürs Log: Name und gekürzte Meldung. SMTP-Antworten nennen gern die Adresse (und fremde Server den Namen):
|
||||
* E-Mail-Adressen und der Name des Kunden werden unkenntlich gemacht.
|
||||
*/
|
||||
function fehlerText(err, customer) {
|
||||
let meldung = String(err?.message ?? '').replace(ADRESSE, '[Adresse]')
|
||||
const name = kundenName(customer)
|
||||
if (name.length >= 2) meldung = meldung.split(name).join('[Name]')
|
||||
return `${err?.name ?? 'Fehler'} ${meldung.slice(0, 200)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine der Mails K24 bis K27 an die E-Mail-Adresse des Kundenkontos senden. Wirft nie; das Log nennt weder Adresse noch Namen.
|
||||
* @param {{ email?: string, name?: string } | null | undefined} customer Kundendokument
|
||||
* @param {{ template: string, values?: object, blocks?: object }} nachricht Ergebnis von k24Mail ... k27Mail
|
||||
* @param {{ bccTeam?: boolean }} [options] bccTeam false: keine Kopie ans Team (K25)
|
||||
* @returns {Promise<{ sent: boolean, reason?: 'keine_adresse' | 'fehler' }>}
|
||||
*/
|
||||
export async function sendeZeitenMail(customer, nachricht, { bccTeam = true } = {}) {
|
||||
const vorlage = nachricht?.template ?? 'Mail'
|
||||
try {
|
||||
const to = String(customer?.email ?? '').trim()
|
||||
if (!to) {
|
||||
console.warn(`[zeiten-mail] ${vorlage} entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse`)
|
||||
return { sent: false, reason: 'keine_adresse' }
|
||||
}
|
||||
await sendCustomerMail(to, { ...nachricht, bccTeam })
|
||||
return { sent: true }
|
||||
} catch (err) {
|
||||
console.error(`[zeiten-mail] ${vorlage} nicht gesendet: ${fehlerText(err, customer)}`)
|
||||
return { sent: false, reason: 'fehler' }
|
||||
}
|
||||
}
|
||||
@@ -502,7 +502,7 @@ test('Platzhalter-/Blocklisten exakt: K15 und K16', () => {
|
||||
'portal_url',
|
||||
'rechnung_url',
|
||||
])
|
||||
assert.deepEqual(k15.blocks, ['rechnung'])
|
||||
assert.deepEqual(k15.blocks, ['rechnung', 'behalten', 'neu'])
|
||||
assert.equal(k15.subject, 'Deine Bestellung: {{funktion}}')
|
||||
const k16 = loadTemplate('K16')
|
||||
assert.deepEqual(k16.placeholders, ['kunde_name', 'projekt', 'funktion', 'funktion_dein', 'funktion_nom', 'bereit', 'knopf', 'portal_url'])
|
||||
@@ -514,7 +514,7 @@ test('Platzhalter-/Blocklisten exakt: T16 und T19', () => {
|
||||
const t16 = loadTemplate('T16')
|
||||
assert.equal(t16.subject, 'Neue Funktion bestellt: {{funktion}}')
|
||||
assert.deepEqual(t16.placeholders, ['kunde_name', 'email', 'projekt', 'funktion', 'woid', 'sitekey', 'sitzung', 'zustand', 'anleitung_url', 'gesundheit_text'])
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit'])
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team'])
|
||||
const t19 = loadTemplate('T19')
|
||||
assert.equal(t19.file, 'T19 Erstattung faellig.html')
|
||||
assert.equal(t19.subject, '[Abo] Erstattung fällig: {{funktion}}-Einrichtung')
|
||||
@@ -533,8 +533,8 @@ test('Die Mail-Werte je Funktion decken genau die Platzhalter der Vorlagen ab',
|
||||
assert.equal(k16.template, 'K16')
|
||||
assert.deepEqual(Object.keys(k15.values).sort(), [...loadTemplate('K15').placeholders].sort(), `${key}: K15`)
|
||||
assert.deepEqual(Object.keys(k16.values).sort(), [...loadTemplate('K16').placeholders].sort(), `${key}: K16`)
|
||||
assert.deepEqual(k15.blocks, { rechnung: true })
|
||||
assert.deepEqual(sf.bestellMail(feature, { ...ctx, invoiceUrl: '' }).blocks, { rechnung: false })
|
||||
assert.deepEqual(k15.blocks, { rechnung: true, behalten: false, neu: true })
|
||||
assert.deepEqual(sf.bestellMail(feature, { ...ctx, invoiceUrl: '' }).blocks, { rechnung: false, behalten: false, neu: true })
|
||||
for (const nachricht of [k15, k16]) {
|
||||
const mail = renderMail(nachricht.template, nachricht.values, nachricht.blocks)
|
||||
assert.deepEqual(mail.problems, [], `${key}: ${nachricht.template}`)
|
||||
|
||||
+10
-10
@@ -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, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b)
|
||||
// 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, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); K24–K27 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b)
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 23), ...ids('T', 19), 'T20']
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 27), ...ids('T', 19), 'T20']
|
||||
const GUEST = ids('G', 8)
|
||||
|
||||
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
|
||||
@@ -75,14 +75,14 @@ test('K15, K16: Platzhalter und Blöcke je Funktion, keine Funktion fest im Text
|
||||
const k15 = loadTemplate('K15')
|
||||
assert.equal(k15.subject, 'Deine Bestellung: {{funktion}}')
|
||||
assert.deepEqual(k15.placeholders, ['kunde_name', 'projekt', 'funktion', 'funktion_gen', 'funktion_akk', 'funktion_nom', 'funktion_pron', 'preis', 'preis_hinweis', 'zeit', 'danach', 'portal_url', 'rechnung_url'])
|
||||
assert.deepEqual(k15.blocks, ['rechnung'])
|
||||
assert.deepEqual(k15.blocks, ['rechnung', 'behalten', 'neu'])
|
||||
const k16 = loadTemplate('K16')
|
||||
assert.equal(k16.subject, '{{funktion_dein}} ist bereit')
|
||||
assert.deepEqual(k16.placeholders, ['kunde_name', 'projekt', 'funktion', 'funktion_dein', 'funktion_nom', 'bereit', 'knopf', 'portal_url'])
|
||||
assert.deepEqual(k16.blocks, [])
|
||||
for (const tpl of [k15, k16]) assert.doesNotMatch(tpl.body, /News|Beitr|#news|#termine|Terminplaner/, `${tpl.id}: keine feste Funktion`)
|
||||
|
||||
const mail = renderMail('K15', { ...sampleValues(k15), rechnung_url: 'https://rechnung.example/1', portal_url: 'https://portal.example/#x' }, { rechnung: true })
|
||||
const mail = renderMail('K15', { ...sampleValues(k15), rechnung_url: 'https://rechnung.example/1', portal_url: 'https://portal.example/#x' }, { rechnung: true, neu: true })
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, 'Deine Bestellung: Wert-funktion')
|
||||
for (const part of ['Wir richten Wert-funktion_akk jetzt für Dein Projekt ein.', 'Sobald Wert-funktion_nom auf Deiner Website eingebaut ist, schalten wir Wert-funktion_pron im Kundenportal frei', 'Wert-danach', 'Wert-preis (Wert-preis_hinweis)', 'Rechnung ansehen: https://rechnung.example/1', 'Zum Kundenportal: https://portal.example/#x']) {
|
||||
@@ -101,7 +101,7 @@ test('K15, K16: Platzhalter und Blöcke je Funktion, keine Funktion fest im Text
|
||||
test('T16, T19: Betreff, Platzhalter und Blöcke stehen in der Vorlage', () => {
|
||||
const t16 = loadTemplate('T16')
|
||||
assert.equal(t16.subject, 'Neue Funktion bestellt: {{funktion}}')
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit'])
|
||||
assert.deepEqual(t16.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team'])
|
||||
const t19 = loadTemplate('T19')
|
||||
assert.equal(t19.file, 'T19 Erstattung faellig.html')
|
||||
assert.match(t19.subject, /^\[Abo\] Erstattung fällig: \{\{funktion\}\}-Einrichtung$/)
|
||||
@@ -511,8 +511,8 @@ test('T02 und T07 erinnern an UptimeRobot', () => {
|
||||
assert.ok(t07.text.includes('Monitor in UptimeRobot entfernen'), t07.text)
|
||||
})
|
||||
|
||||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14 und T12', () => {
|
||||
for (const id of templateIds()) if (!['K12', 'K14', 'T12'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14, T12 und K26 (Abo beendet, Öffnungszeiten live)', () => {
|
||||
for (const id of templateIds()) if (!['K12', 'K14', 'T12', 'K26'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||||
})
|
||||
|
||||
test('K01-Anhang: Fassung 3 für v3-Abos, Fassung 4 sonst', () => {
|
||||
@@ -918,9 +918,9 @@ test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und na
|
||||
assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
|
||||
})
|
||||
|
||||
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest)
|
||||
test('K22, K23: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
|
||||
for (const id of ['K22', 'K23']) {
|
||||
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest); Öffnungszeiten live: K24–K27 ebenso
|
||||
test('K22, K23, K24–K27: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
|
||||
for (const id of ['K22', 'K23', 'K24', 'K25', 'K26', 'K27']) {
|
||||
const tpl = loadTemplate(id)
|
||||
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))
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Mails K24–K27 (Öffnungszeiten live: eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet), ihre Builder und
|
||||
// sendeZeitenMail (server/services/zeiten/mails.js, Task 13) sowie die neuen Blöcke behalten/neu in K15 und abo/team in T16.
|
||||
// Die allgemeinen Vorlagenregeln (Kopf, Platzhalter, Blöcke, Logo, Text ohne Entities, Du-Form) prüft test/mailTemplates.test.js.
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { loadTemplate, renderMail } = await import('../server/services/mailTemplates.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const reg = await import('../server/services/featureRegistry.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const { k24Mail, k25Mail, k26Mail, k27Mail, sendeZeitenMail } = await import('../server/services/zeiten/mails.js')
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const KUNDE = { $id: 'c1', name: 'Silke Müller', email: 'silke@example.com' }
|
||||
const PORTAL = `${config.publicBaseUrl}/dashboard.html#zeiten`
|
||||
|
||||
let sink
|
||||
let log
|
||||
beforeEach(() => {
|
||||
sink = mailSink()
|
||||
setMailTransportForTests(sink)
|
||||
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}), info: mock.method(console, 'info', () => {}), log: mock.method(console, 'log', () => {}) }
|
||||
})
|
||||
afterEach(() => mock.restoreAll())
|
||||
const logText = () => Object.values(log).flatMap((m) => m.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' '))).join('\n')
|
||||
|
||||
const render = (nachricht) => renderMail(nachricht.template, nachricht.values, nachricht.blocks)
|
||||
const an = (nachricht) => Object.entries(nachricht.blocks || {}).filter(([, on]) => on).map(([name]) => name).sort()
|
||||
|
||||
// Ein Beispiel je Variante: genau diese Nachrichten gehen durch die Prüfung "Werte decken die Platzhalter ab"
|
||||
const FRIST = new Date('2026-11-04T09:00:00.000Z')
|
||||
const BEISPIELE = {
|
||||
'K24 angestossen': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: false, ticketNr: 100123 }),
|
||||
'K24 manuell mit Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'manuell', wieder: false, ticketNr: 100123 }),
|
||||
'K24 wieder ohne Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: true }),
|
||||
'K25 ein Tag': () => k25Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', tage: [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }] }),
|
||||
'K25 mehrere mit prüfen': () =>
|
||||
k25Mail({
|
||||
kunde: KUNDE.name,
|
||||
projekt: 'Praxis Muster',
|
||||
tage: [
|
||||
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
|
||||
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
|
||||
],
|
||||
}),
|
||||
'K26 beendet': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'beendet', frist: FRIST, preis: '30 €' }),
|
||||
'K26 hosting': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'hosting', frist: FRIST, preis: '30 €' }),
|
||||
'K26 Wechsel widerrufen': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'wechsel_widerrufen', frist: FRIST, preis: '30 €' }),
|
||||
K27: () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', loeschdatum: FRIST, preis: '30 €' }),
|
||||
}
|
||||
|
||||
test('Vorlagen K24–K27: Datei, Betreff, Platzhalter und Blöcke stehen fest', () => {
|
||||
const erwartet = {
|
||||
K24: { file: 'K24 Zeiten eingeschaltet.html', subject: 'Öffnungszeiten live ist eingeschaltet', placeholders: ['kunde_name', 'projekt', 'ticket', 'portal_url'], blocks: ['angestossen', 'manuell', 'wieder'] },
|
||||
K25: { file: 'K25 Zeiten Feiertage pruefen.html', subject: 'Feiertage stehen an – stimmen Deine Zeiten?', placeholders: ['kunde_name', 'projekt', 'tage', 'portal_url'], blocks: ['pruefen', 'mehrere'] },
|
||||
K26: { file: 'K26 Zeiten Abo beendet.html', subject: 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?', placeholders: ['kunde_name', 'projekt', 'frist', 'preis', 'portal_url'], blocks: ['abo_ende', 'hosting', 'wechsel_widerrufen'] },
|
||||
K27: { file: 'K27 Zeiten abgeschaltet.html', subject: 'Öffnungszeiten live ist abgeschaltet', placeholders: ['kunde_name', 'projekt', 'loeschdatum', 'preis', 'portal_url'], blocks: [] },
|
||||
}
|
||||
for (const [id, soll] of Object.entries(erwartet)) {
|
||||
const tpl = loadTemplate(id)
|
||||
assert.equal(tpl.file, soll.file, id)
|
||||
assert.equal(tpl.subject, soll.subject, id)
|
||||
assert.deepEqual(tpl.placeholders, soll.placeholders, id)
|
||||
assert.deepEqual(tpl.blocks, soll.blocks, id)
|
||||
}
|
||||
})
|
||||
|
||||
test('Builder decken genau die Platzhalter und Blöcke der Vorlage ab', () => {
|
||||
for (const [name, bauen] of Object.entries(BEISPIELE)) {
|
||||
const nachricht = bauen()
|
||||
const tpl = loadTemplate(nachricht.template)
|
||||
assert.deepEqual(Object.keys(nachricht.values).sort(), [...tpl.placeholders].sort(), `${name}: Platzhalter`)
|
||||
for (const block of Object.keys(nachricht.blocks || {})) assert.ok(tpl.blocks.includes(block), `${name}: Block ${block} gehört zur Vorlage`)
|
||||
const mail = render(nachricht)
|
||||
assert.deepEqual(mail.problems, [], name)
|
||||
assert.equal(nachricht.values.portal_url, PORTAL, `${name}: Knopf führt in den Editor`)
|
||||
assert.equal(nachricht.values.kunde_name, 'Silke Müller', name)
|
||||
assert.match(mail.text, /Hallo Silke Müller,/, name)
|
||||
assert.ok(mail.text.includes(`: ${PORTAL}`), `${name}: Knopf mit Adresse im Text`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Anrede: Kunde als Name oder als Kundendokument, ohne Namen bleibt der Wert leer (wie K22/K23)', () => {
|
||||
assert.equal(k27Mail({ kunde: KUNDE, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, 'Silke Müller')
|
||||
for (const kunde of [undefined, null, '', ' ', {}, { name: null }]) {
|
||||
assert.equal(k27Mail({ kunde, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, '', JSON.stringify(kunde))
|
||||
}
|
||||
})
|
||||
|
||||
test('K24: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen, manuell: das Team baut ein)', () => {
|
||||
const auto = BEISPIELE['K24 angestossen']()
|
||||
assert.equal(auto.template, 'K24')
|
||||
assert.deepEqual(auto.blocks, { angestossen: true, manuell: false, wieder: false })
|
||||
const textAuto = render(auto).text
|
||||
assert.match(textAuto, /nach dem nächsten Veröffentlichen/)
|
||||
assert.doesNotMatch(textAuto, /baut unser Team/)
|
||||
assert.match(textAuto, /Ticket #100123/, 'Ticketnummer steht in beiden Fällen in der Mail')
|
||||
|
||||
const hand = BEISPIELE['K24 manuell mit Ticket']()
|
||||
assert.deepEqual(hand.blocks, { angestossen: false, manuell: true, wieder: false })
|
||||
const textHand = render(hand).text
|
||||
assert.match(textHand, /baut unser Team/)
|
||||
assert.doesNotMatch(textHand, /nach dem nächsten Veröffentlichen/)
|
||||
assert.match(textHand, /Ticket #100123/)
|
||||
|
||||
// ohne bekannten Einbau sagt die Mail das Vorsichtige: das Team baut ein
|
||||
for (const einbau of [undefined, null, '', 'unbekannt']) {
|
||||
const unklar = k24Mail({ kunde: 'K', projekt: 'P', einbau })
|
||||
assert.deepEqual(unklar.blocks, { angestossen: false, manuell: true, wieder: false }, String(einbau))
|
||||
}
|
||||
})
|
||||
|
||||
test('K24: ohne Ticketnummer steht kein leeres Ticket in der Mail; wieder nennt das Abo', () => {
|
||||
const ohne = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell' })
|
||||
assert.equal(ohne.values.ticket, '')
|
||||
assert.doesNotMatch(render(ohne).text, /Ticket|\(\)|\s\./)
|
||||
const leer = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: ' ' })
|
||||
assert.equal(leer.values.ticket, '')
|
||||
assert.equal(k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: '#100124' }).values.ticket, ' (Ticket #100124)')
|
||||
|
||||
const wieder = BEISPIELE['K24 wieder ohne Ticket']()
|
||||
assert.deepEqual(wieder.blocks, { angestossen: true, manuell: false, wieder: true })
|
||||
const mitWieder = render(wieder).text
|
||||
assert.match(mitWieder, /Dein Wartungs-Abo läuft wieder/)
|
||||
assert.match(mitWieder, /wieder eingeschaltet/)
|
||||
assert.match(mitWieder, /Zeiten und Einstellungen sind unverändert/)
|
||||
assert.doesNotMatch(render(BEISPIELE['K24 angestossen']()).text, /Wartungs-Abo läuft wieder/)
|
||||
})
|
||||
|
||||
test('K24: Betreff, Projekt und Knopf', () => {
|
||||
const mail = render(BEISPIELE['K24 angestossen']())
|
||||
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
|
||||
assert.match(mail.text, /Projekt: Praxis Muster/)
|
||||
assert.match(mail.html, new RegExp(`<a href="${PORTAL.replaceAll('.', '\\.')}"`))
|
||||
})
|
||||
|
||||
test('K25: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“ und Mehrzahl nur bei Bedarf', () => {
|
||||
const ein = BEISPIELE['K25 ein Tag']()
|
||||
assert.equal(ein.template, 'K25')
|
||||
assert.equal(ein.values.tage, 'Sa 03.10. – Tag der Deutschen Einheit')
|
||||
assert.deepEqual(an(ein), [])
|
||||
assert.deepEqual(ein.blocks, { pruefen: false, mehrere: false })
|
||||
const textEin = render(ein).text
|
||||
assert.match(textEin, /Betrifft: Sa 03\.10\. – Tag der Deutschen Einheit/)
|
||||
assert.doesNotMatch(textEin, /bitte prüfen|mehrere Tage/)
|
||||
assert.match(textEin, /Stimmen Deine Zeiten/)
|
||||
assert.match(textEin, /Google/, 'Hinweis: bei Google auch eintragen')
|
||||
|
||||
const viele = BEISPIELE['K25 mehrere mit prüfen']()
|
||||
assert.equal(viele.values.tage, 'Do 24.12. – Heiligabend (bitte prüfen)\nFr 25.12. – 1. Weihnachtstag')
|
||||
assert.deepEqual(viele.blocks, { pruefen: true, mehrere: true })
|
||||
const mail = render(viele)
|
||||
assert.match(mail.text, /Betrifft: Do 24\.12\. – Heiligabend \(bitte prüfen\)\nFr 25\.12\. – 1\. Weihnachtstag/)
|
||||
assert.match(mail.text, /mehrere Tage/)
|
||||
assert.match(mail.text, /Silvester/, 'Erklärung der „bitte prüfen“-Tage')
|
||||
assert.match(mail.html, /Heiligabend \(bitte prüfen\)<br>Fr 25\.12\./, 'Zeilenumbrüche werden zu <br>')
|
||||
})
|
||||
|
||||
test('K25: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woche', () => {
|
||||
const nachricht = k25Mail({
|
||||
kunde: 'K',
|
||||
projekt: 'P',
|
||||
tage: [
|
||||
{ tag: '2026-10-05', name: 'A <b>fett</b>', art: 'feiertag' },
|
||||
{ tag: '2026-10-06', name: 'B', art: 'feiertag' },
|
||||
{ tag: '2026-10-07', name: 'C', art: 'feiertag' },
|
||||
{ tag: '2026-10-08', name: 'D', art: 'feiertag' },
|
||||
{ tag: '2026-10-09', name: 'E', art: 'feiertag' },
|
||||
{ tag: '2026-10-10', name: 'F', art: 'feiertag' },
|
||||
{ tag: '2026-10-11', name: 'G', art: 'feiertag' },
|
||||
],
|
||||
})
|
||||
assert.deepEqual(nachricht.values.tage.split('\n').map((zeile) => zeile.slice(0, 9)), ['Mo 05.10.', 'Di 06.10.', 'Mi 07.10.', 'Do 08.10.', 'Fr 09.10.', 'Sa 10.10.', 'So 11.10.'])
|
||||
const mail = render(nachricht)
|
||||
assert.match(mail.html, /A <b>fett<\/b>/)
|
||||
assert.doesNotMatch(mail.html, /<b>fett/)
|
||||
})
|
||||
|
||||
test('K25 geht ohne BCC ans Team, K24 mit', async () => {
|
||||
const k25 = await sendeZeitenMail(KUNDE, BEISPIELE['K25 ein Tag'](), { bccTeam: false })
|
||||
assert.deepEqual(k25, { sent: true })
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.deepEqual(sink.sent[0].to.map((t) => t.address), ['silke@example.com'])
|
||||
assert.ok(!sink.sent[0].bcc, 'keine Kopie ans Team')
|
||||
assert.equal(sink.sent[0].subject, 'Feiertage stehen an – stimmen Deine Zeiten?')
|
||||
|
||||
assert.deepEqual(await sendeZeitenMail(KUNDE, BEISPIELE['K24 angestossen']()), { sent: true })
|
||||
assert.equal(sink.sent.length, 2)
|
||||
assert.deepEqual(sink.sent[1].bcc.map((t) => t.address), config.chatNotify.emails, 'Standard: Kopie ans Team')
|
||||
assert.ok(sink.sent[1].bcc.length > 0)
|
||||
assert.equal(sink.sent[1].subject, 'Öffnungszeiten live ist eingeschaltet')
|
||||
assert.match(sink.sent[1].html, /Hallo Silke Müller,/)
|
||||
})
|
||||
|
||||
test('K26: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf', () => {
|
||||
const faelle = [
|
||||
['K26 beendet', { abo_ende: true, hosting: false, wechsel_widerrufen: false }, /Dein Wartungs-Abo ist beendet/, /Hosting-Abo|Wechsel/],
|
||||
['K26 hosting', { abo_ende: false, hosting: true, wechsel_widerrufen: false }, /Hosting-Abo/, /Dein Wartungs-Abo ist beendet|widerrufen/],
|
||||
['K26 Wechsel widerrufen', { abo_ende: false, hosting: false, wechsel_widerrufen: true }, /Wechsel.*widerrufen/, /Dein Wartungs-Abo ist beendet|Dein Abo ist ein Hosting-Abo/],
|
||||
]
|
||||
for (const [name, blocks, passt, fremd] of faelle) {
|
||||
const nachricht = BEISPIELE[name]()
|
||||
assert.equal(nachricht.template, 'K26')
|
||||
assert.deepEqual(nachricht.blocks, blocks, name)
|
||||
assert.equal(nachricht.values.frist, '04.11.2026', `${name}: Datum TT.MM.JJJJ`)
|
||||
assert.equal(nachricht.values.preis, '30 €')
|
||||
const text = render(nachricht).text
|
||||
assert.match(text, passt, name)
|
||||
assert.doesNotMatch(text, fremd, name)
|
||||
assert.match(text, /04\.11\.2026/, name)
|
||||
assert.match(text, /einmalig 30 €/, name)
|
||||
assert.match(text, /Für 30 € behalten: .*#zeiten/, `${name}: Knopf zum Behalten`)
|
||||
}
|
||||
// unbekannte Lage: lieber die Standard-Fassung (Abo beendet) als gar kein Grund
|
||||
assert.deepEqual(k26Mail({ kunde: 'K', projekt: 'P', art: 'was-neues', frist: FRIST, preis: '30 €' }).blocks, { abo_ende: true, hosting: false, wechsel_widerrufen: false })
|
||||
assert.equal(render(BEISPIELE['K26 beendet']()).subject, 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?')
|
||||
})
|
||||
|
||||
test('K26/K27: Datum aus Date (Berliner Tag), ungültiges Datum wirft früh', () => {
|
||||
// Berliner Tag, nicht UTC: 22:30 UTC am 30.10. ist 23:30 MEZ (noch der 30.), 23:30 UTC am 31.10. ist 00:30 MEZ (schon der 1.11.)
|
||||
assert.equal(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-30T22:30:00.000Z'), preis: '30 €' }).values.frist, '30.10.2026')
|
||||
assert.equal(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-31T23:30:00.000Z'), preis: '30 €' }).values.frist, '01.11.2026')
|
||||
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('kaputt'), preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k27Mail({ kunde: 'K', projekt: 'P', loeschdatum: 'morgen', preis: '30 €' }), TypeError)
|
||||
})
|
||||
|
||||
test('K27: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalten für den Preis', () => {
|
||||
const nachricht = BEISPIELE.K27()
|
||||
assert.equal(nachricht.template, 'K27')
|
||||
assert.equal(nachricht.values.loeschdatum, '04.11.2026')
|
||||
assert.equal(nachricht.values.preis, '30 €')
|
||||
const mail = render(nachricht)
|
||||
assert.equal(mail.subject, 'Öffnungszeiten live ist abgeschaltet')
|
||||
assert.match(mail.text, /wieder feste Zeiten/)
|
||||
assert.match(mail.text, /04\.11\.2026/)
|
||||
assert.match(mail.text, /für einmalig 30 € wieder einschalten/)
|
||||
assert.match(mail.text, /Projekt: Praxis Muster/)
|
||||
})
|
||||
|
||||
test('K26 und K27 gehen mit Kopie ans Team', async () => {
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE['K26 beendet']())).sent, true)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K27())).sent, true)
|
||||
for (const mail of sink.sent) assert.ok(mail.bcc?.length > 0)
|
||||
})
|
||||
|
||||
test('ohne E-Mail-Adresse: nichts senden, nichts werfen', async () => {
|
||||
for (const kunde of [{ name: 'Silke' }, { name: 'Silke', email: '' }, { email: ' ' }, { email: null }, null, undefined]) {
|
||||
assert.deepEqual(await sendeZeitenMail(kunde, BEISPIELE.K27()), { sent: false, reason: 'keine_adresse' }, JSON.stringify(kunde))
|
||||
}
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Versandfehler werden gemeldet, nie geworfen; der Grund nennt keine Adresse', async () => {
|
||||
setMailTransportForTests({
|
||||
sendMail: async () => {
|
||||
throw new Error('550 Recipient silke@example.com rejected for Silke Müller')
|
||||
},
|
||||
})
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE.K27())
|
||||
assert.equal(ergebnis.sent, false)
|
||||
assert.equal(ergebnis.reason, 'fehler')
|
||||
assert.doesNotMatch(JSON.stringify(ergebnis), /silke|Müller/i)
|
||||
// auch ohne SMTP-Konfiguration (sendCustomerMail wirft) und mit unbrauchbarer Nachricht
|
||||
setMailTransportForTests(null)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K27())).sent, false)
|
||||
setMailTransportForTests(sink)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, undefined)).sent, false)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, { template: 'K99' })).sent, false)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Log enthält weder Adresse noch Namen', async () => {
|
||||
setMailTransportForTests({
|
||||
sendMail: async () => {
|
||||
throw new Error('550 Recipient silke@example.com rejected for Silke Müller (Praxis Muster)')
|
||||
},
|
||||
})
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE['K26 beendet']())
|
||||
assert.equal(ergebnis.sent, false)
|
||||
const text = logText()
|
||||
assert.match(text, /K26/, 'das Log nennt die Vorlage')
|
||||
assert.ok(text.length > 0)
|
||||
assert.doesNotMatch(text, /silke|example\.com|Müller/i, text)
|
||||
// auch der Fall ohne Adresse loggt höchstens ohne Namen
|
||||
await sendeZeitenMail({ name: 'Silke Müller' }, BEISPIELE.K27())
|
||||
assert.doesNotMatch(logText(), /silke|Müller/i)
|
||||
})
|
||||
|
||||
test('Erfolg schreibt nichts ins Log', async () => {
|
||||
await sendeZeitenMail(KUNDE, BEISPIELE['K24 angestossen']())
|
||||
assert.equal(logText(), '')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- K15 behalten, T16 abo/team ---
|
||||
test('K15: Block behalten ersetzt den Einrichtungstext, ohne ihn bleibt die Mail wie gehabt', () => {
|
||||
const feature = reg.featureConfig('zeiten')
|
||||
const ctx = { kundeName: 'Silke Müller', projekt: 'Praxis Muster', invoiceUrl: 'https://invoice.stripe.test/i/1' }
|
||||
const neu = sf.bestellMail(feature, ctx)
|
||||
assert.deepEqual(neu.blocks, { rechnung: true, behalten: false, neu: true })
|
||||
assert.deepEqual(sf.bestellMail(feature, { ...ctx, behalten: false }).blocks, neu.blocks)
|
||||
const behalten = sf.bestellMail(feature, { ...ctx, behalten: true })
|
||||
assert.deepEqual(behalten.blocks, { rechnung: true, behalten: true, neu: false })
|
||||
assert.deepEqual(behalten.values, neu.values, 'dieselben Werte, nur der Wortlaut wechselt')
|
||||
|
||||
const textNeu = render(neu)
|
||||
assert.deepEqual(textNeu.problems, [])
|
||||
assert.match(textNeu.text, /Wir richten Öffnungszeiten live jetzt für Dein Projekt ein\./)
|
||||
assert.match(textNeu.text, /Funktion: Öffnungszeiten live – Einrichtung/)
|
||||
assert.match(textNeu.text, /Einrichtung: meist innerhalb weniger Tage/)
|
||||
assert.match(textNeu.text, /Sobald Öffnungszeiten live auf Deiner Website eingebaut ist/)
|
||||
assert.doesNotMatch(textNeu.text, /behält|behalten/)
|
||||
|
||||
const mail = render(behalten)
|
||||
assert.deepEqual(mail.problems, [])
|
||||
assert.equal(mail.subject, 'Deine Bestellung: Öffnungszeiten live')
|
||||
assert.match(mail.text, /Hallo Silke Müller,\nvielen Dank für Deine Bestellung und Deine Zahlung\. Du behältst Öffnungszeiten live für Dein Projekt/)
|
||||
assert.match(mail.text, /Funktion: Öffnungszeiten live – behalten/)
|
||||
assert.match(mail.text, /Einmalig bezahlt: 30,00 €/)
|
||||
assert.match(mail.text, /nichts neu eingebaut/)
|
||||
assert.match(mail.text, /Rechnung ansehen: https:\/\/invoice\.stripe\.test\/i\/1/)
|
||||
assert.ok(mail.text.includes(`Zum Kundenportal: ${config.publicBaseUrl}/dashboard.html#zeiten`))
|
||||
// nichts vom Einrichtungsablauf in der Behalten-Fassung
|
||||
assert.doesNotMatch(mail.text, /Wir richten|Einrichtung|Sobald|eingebaut ist/)
|
||||
})
|
||||
|
||||
test('K15: die Blöcke behalten und neu stehen im Kopf der Vorlage', () => {
|
||||
const tpl = loadTemplate('K15')
|
||||
assert.deepEqual(tpl.blocks, ['rechnung', 'behalten', 'neu'])
|
||||
// jeder Block erscheint, und in der Behalten-Fassung (neu aus) fehlt jeder Rest von zeit/danach
|
||||
const behalten = renderMail('K15', Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`])), { behalten: true })
|
||||
assert.deepEqual(behalten.problems, [])
|
||||
assert.doesNotMatch(behalten.text, /Wert-zeit|Wert-danach/)
|
||||
})
|
||||
|
||||
test('T16: Blöcke abo und team nennen den Weg ohne Zahlung', () => {
|
||||
const tpl = loadTemplate('T16')
|
||||
assert.deepEqual(tpl.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team'])
|
||||
const werte = { kunde_name: 'Silke Müller', email: 'silke@example.com', projekt: 'Praxis Muster', funktion: 'Öffnungszeiten live', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: '', gesundheit_text: '' }
|
||||
const aus = { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false }
|
||||
const abo = renderMail('T16', werte, { ...aus, abo: true, team: false })
|
||||
assert.deepEqual(abo.problems, [])
|
||||
assert.match(abo.text, /im Wartungs-Abo enthalten/i)
|
||||
assert.doesNotMatch(abo.text, /vom Team freigeschaltet/i)
|
||||
const team = renderMail('T16', werte, { ...aus, abo: false, team: true })
|
||||
assert.deepEqual(team.problems, [])
|
||||
assert.match(team.text, /vom Team freigeschaltet/i)
|
||||
assert.doesNotMatch(team.text, /im Wartungs-Abo enthalten/i)
|
||||
const keiner = renderMail('T16', werte, aus)
|
||||
assert.doesNotMatch(keiner.text, /im Wartungs-Abo enthalten|vom Team freigeschaltet/i)
|
||||
// sendT16 führt beide Blöcke als Vorgabe (aus), damit jede Team-Mail sie nennt
|
||||
assert.match(readFileSync(new URL('../server/services/siteFeatures.js', import.meta.url), 'utf8'), /gesundheit: false, abo: false, team: false, \.\.\.bloecke/)
|
||||
})
|
||||
Reference in new issue
Block a user