Files
Webklar-Kundenbereich/server/services/zeiten/mails.js
T
ClaudeandClaude Opus 5.5 0042d8dd8f Ö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>
2026-10-05 01:33:04 +00:00

149 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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' }
}
}