Terminplaner: Kunden-Schnittstelle

/api/termine: Einstellungen (Vorlage, PATCH mit Version, Feed-Link), Buchungen
(Liste, Zeiten, von Hand, Sperre, Aktionen), CSV-Export, Protokoll der
Kundenansicht. Pflichtangaben fuer Gaeste (Telefon immer, Anschrift bei
Widerrufsbelehrung) in normalisiereEinstellung, Entwurf aus Vorlagen,
kontextPerSiteKey ohne Pflichtangaben nicht buchbar; speichereEinstellung
nimmt eine Funktion unter der Sperre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:43 +00:00
1 parent 9e8fd33713
commit e3e198fb1a
10 files changed
+1766 -40

No files matched your search

+43 -2
View File
@@ -475,9 +475,12 @@ const MELDUNG_RESSOURCE = (r, n) => (r.typ === 'platz'
* Prueft und normalisiert eine Einstellung (Eingabe vom Client oder aus einer Vorlage).
* bisher = zuletzt gespeicherte Einstellung (Feed-Token, Storno-Fassung, Entfernen-Pruefung);
* kuenftigeTermine = { leistungen: { [id]: Anzahl }, ressourcen: { [id]: Anzahl } } kuenftiger belegender Termine.
* Pflichtangaben fuer Gaeste (fehlendePflichtangaben): Telefon immer, Anschrift bei Widerrufsbelehrung. entwurf: true
* laesst nur diese beiden Pruefungen aus (Entwurf aus einer Vorlage, Kunde ohne Telefon/Anschrift im Konto); als
* oeffentlich buchbar gilt eine solche Einstellung erst, wenn die Angaben da sind (kontextPerSiteKey).
* => { einstellung } oder { fehler: { [pfad]: text } }.
*/
export function normalisiereEinstellung(eingabe, { bisher = null, kuenftigeTermine = null, now } = {}) {
export function normalisiereEinstellung(eingabe, { bisher = null, kuenftigeTermine = null, entwurf = false, now } = {}) {
if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw new TypeError('normalisiereEinstellung: now fehlt')
const fehler = {}
const fehl = (pfad, meldung) => { if (!Object.hasOwn(fehler, pfad)) fehler[pfad] = meldung }
@@ -534,6 +537,10 @@ export function normalisiereEinstellung(eingabe, { bisher = null, kuenftigeTermi
aufbewahrungTage: zahl(fehl, 'aufbewahrungTage', eingabe.aufbewahrungTage, { vorgabe: 90, min: 14, max: 365, meldung: 'Die Aufbewahrung muss zwischen 14 und 365 Tagen liegen.' }),
}
if (!entwurf) {
for (const pfad of fehlendePflichtangaben(einstellung)) fehl(pfad, MELDUNG_PFLICHT[pfad])
}
if (Object.keys(fehler).length === 0 && JSON.stringify(einstellung).length > GRENZEN.json) {
fehl('_', 'Die Einstellungen sind zu umfangreich. Bitte entferne Zeitfenster oder Einträge.')
}
@@ -550,6 +557,40 @@ function farbe(fehl, wert) {
return s
}
// ---------------------------------------------------------------- Pflichtangaben und Widerruf
const MELDUNG_PFLICHT = {
'betrieb.telefon': 'Bitte gib die Telefonnummer deines Betriebs an. Gäste sehen sie als Ausweichweg und in ihren Mails.',
'betrieb.adresse': 'Bitte gib die Anschrift deines Betriebs an. Sie steht in der Widerrufsbelehrung, die Gäste vor der Buchung sehen.',
}
/** Leistungen, die Gaeste sehen: aktiv und mit mindestens einer aktiven Ressource (wie in oeffentlicheEinstellung). */
function sichtbareLeistungen(einstellung) {
const aktiveIds = new Set((einstellung?.ressourcen || []).filter((r) => r?.aktiv).map((r) => r.id))
return (einstellung?.leistungen || []).filter((l) => l?.aktiv && (l.ressourcen || []).some((id) => aktiveIds.has(id)))
}
/**
* Zeigt die oeffentliche Sicht eine Widerrufsbelehrung (oeffentlicheEinstellung.widerruf)? Verbraucher-Gaeste und
* mindestens eine sichtbare Leistung, die sofort gebucht wird.
*/
export function zeigtWiderruf(einstellung) {
return istVerbraucher(einstellung) && sichtbareLeistungen(einstellung).some((l) => l.buchungsart === 'sofort')
}
/**
* Pflichtangaben des Betriebs, die in einer Einstellung fehlen: 'betrieb.telefon' (immer noetig) und 'betrieb.adresse'
* (noetig, sobald die Widerrufsbelehrung erscheint, zeigtWiderruf). Reihenfolge Telefon, Anschrift; [] = vollstaendig.
* Ohne diese Angaben ist der Terminplaner fuer Gaeste nicht buchbar.
*/
export function fehlendePflichtangaben(einstellung) {
const betrieb = einstellung?.betrieb || {}
const fehlt = []
if (typeof betrieb.telefon !== 'string' || betrieb.telefon.trim() === '') fehlt.push('betrieb.telefon')
if (zeigtWiderruf(einstellung) && (typeof betrieb.adresse !== 'string' || betrieb.adresse.trim() === '')) fehlt.push('betrieb.adresse')
return fehlt
}
// ---------------------------------------------------------------- Texte und Regeln
/** Hat die Leistung einen Preis (fest/ab und mehr als 0)? */
@@ -652,7 +693,7 @@ export function oeffentlicheEinstellung(einstellung, now) {
zahlung: { modus: 'vorort', text: zahlungText(e) },
preisHinweis: preisHinweis(e),
storno: e.storno?.text ? { text: e.storno.text, version: e.storno.version } : null,
widerruf: verbraucher && leistungen.some((l) => l.buchungsart === 'sofort'),
widerruf: zeigtWiderruf(e),
erinnerung: { anbieten: e.erinnerung.anbieten, stundenVorher: e.erinnerung.stundenVorher },
gastFelder: structuredClone(e.gastFelder),
}