Terminplaner: Einbauanleitung als reiner Generator (Einbau bis Abrechnung, Warnungen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
13bbadc729
commit
90692b646b
2 files changed
+706
-33
No files matched your search
@@ -1,6 +1,10 @@
|
||||
import { config } from '../../config.js'
|
||||
import { VORGABE_FARBE, preisHinweis, preisText, sichtbareLeistungen, zahlungText, zeigtWiderruf } from './einstellungen.js'
|
||||
import { RECHTSTEXTE_ENTWURF } from './rechtstexte.js'
|
||||
import { anleitungsLink } from '../featureRegistry.js'
|
||||
import { gesundheitsWarnungen } from './fragebogen.js'
|
||||
import {
|
||||
VORGABE_FARBE, fehlendePflichtangaben, hatPreis, preisHinweis, preisText, sichtbareLeistungen, text as bereinigterText, zahlungText, zeigtWiderruf,
|
||||
} from './einstellungen.js'
|
||||
import { RECHTSTEXTE_ENTWURF, datenschutzAbsatz } from './rechtstexte.js'
|
||||
import { datumKurz, heute } from './zeit.js'
|
||||
|
||||
/**
|
||||
@@ -12,7 +16,8 @@ import { datumKurz, heute } from './zeit.js'
|
||||
* des Portals (`apiBase`) und alle Werte kommen als Parameter. Der Einbau-Code stand bis TP2 in routes/termine.js;
|
||||
* GET /api/termine/einstellungen ruft ihn von hier auf (Modus inline, die Kundenansicht bleibt unveraendert).
|
||||
* - Generator einbauanleitung() (ganz unten): setzt aus den Einstellungen des Betriebs die Anleitung fuer das Team zusammen.
|
||||
* Auch er ist rein (kein I/O, keine Zufallswerte); config wird nur beim Aufruf gelesen (Portal-Adresse, Haltezeit).
|
||||
* Auch er ist rein (kein I/O, keine Zufallswerte); config wird nur beim Aufruf gelesen (Portal-Adresse, Haltezeit, WOMS-Adresse,
|
||||
* Preis). Er nutzt die reinen Hilfen gesundheitsWarnungen (fragebogen.js) und datenschutzAbsatz (rechtstexte.js).
|
||||
*
|
||||
* Die Farbfunktionen (luminanz, textFarbeAuf) sind 1:1 die aus public/embed/termine.js (Marken @farben-anfang/@farben-ende),
|
||||
* bewusst kopiert und nicht importiert: das Widget ist ein eigenstaendiges Browser-Skript. test/termineEmbedStatic.test.js
|
||||
@@ -127,13 +132,18 @@ export function cssVorschlag(farbe) {
|
||||
}
|
||||
|
||||
// ================================================================ Generator einbauanleitung()
|
||||
// Task D3a: Modell, Leistungen, Ressourcen, Regeln, Gastfelder, Benachrichtigungen. Task D3b haengt Einbau bis Abrechnung, die
|
||||
// Warnungen und den vollen Volltext an, Task D4 die Kurzfassung (`kurz`).
|
||||
// Task D3a: Modell, Leistungen, Ressourcen, Regeln, Gastfelder, Benachrichtigungen. Task D3b: Einbau, Snippet, CSS, Datenschutz-Absaetze,
|
||||
// Checkliste, Abrechnung, die Warnungen und der Volltext. Task D4 haengt die Kurzfassung (`kurz`) an.
|
||||
//
|
||||
// Feldweissliste: gelesen werden nur ausdruecklich benannte Felder, nie ganze Objekte. Nie ausgegeben werden feedToken (Kalender-
|
||||
// Abo der Ressource), die Betriebs-E-Mail (sie steht nur im Datenschutz-Absatz, D3b), Beschreibungen der Leistungen und alles,
|
||||
// was sonst an den Objekten haengt. Kundentexte (Namen, Titel, Storno-Text) stehen unmaskiert als Text; wer sie anzeigt, setzt
|
||||
// sie als Text ein. Einzeilige Werte werden auf eine Zeile gebracht, damit ein Name keine Ueberschrift vortaeuschen kann.
|
||||
// Abo der Ressource), die Betriebs-E-Mail (sie steht nur in den Datenschutz-Absaetzen), Beschreibungen der Leistungen und alles,
|
||||
// was sonst an den Objekten haengt. Kundentexte (Namen, Titel, Storno-Text, Wuensche zum Einbau) stehen unmaskiert als Text; wer sie
|
||||
// anzeigt, setzt sie als Text ein. Einzeilige Werte werden auf eine Zeile gebracht, mehrzeilige eingerueckt, damit ein Kundentext
|
||||
// keine Ueberschrift vortaeuschen kann. Wuensche des Kunden stehen nie im Snippet und nie im CSS (nur der Knopf-Text, maskiert,
|
||||
// als data-knopf).
|
||||
|
||||
/** Kennzeichnung der Gaestetexte als Entwurf; erste Warnung, solange `entwurf` gilt (Standard RECHTSTEXTE_ENTWURF). */
|
||||
export const ENTWURF_BANNER = 'ENTWURF – nur Pilot, vor Einsatz bei fremden Betrieben prüfen'
|
||||
|
||||
const WOCHENTAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
|
||||
const TAG_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||
@@ -179,7 +189,7 @@ export function kuerzeListe(eintraege, max) {
|
||||
return [...eintraege.slice(0, max), `… und ${eintraege.length - max} weitere`]
|
||||
}
|
||||
|
||||
const neuerAbschnitt = (id, titel, text) => ({ id, titel, text, kopierbar: false })
|
||||
const neuerAbschnitt = (id, titel, text, kopierbar = false) => ({ id, titel, text, kopierbar })
|
||||
|
||||
/** Zeilen als Aufzaehlung: '- Eintrag'. */
|
||||
const aufzaehlung = (eintraege) => eintraege.map((eintrag) => `- ${eintrag}`)
|
||||
@@ -373,9 +383,197 @@ function abschnittBenachrichtigungen(d) {
|
||||
].join('\n\n'))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Wuensche des Kunden (Fragebogen, Schritt Einbau)
|
||||
|
||||
const EINBAU_MODI = ['inline', 'knopf']
|
||||
const MODUS_TEXT = {
|
||||
inline: 'inline – die Buchungsstrecke steht direkt in der Seite',
|
||||
knopf: 'knopf – ein Knopf öffnet die Buchungsstrecke in einem Dialog',
|
||||
}
|
||||
// Grenzen wie in fragebogen.js (TEXTGRENZEN): hier erneut angewandt, die Anleitung traut dem gespeicherten Fragebogen nicht blind
|
||||
const WUNSCH_GRENZEN = { knopfText: 40, stelle: 300, gestaltung: 1000, bemerkung: 1000 }
|
||||
const WUNSCH_MEHRZEILIG = new Set(['stelle', 'gestaltung', 'bemerkung'])
|
||||
|
||||
/** Modus (ohne Fragebogen oder ohne gueltige Wahl: inline) und die vier Wunschtexte, bereinigt, gekuerzt und wohlgeformt. */
|
||||
function kundenwunsch(fragebogen) {
|
||||
const einbau = istObjekt(fragebogen) && istObjekt(fragebogen.einbau) ? fragebogen.einbau : {}
|
||||
const modusGewaehlt = EINBAU_MODI.includes(einbau.modus)
|
||||
const wunsch = { modus: modusGewaehlt ? einbau.modus : 'inline', modusGewaehlt }
|
||||
for (const [feld, max] of Object.entries(WUNSCH_GRENZEN)) {
|
||||
wunsch[feld] = bereinigterText(einbau[feld], max, { mehrzeilig: WUNSCH_MEHRZEILIG.has(feld) }).toWellFormed()
|
||||
}
|
||||
return wunsch
|
||||
}
|
||||
|
||||
/** ' Stelle: erste Zeile' und weitere Zeilen um vier Leerzeichen eingerueckt (nie am Zeilenanfang, also nie eine Ueberschrift). */
|
||||
function wunschZeilen(bezeichnung, wert) {
|
||||
if (!wert) return []
|
||||
const [erste, ...weitere] = wert.split('\n')
|
||||
return [` ${bezeichnung}: ${erste}`, ...weitere.map((z) => (z === '' ? '' : ` ${z}`))]
|
||||
}
|
||||
|
||||
function kundenwunschBlock(wunsch) {
|
||||
const zeilen = [
|
||||
...wunschZeilen('Knopf-Text', wunsch.knopfText),
|
||||
...wunschZeilen('Stelle', wunsch.stelle),
|
||||
...wunschZeilen('Gestaltung', wunsch.gestaltung),
|
||||
...wunschZeilen('Bemerkung', wunsch.bemerkung),
|
||||
]
|
||||
return zeilen.length ? ['Kundenwunsch (Text des Kunden):', ...zeilen].join('\n') : 'Kundenwunsch (Text des Kunden): keine Angaben im Fragebogen'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Abschnitte Einbau, Snippet, CSS
|
||||
|
||||
function abschnittEinbau(d) {
|
||||
const w = d.wunsch
|
||||
const quelle = w.modusGewaehlt ? 'Wahl des Kunden im Fragebogen' : 'Standard, der Fragebogen nennt keinen Modus'
|
||||
const telefon = zeile(d.betrieb.telefon) || NICHT_ANGEGEBEN
|
||||
const impressum = zeile(d.betrieb.impressumUrl) || NICHT_ANGEGEBEN
|
||||
const datenschutz = zeile(d.betrieb.datenschutzUrl) || NICHT_ANGEGEBEN
|
||||
return neuerAbschnitt('einbau', 'Einbau', [
|
||||
`siteKey: ${d.siteKey || NICHT_ANGEGEBEN}`,
|
||||
`Modus: ${MODUS_TEXT[w.modus]} (${quelle})`,
|
||||
['So wird eingebaut:', ...aufzaehlung([
|
||||
'Platzhalter und Skript aus dem Abschnitt „Einbau-Code (Snippet)“ in die Seite setzen: den Platzhalter an die Stelle, die der Kunde nennt '
|
||||
+ '(siehe Kundenwunsch), das Skript einmal pro Seite, am besten vor dem schließenden body-Tag. Das Skript hat keine Abhängigkeiten, '
|
||||
+ 'setzt keine Cookies und lädt nichts von Dritten.',
|
||||
'Der Ausweichtext im Platzhalter gehört der Website: Er bleibt ohne JavaScript, bei falschem siteKey, abgeschalteter Funktion, fehlenden '
|
||||
+ `Pflichtangaben und Netzfehlern stehen. Telefon des Betriebs: ${telefon}. Gestalte ihn im Stil der Seite.`,
|
||||
'Aussehen: den Abschnitt „Aussehen (CSS)“ in die CSS der Website übernehmen. Weitere Variablen stehen in docs/funktionen/termine-einbau.md, Abschnitt 3.',
|
||||
'Datenschutz: den Absatz „Online-Terminbuchung“ (Abschnitte „Datenschutz-Absatz für die Website“) in die Datenschutzerklärung der Website aufnehmen, '
|
||||
+ 'bevor das Widget freigeschaltet wird.',
|
||||
`Impressum und Datenschutzerklärung: Die URLs stehen in den Betriebsangaben im Portal (Impressum: ${impressum}; Datenschutzerklärung: ${datenschutz}), `
|
||||
+ 'das Widget verlinkt sie im Fuß.',
|
||||
])].join('\n'),
|
||||
kundenwunschBlock(w),
|
||||
].join('\n\n'))
|
||||
}
|
||||
|
||||
/** Einbau-Code aus den Angaben des Betriebs und dem Modus des Fragebogens; Wuensche ausser dem Knopf-Text kommen nie hinein. */
|
||||
const baueSnippet = (d) =>
|
||||
einbauSnippet({ siteKey: d.siteKey, apiBase: d.portal, telefon: d.telefon, modus: d.wunsch.modus, knopfText: d.wunsch.knopfText })
|
||||
|
||||
const abschnittSnippet = (d) => neuerAbschnitt('snippet', 'Einbau-Code (Snippet)', baueSnippet(d), true)
|
||||
|
||||
const abschnittCss = (d) => neuerAbschnitt('css', 'Aussehen (CSS)', cssVorschlag(d.farbe).css, true)
|
||||
|
||||
// ---------------------------------------------------------------- Abschnitte Datenschutz-Absatz (Sie, Du)
|
||||
|
||||
const DATENSCHUTZ_HINWEIS =
|
||||
'Der Datenschutz-Absatz konnte nicht erzeugt werden: In den Einstellungen fehlt der Name oder die E-Mail-Adresse des Betriebs. '
|
||||
+ 'Beides im Portal eintragen lassen und die Anleitung neu laden.'
|
||||
|
||||
/** { sie, du } oder null, wenn Name oder E-Mail-Adresse des Betriebs fehlen (datenschutzAbsatz wirft dann einen TypeError). */
|
||||
function datenschutzTexte(einstellung) {
|
||||
try {
|
||||
return { sie: datenschutzAbsatz(einstellung, 'sie'), du: datenschutzAbsatz(einstellung, 'du') }
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/** Nur ein echter Absatz ist kopierbar; der Hinweis, dass er fehlt, nie (er gehoert nicht in eine Datenschutzerklaerung). */
|
||||
const abschnittDatenschutz = (anrede) => (d) => {
|
||||
const id = `datenschutz_${anrede}`
|
||||
const titel = `Datenschutz-Absatz für die Website (${anrede === 'du' ? 'Du' : 'Sie'}-Form)`
|
||||
const absatz = d.datenschutz?.[anrede]
|
||||
return absatz ? neuerAbschnitt(id, titel, absatz, true) : neuerAbschnitt(id, titel, DATENSCHUTZ_HINWEIS)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Abschnitt Abnahme-Checkliste
|
||||
|
||||
// Zwischenregel bis Block F (F10b2 ersetzt sie durch die Endregel aus F4: WEBklar fuegt den Absatz ein, wenn die Website von WEBklar betreut wird)
|
||||
const DATENSCHUTZ_ZWISCHENREGEL = 'liegt die Datenschutzerklärung nicht in unserem Repo: Absatz per Chat an den Kunden senden'
|
||||
|
||||
function abschnittCheckliste(d) {
|
||||
const { e } = d
|
||||
const widerruf = zeigtWiderruf(e)
|
||||
const preis = sichtbareLeistungen(e).some(hatPreis)
|
||||
const knopf = d.wunsch.modus === 'knopf'
|
||||
const impressum = zeile(d.betrieb.impressumUrl) || 'fehlt'
|
||||
const datenschutz = zeile(d.betrieb.datenschutzUrl) || 'fehlt'
|
||||
const wo = d.anleitungsLink ? ` (Kunde → Reiter Funktionen, Zeile Terminplaner: ${d.anleitungsLink})` : ''
|
||||
const punkte = [
|
||||
// vor der Freischaltung
|
||||
'Einbau-Code eingefügt (Abschnitt „Einbau-Code (Snippet)“). Das Widget bleibt unsichtbar bis zur Freischaltung: Bis dahin steht nur der Ausweichtext auf der Seite.',
|
||||
...(knopf ? ['Modus knopf: --wkt-akzent immer setzen (Abschnitt „Aussehen (CSS)“). Der Knopf kennt die Betriebsfarbe erst nach dem Klick und nimmt sonst #1d4ed8.'] : []),
|
||||
'Ausweichtext geprüft: JavaScript aus oder falscher siteKey: Der Text steht, nichts ist kaputt.',
|
||||
`Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${DATENSCHUTZ_ZWISCHENREGEL}).`,
|
||||
`URLs von Impressum (${impressum}) und Datenschutzerklärung (${datenschutz}) stimmen und beginnen mit https://, sonst fehlen die Links im Fuß des Widgets.`,
|
||||
`„Freischalten (Abnahme)“ im Ticketsystem ausgelöst${wo}. Erst danach antwortet die öffentliche Schnittstelle (nur im Zustand active), und das Widget erscheint.`,
|
||||
// am sichtbaren Widget
|
||||
'Probebuchung bis „Fast geschafft“ durchgespielt: Die Bestätigungs-E-Mail kommt an, der Link bestätigt, die zweite E-Mail (mit Kalenderdatei) kommt an, im Portal steht der Termin. '
|
||||
+ 'Danach den Testtermin stornieren (Verwaltungslink aus der E-Mail oder im Portal).',
|
||||
...(widerruf ? ['Widerrufsbelehrung ist im Widget aufklappbar (Verbraucher-Betrieb, auch bei nur Anfrage-Leistungen).'] : []),
|
||||
...(widerruf && preis ? ['Der Haken für den vorzeitigen Beginn erscheint bei Leistungen mit Preis für Termine in den nächsten 14 Tagen.'] : []),
|
||||
...(widerruf ? ['Der Knopf „Vertrag widerrufen“ steht im Fuß, das Formular funktioniert.'] : []),
|
||||
...(widerruf && knopf
|
||||
? ['Modus knopf: Widerrufsknopf dauerhaft neben dem Buchungsknopf (nicht erst im Dialog); er öffnet den Dialog gleich beim Widerrufsformular.']
|
||||
: []),
|
||||
'Kontrast geprüft: Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1; globale Regeln des Themes (button, input) stören nicht.',
|
||||
'Handy (390 px Breite): kein seitliches Scrollen, Knöpfe gut treffbar, der Dialog passt in den Bildschirm; Zoom 200 %: nichts wird abgeschnitten.',
|
||||
'Nur mit der Tastatur bedienen: Fokus immer sichtbar, Monatsraster mit Pfeilen, Pos1/Ende und Bild ↑/↓, Esc schließt den Dialog.',
|
||||
]
|
||||
return neuerAbschnitt('checkliste', 'Abnahme-Checkliste', punkte.map((p) => `- [ ] ${p}`).join('\n'))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Abschnitt Abrechnung
|
||||
|
||||
const RECHNUNG_RE = /^https:\/\/\S+$/i
|
||||
|
||||
/** Preis der Einrichtung aus dem Register (config.features.termine.priceLabel), sonst „149 €“. */
|
||||
function einrichtungsPreis() {
|
||||
const preis = zeile(config.features?.termine?.priceLabel)
|
||||
return preis || '149 €'
|
||||
}
|
||||
|
||||
function abschnittAbrechnung(d) {
|
||||
if (d.feature?.unlockedByPurchase !== true) {
|
||||
return neuerAbschnitt('abrechnung', 'Abrechnung', 'Abrechnung: ohne Zahlung freigeschaltet (Pilot/Kulanz), keine Abrechnung.')
|
||||
}
|
||||
const rechnung = zeile(d.feature.invoiceUrl)
|
||||
return neuerAbschnitt('abrechnung', 'Abrechnung', [
|
||||
`Abrechnung: Einrichtung bezahlt (${einrichtungsPreis()}, Rechnung ${RECHNUNG_RE.test(rechnung) ? rechnung : 'folgt im Portal'}), Zeit im Arbeitsblatt, nicht auf Abo-Stunden.`,
|
||||
'Leistungsumfang: ein Widget an einer Stelle, Gestaltung nach Fragebogen, Einbau; Mehrwünsche (zweites Widget, Knöpfe je Leistung, Sonderdesign) nach Stunden, vorher im Chat abstimmen.',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Warnungen
|
||||
|
||||
const WARNUNG_PFLICHT = {
|
||||
'betrieb.telefon': 'Telefonnummer des Betriebs fehlt: Gäste können nicht buchen, das Widget zeigt nur den Ausweichtext.',
|
||||
'betrieb.adresse':
|
||||
'Anschrift des Betriebs fehlt (sie steht in der Widerrufsbelehrung): Gäste können nicht buchen, das Widget zeigt nur den Ausweichtext.',
|
||||
}
|
||||
const WARNUNG_OHNE_URL = 'fehlt: Im Fuß des Widgets fehlt der Link.'
|
||||
const WARNUNG_OHNE_DATENSCHUTZ = 'Datenschutz-Absatz nicht erzeugbar: Name oder E-Mail-Adresse des Betriebs fehlt in den Einstellungen.'
|
||||
|
||||
/**
|
||||
* Meldungen aus gesundheitsWarnungen (Block C) tragen Namen des Kunden: gekuerzt in UTF-16-Einheiten (ein halbes Zeichenpaar ist moeglich)
|
||||
* und ungemaskiert. Hier werden sie wohlgeformt und einzeilig; sie bleiben Klartext, der Anzeiger setzt sie als Text ein (nie als HTML).
|
||||
*/
|
||||
const meldungText = (wert) => zeile(typeof wert === 'string' ? wert.toWellFormed() : '')
|
||||
|
||||
/** Warnungen fuer das Team in fester Reihenfolge: Banner, Pflichtangaben, Impressum, Datenschutz-URL, Gesundheit, Kontrast, Datenschutz-Absatz. */
|
||||
function warnungenAus(d) {
|
||||
const warnungen = []
|
||||
if (d.entwurf) warnungen.push(ENTWURF_BANNER)
|
||||
for (const feld of fehlendePflichtangaben(d.e)) warnungen.push(WARNUNG_PFLICHT[feld] ?? `Pflichtangabe fehlt: ${feld}`)
|
||||
if (!zeile(d.betrieb.impressumUrl)) warnungen.push(`Impressum-URL ${WARNUNG_OHNE_URL}`)
|
||||
if (!zeile(d.betrieb.datenschutzUrl)) warnungen.push(`Datenschutz-URL ${WARNUNG_OHNE_URL}`)
|
||||
for (const meldung of gesundheitsWarnungen(d.e)) warnungen.push(meldungText(meldung))
|
||||
warnungen.push(...cssVorschlag(d.farbe).hinweise)
|
||||
if (!d.datenschutz) warnungen.push(WARNUNG_OHNE_DATENSCHUTZ)
|
||||
return warnungen
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Zusammenbau
|
||||
|
||||
const ABSCHNITTE = [abschnittModell, abschnittLeistungen, abschnittRessourcen, abschnittRegeln, abschnittGastfelder, abschnittBenachrichtigungen]
|
||||
const ABSCHNITTE = [
|
||||
abschnittModell, abschnittLeistungen, abschnittRessourcen, abschnittRegeln, abschnittGastfelder, abschnittBenachrichtigungen,
|
||||
abschnittEinbau, abschnittSnippet, abschnittCss, abschnittDatenschutz('sie'), abschnittDatenschutz('du'), abschnittCheckliste, abschnittAbrechnung,
|
||||
]
|
||||
|
||||
/** Alle Abschnitte mit Ueberschrift '## Titel', durch eine Leerzeile getrennt. */
|
||||
const volltext = (abschnitte) => abschnitte.map((a) => `## ${a.titel}\n${a.text}`).join('\n\n')
|
||||
@@ -392,42 +590,68 @@ function projektText(project) {
|
||||
* ergeben gleiche Ausgabe (deepEqual). config wird beim Aufruf gelesen.
|
||||
*
|
||||
* @param {object} p
|
||||
* @param {object} p.feature portalFeatures-Dokument der Funktion (D3b: siteKey, unlockedByPurchase; der Registereintrag
|
||||
* config.features.termine wird nicht gebraucht)
|
||||
* @param {object} [p.feature] portalFeatures-Dokument der Funktion (gelesen: siteKey, customerId, projectId, unlockedByPurchase,
|
||||
* invoiceUrl; der Registereintrag config.features.termine wird nicht gebraucht)
|
||||
* @param {object} [p.project] websiteProjects-Dokument (gelesen: projectName, subdomain, liveDomain, previewUrl)
|
||||
* @param {object|null} p.einstellung gespeicherte Einstellung (termineConfig.einstellung); null = noch keine
|
||||
* @param {object|null} [p.fragebogen] geparster Fragebogen oder null (D3b: Kundenwuensche zum Einbau)
|
||||
* @param {object|null} [p.fragebogen] geparster Fragebogen oder null (gelesen: einbau.modus, knopfText, stelle, gestaltung, bemerkung)
|
||||
* @param {number} p.version Version der Einstellungen (0 = noch nie gespeichert)
|
||||
* @param {Date} p.now Zeitpunkt der Erzeugung; ein ungueltiges now wirft einen TypeError
|
||||
* @param {string|null} [p.updatedAt] ISO-Zeitpunkt der letzten Aenderung der Einstellungen
|
||||
* @param {{name?: string}|null} [p.kunde] gelesen wird nur der Name
|
||||
* @param {boolean} [p.entwurf] D3b: Rechtstexte sind noch ENTWURF (Warnung); Standard RECHTSTEXTE_ENTWURF
|
||||
* @param {boolean} [p.entwurf] Rechtstexte sind noch ENTWURF (erste Warnung ENTWURF_BANNER); Standard RECHTSTEXTE_ENTWURF
|
||||
* @returns {{stand: {version: number, updatedAt: string|null}, warnungen: string[], abschnitte: Array<{id: string, titel: string, text: string, kopierbar: boolean}>, text: string, kurz: string}}
|
||||
* D3a: warnungen [] und kurz '' (kommen mit D3b und D4); ohne Einstellung keine Abschnitte. Die Anrede der Gaeste kommt aus einstellung.anrede.
|
||||
* abschnitte: 13 Stueck in fester Reihenfolge (modell bis abrechnung), kopierbar nur snippet, css und die beiden Datenschutz-Absaetze.
|
||||
* text: alle Abschnitte mit Ueberschrift, aber nur die Datenschutz-Fassung, die zu einstellung.anrede passt ('du', sonst Sie).
|
||||
* Ohne Einstellung (null): nur der Abschnitt snippet und die Warnung "Keine Einstellungen gespeichert". `kurz` ist '' (kommt mit D4).
|
||||
*/
|
||||
export function einbauanleitung({ feature, project, einstellung, fragebogen, version, now, updatedAt = null, kunde = null, entwurf = RECHTSTEXTE_ENTWURF }) {
|
||||
if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw new TypeError('einbauanleitung: now fehlt')
|
||||
const stand = { version, updatedAt }
|
||||
if (!istObjekt(einstellung)) return { stand, warnungen: [], abschnitte: [], text: '', kurz: '' }
|
||||
const banner = entwurf ? [ENTWURF_BANNER] : []
|
||||
const gemeinsam = {
|
||||
feature,
|
||||
siteKey: zeile(feature?.siteKey),
|
||||
portal: zeile(config.publicBaseUrl),
|
||||
wunsch: kundenwunsch(fragebogen),
|
||||
anleitungsLink: feature && zeile(feature.customerId) && zeile(feature.projectId) ? anleitungsLink(zeile(feature.customerId), zeile(feature.projectId)) : '',
|
||||
}
|
||||
|
||||
if (!istObjekt(einstellung)) {
|
||||
const abschnitte = [abschnittSnippet({ ...gemeinsam, telefon: '' })]
|
||||
return { stand, warnungen: [...banner, 'Keine Einstellungen gespeichert'], abschnitte, text: volltext(abschnitte), kurz: '' }
|
||||
}
|
||||
|
||||
const ressourcen = liste(einstellung.ressourcen).filter(istObjekt)
|
||||
const geaendert = updatedAt ? new Date(updatedAt) : null
|
||||
const betrieb = istObjekt(einstellung.betrieb) ? einstellung.betrieb : {}
|
||||
const daten = {
|
||||
...gemeinsam,
|
||||
e: einstellung,
|
||||
now,
|
||||
entwurf: Boolean(entwurf),
|
||||
heuteTag: heute(now),
|
||||
version,
|
||||
geaendert: geaendert && !Number.isNaN(geaendert.getTime()) ? datumKurz(geaendert) : '',
|
||||
betrieb: istObjekt(einstellung.betrieb) ? einstellung.betrieb : {},
|
||||
betrieb,
|
||||
telefon: zeile(betrieb.telefon),
|
||||
kunde: zeile(kunde?.name),
|
||||
projekt: projektText(project),
|
||||
farbe: typeof einstellung.farbe === 'string' && FARBE_RE.test(einstellung.farbe) ? einstellung.farbe.toLowerCase() : VORGABE_FARBE,
|
||||
portal: zeile(config.publicBaseUrl),
|
||||
haltezeit: config.termine?.holdMinutes,
|
||||
ressourcen,
|
||||
leistungen: liste(einstellung.leistungen).filter(istObjekt),
|
||||
nachId: new Map(ressourcen.map((r) => [r.id, { typ: r.typ, name: zeile(r.name) || 'ohne Namen' }])),
|
||||
datenschutz: datenschutzTexte(einstellung),
|
||||
}
|
||||
const abschnitte = ABSCHNITTE.map((baue) => baue(daten))
|
||||
return { stand, warnungen: [], abschnitte, text: volltext(abschnitte), kurz: '' }
|
||||
// der Volltext fuehrt nur die Datenschutz-Fassung, die zur Anrede der Gaeste passt
|
||||
const andere = einstellung.anrede === 'du' ? 'datenschutz_sie' : 'datenschutz_du'
|
||||
return {
|
||||
stand,
|
||||
warnungen: warnungenAus(daten),
|
||||
abschnitte,
|
||||
text: volltext(abschnitte.filter((a) => a.id !== andere)),
|
||||
kurz: '',
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user