Files
Webklar-Kundenbereich/server/services/zeiten/mails.js
T
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00

153 lines
7.7 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/K27 bis K30):
* K27 eingeschaltet (Gratisweg im Wartungs-Abo; mit Block `wieder` nach der Rückkehr des Abos)
* K28 Feiertags-Erinnerung (eine Mail mit allen neuen Tagen, ohne BCC)
* K29 Wartungs-Abo beendet: Funktion läuft bis zur Frist, „für einmalig 30 € behalten“ (Varianten nach der Abo-Lage)
* K30 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)
}
/**
* K27: Ö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.
* Ausnahme: einbau null zusammen mit wieder = es gab keinen Neubau (das Abo kehrte vor dem Abschalten zurück, die Leisten waren nie weg):
* beide Einbau-Blöcke und der ganze Kasten „Wie geht es weiter?“ (Block weiter) entfallen.
* 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 k27Mail({ kunde, projekt, einbau, wieder = false, ticketNr = null }) {
const nr = String(ticketNr ?? '').trim().replace(/^#/, '')
const keinNeubau = wieder === true && einbau === null
const angestossen = einbau === 'angestossen'
const manuell = !keinNeubau && !angestossen
return {
template: 'K27',
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, wieder: Boolean(wieder), weiter: angestossen || manuell },
}
}
/** 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
}
/**
* K28: 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 k28Mail({ kunde, projekt, tage }) {
const liste = Array.isArray(tage) ? tage : []
return {
template: 'K28',
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 },
}
}
/**
* K29: 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 k29Mail({ kunde, projekt, art, frist, preis }) {
return {
template: 'K29',
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' },
}
}
/**
* K30: Ö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 k30Mail({ kunde, projekt, loeschdatum, preis }) {
return {
template: 'K30',
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 K27 bis K30 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 k27Mail ... k30Mail
* @param {{ bccTeam?: boolean }} [options] bccTeam false: keine Kopie ans Team (K28)
* @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' }
}
}