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 * K40, K41 dieselben Mails im Abo-Modell der Kundenansicht neu (k29Mail und k30Mail mit modell: true): ohne Preis und ohne Behalten * 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 '' (renderMail macht aus „Hallo ,“ dann „Hallo,“, wie bei 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, modell?: boolean }} 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). * modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K40 ohne Preis und ohne Behalten, die Blöcke bleiben gleich. */ export function k29Mail({ kunde, projekt, art, frist, preis, modell = false }) { const blocks = { abo_ende: art !== 'hosting' && art !== 'wechsel_widerrufen', hosting: art === 'hosting', wechsel_widerrufen: art === 'wechsel_widerrufen' } if (modell) { return { template: 'K40', values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), portal_url: portalUrl() }, blocks } } return { template: 'K29', values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), preis, portal_url: portalUrl() }, blocks, } } /** * K30: Öffnungszeiten live ist abgeschaltet (Wartungs-Abo beendet, Frist zum Behalten abgelaufen). * @param {{ kunde: string | { name?: string }, projekt: string, loeschdatum: Date, preis: string, modell?: boolean }} p * loeschdatum: Tag, ab dem die Eingaben gelöscht werden (30 Tage nach dem Abschalten); preis: Preis zum Wiedereinschalten. * modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K41 ohne Preis. */ export function k30Mail({ kunde, projekt, loeschdatum, preis, modell = false }) { if (modell) { return { template: 'K41', values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), portal_url: portalUrl() } } } 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' } } }