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>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 18:49:06 +00:00
1 parent 36b36bcc4d
commit ab47fabcb2
4 files changed
+416 -1

No files matched your search

+109
View File
@@ -1,4 +1,113 @@
// 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.
+3
View File
@@ -26,6 +26,9 @@ export const TERMINE_COLLECTIONS = {
int('version'),
dt('updatedAt'),
str('updatedBy', 100),
// TP2: Bestell-Fragebogen als JSON-Text. Der Inhalt ist auf 16000 Zeichen begrenzt (FRAGEBOGEN_MAX in
// termine/fragebogen.js); die groessere Spalte wird in Appwrite eine Textspalte und zaehlt nicht zur Zeilenbreite.
str('fragebogen', 20000),
],
indexes: [],
},