Files
Webklar-Kundenbereich/server/services/termine/fragebogen.js
T
ClaudeandClaude Sonnet 5.5 ab47fabcb2 Terminplaner TP2: Fragebogen-Attribut, Branchen, Schritte, Normalisierung
termineConfig bekommt das Attribut fragebogen (Text 20000, Inhalt bis
FRAGEBOGEN_MAX 16000). fragebogen.js: SCHRITTE, BRANCHEN (Vorlagen plus
Gesundheitsbereich), leerFragebogen und normalisiereFragebogen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 18:49:06 +00:00

121 lines
5.4 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.
// STUB (Block B) – ersetzt Task C2 (bestellpruefung); Task C1 legt in dieser Datei den Rest des Fragebogen-Dienstes an
import { text } from './einstellungen.js'
import { vorlagenListe } from './vorlagen.js'
/**
* Bestell-Fragebogen des Terminplaners (TP2, Spezifikation 2026-10-03-terminplaner-tp2-verkauf-design.md): Der Kunde
* richtet den Terminplaner vor der Bestellung ein; der Entwurf steht in termineConfig.einstellung, der Fragebogen
* (Branche, bestaetigte Schritte, Wuensche zum Einbau) im Attribut termineConfig.fragebogen als JSON-Text.
* Dieser Teil: Schritte, Branchen, Grundform und Normalisierung. Reine Funktionen ohne Appwrite und ohne Uhr.
* Fehler sind { [pfad]: text } wie bei normalisiereEinstellung (Pfad '_' = das Ganze); Texte in Du-Form.
*/
/** Die acht Schritte des Fragebogens in fester Reihenfolge; `bestaetigt` nennt die vom Kunden abgehakten. */
export const SCHRITTE = Object.freeze(['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'])
/**
* Branchen zur Auswahl: alle Vorlagen in der Reihenfolge von vorlagenListe(), danach der Gesundheitsbereich
* (ohne Vorlage; er fuehrt in den Hinweis- und Pruefweg fuer Gesundheitsangaben). Schluessel und Name der letzten
* Branche gelten woertlich fuer Bestellpruefung, Oberflaeche, Einbauanleitung und Ticket.
*/
export const BRANCHEN = Object.freeze([
...vorlagenListe().map(({ key, name }) => Object.freeze({ key, name, vorlage: key, gesundheit: false })),
Object.freeze({ key: 'gesundheit', name: 'Gesundheitsbereich', vorlage: null, gesundheit: true }),
])
/** Obergrenze fuer den JSON-Text des Fragebogens (Zeichen); die Spalte in Appwrite ist mit 20000 groesser. */
export const FRAGEBOGEN_MAX = 16000
const EINBAU_MODI = ['inline', 'knopf']
const TEXTGRENZEN = { knopfText: 40, stelle: 300, gestaltung: 1000, bemerkung: 1000 }
const MEHRZEILIG = new Set(['stelle', 'gestaltung', 'bemerkung'])
const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert)
/** Ein leerer Fragebogen; bei jedem Aufruf ein neues Objekt. */
export function leerFragebogen() {
return {
schema: 1,
branche: null,
bestaetigt: [],
bundeslandGewaehlt: false,
preiseGeprueft: false,
einbau: { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: '' },
}
}
function wahrheit(fehl, pfad, wert) {
if (wert === undefined || wert === null) return false
if (typeof wert !== 'boolean') {
fehl(pfad, 'Bitte „ja“ oder „nein“ wählen.')
return false
}
return wert
}
function einbauFelder(fehl, eingabe) {
const quelle = eingabe === undefined || eingabe === null ? {} : eingabe
if (!istObjekt(quelle)) {
fehl('einbau', 'Die Angaben zum Einbau sind ungültig.')
return leerFragebogen().einbau
}
const einbau = { modus: null }
if (quelle.modus !== undefined && quelle.modus !== null) {
if (EINBAU_MODI.includes(quelle.modus)) einbau.modus = quelle.modus
else fehl('einbau.modus', 'Bitte wähle, ob der Terminplaner im Text oder als Knopf erscheint.')
}
for (const [feld, max] of Object.entries(TEXTGRENZEN)) {
const wert = quelle[feld]
if (wert === undefined || wert === null) einbau[feld] = ''
else if (typeof wert !== 'string') {
fehl(`einbau.${feld}`, 'Der Text ist ungültig.')
einbau[feld] = ''
} else einbau[feld] = text(wert, max, { mehrzeilig: MEHRZEILIG.has(feld) })
}
return einbau
}
/**
* Prueft und bereinigt einen Fragebogen aus Kundeneingabe oder gespeichertem JSON.
* Baut ein neues Objekt Feld fuer Feld (unbekannte Schluessel und __proto__ fallen weg), kuerzt Texte, entfernt
* Steuerzeichen und ordnet `bestaetigt` nach SCHRITTE ohne Doppelte und Fremde. Fehlende Felder sind leer/false.
* Ergebnis: { fragebogen } oder { fehler: { [pfad]: text } }; idempotent.
*/
export function normalisiereFragebogen(eingabe) {
if (!istObjekt(eingabe)) return { fehler: { _: 'Der Fragebogen ist ungültig.' } }
const fehler = {}
const fehl = (pfad, meldung) => { if (!Object.hasOwn(fehler, pfad)) fehler[pfad] = meldung }
let branche = null
if (eingabe.branche !== undefined && eingabe.branche !== null) {
if (BRANCHEN.some((b) => b.key === eingabe.branche)) branche = eingabe.branche
else fehl('branche', 'Bitte wähle eine Branche aus der Liste.')
}
let bestaetigt = []
if (eingabe.bestaetigt !== undefined && eingabe.bestaetigt !== null) {
if (Array.isArray(eingabe.bestaetigt)) bestaetigt = SCHRITTE.filter((schritt) => eingabe.bestaetigt.includes(schritt))
else fehl('bestaetigt', 'Die bestätigten Schritte sind ungültig.')
}
const fragebogen = {
schema: 1,
branche,
bestaetigt,
bundeslandGewaehlt: wahrheit(fehl, 'bundeslandGewaehlt', eingabe.bundeslandGewaehlt),
preiseGeprueft: wahrheit(fehl, 'preiseGeprueft', eingabe.preiseGeprueft),
einbau: einbauFelder(fehl, eingabe.einbau),
}
return Object.keys(fehler).length ? { fehler } : { fragebogen }
}
/**
* Bestellpruefung des Terminplaners: Liste der offenen Punkte `[{ schritt, text }]`, leer = bestellbar.
* Die Signatur ist endgueltig (Block C ersetzt nur den Rumpf): `mitAbo !== false` schaltet den Abo-Punkt (h)
* dazu, den nur die Vorschau (GET /api/termine/einstellungen) mit `abo` anfordert; der Checkout ruft mit
* `mitAbo: false` und ohne `abo`. Der Platzhalter meldet nichts offen.
*/
export function bestellpruefung({ einstellung, fragebogen, feature, abo, mitAbo = true } = {}) {
return []
}