- AVV Vorbemerkung (2): K16 der Textform-Aktivierung trägt nur Leistungsbeschreibung und AVV als PDF - AVV Nr. 9 Abs. 6, Anlage (Löschung), LB Nr. 8 Abs. 3 und Datenschutz 6c: das Einrichtungs-Ticket bleibt mit einer Kurzfassung der Einstellungen als Arbeitsnachweis (keine Daten von Gästen) - AVV Nr. 8 Abs. 2: "Gastdaten löschen" löscht nur wartende E-Mails; fehlgeschlagene behalten Adresse und Inhalt bis Nr. 9 Abs. 4 - Datenschutz § 7: Frist für Bestellungen ohne Abo (drei Jahre nach Ablauf des Jahres der Bestellung) - News-Leistungsbeschreibung vom 30.09.2026 unverändert archiviert (SHA-256-Pin), die geltende Datei verweist darauf - Kanzlei-Paket: Zitate, Q4/Q9/Q23, Livegang-Checkliste (Abschnitt 7.6/7.7), K23-Hinweis - build-legal-html: Rückwärtsschrägstrich in Navigationsadressen abgelehnt; TP1-Spezifikation E2 (ohne USt) - HTML, PDFs und Prüfsummen neu erzeugt (Terminplaner-Unterlagen und Datenschutzerklärung) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
47 KiB
Terminplaner – Design
Stand: 2026-10-02 · Status: im Chat freigegeben (Entscheidungen E1–E8), Spec zur Durchsicht Teilprojekte: TP1 Kern (diese Spec im Detail, erster Plan) · TP2 Verkauf · TP3 Online-Zahlung (TP2 und TP3 sind hier verbindlich umrissen und bekommen je einen eigenen Plan; Regel des Nutzers: ein Teilprojekt live, vom Nutzer getestet, erst dann das nächste).
Recherche (nur lesend, Quellen dort): /root/reports/terminplaner-2026-10-02/
code-funktionen.md, code-kern.md, code-woms.md, recherche-branchen.md, recherche-recht.md,
recherche-technik.md (+ spike-code/), recherche-stripe-connect.md, recherche-recht-zahlung.md.
Ziel
Betriebe (unsere Kunden) bekommen eine Online-Terminbuchung auf ihrer eigenen Website. Der Teil auf der Website sieht bei jedem Kunden anders aus (Design der Kundenseite), die Verwaltung im Kundenportal ist für alle gleich. Ein Fragebogen (TP2) klassifiziert den Betrieb und erzeugt für WEBklar eine fertige Einbauanleitung: wir bauen ein, schließen das Ticket, der Terminplaner läuft.
Die Funktion ersetzt die Kachel „Terminplaner · Kommt bald“ (public/funktionen.js:151).
Entscheidungen (vom Nutzer)
| # | Entscheidung |
|---|---|
| E1 | Abrechnung: einmalige Einrichtung + nur solange ein Wartungs-Abo läuft (wie Website-Analyse). Endet das Abo, endet der Terminplaner. Hosting-Abo reicht nicht. |
| E2 | Einrichtungspreis 149 € (ursprünglich „brutto“, seit TP2 ohne USt nach § 19 UStG; einmalig, Stripe Checkout wie News). |
| E3 | Umfang V1: Personen und Plätze (Einzelperson; Team mit Wahl der Person oder „egal“; buchbare Plätze wie Kabine, Hebebühne). Pro Leistung: sofort verbindlich oder Anfrage (Betrieb bestätigt). Gruppen/Kurse/Tische später. |
| E4 | Der Gast bestätigt seine E-Mail-Adresse per Link, bevor der Termin gilt; der Slot ist so lange 30 Minuten reserviert. |
| E5 | Ein gemeinsames Widget, je Kunde gestaltet (Farben, Schrift, Rundungen, Texte, Position) + offene, versionierte Schnittstelle für Sonderfälle. |
| E6 | Der Fragebogen kommt vor der Zahlung; das Ticket enthält sofort die Einbauanleitung (TP2). |
| E7 | Zahlungsmodus legt der Betrieb fest: nur vor Ort · nur online · online oder vor Ort (dann wählt der Gast). Online-Geld geht direkt an den Betrieb (Stripe Connect, WEBklar berührt kein Geld). TP1 kann nur „vor Ort“, TP3 bringt „online“. |
| E8 | Reihenfolge: TP1 Kern (Pilot mit Kunde „test“) → TP2 Verkauf → TP3 Online-Zahlung. |
Vorgaben aus dem Entwurf, denen der Nutzer nicht widersprochen hat:
- Gebaut im Kundenportal (kein eigener Container, keine Fremdsoftware), Daten in Appwrite
woms-database. - Ein Buchungssystem für alle Branchen; Branchen-Vorlagen sind Daten, kein Code je Nische.
- Gesundheitsbereich ausgeschlossen (Arzt, Zahnarzt, Physio/Ergo/Logo/Podologie, Psychotherapie, Heilpraktiker, medizinische Kosmetik/Massage). Freitext beim Gast standardmäßig aus.
- Erinnerung an den Gast nur per nicht vorangekreuztem Haken.
- Gästemails: Anzeigename = Betrieb, Antworten an den Betrieb, keine Team-Kopie (
bccTeamaus), kein Tracking, keine Werbung. - Mailversand über eine Warteschlange (Hetzner-Grenze 500 Mails/h für alles).
- Löschfristen: unbestätigt 48 h, abgesagt 30 Tage, vergangen 90 Tage (einstellbar 14–365 Tage).
- Schalter
TERMINE_ENABLED=off|pilot|on.
Begriffe
- Betrieb = unser Kunde (Portal-Kunde, Inhaber der Website). Gast = wer auf der Website bucht.
- Ressource = was gebucht wird: eine Person oder ein Platz (je Kapazität 1; zwei Hebebühnen = zwei Plätze). Leistung = was gebucht wird (Dauer, Puffer, Preis, Buchungsart).
- Funktion
termine= Dokument inportalFeatures(featureKey: 'termine'), mit eigenemsiteKey.
TP1 Kern
1. Lebenszyklus in TP1
TERMINE_ENABLED=off(Standard): Kachel „Kommt bald“, öffentliche Schnittstelle antwortet für jeden Schlüssel „nicht aktiv“, Routen des Kunden 409.pilot: Kachel und Ansicht nur für Kunden, deren E-Mail inTERMINE_PILOT_EMAILSsteht (Komma-Liste, MusterABO_PILOT_EMAILS). Bestellen gibt es in TP1 nicht; die Funktion wird im Ticketsystem mit „Aktivieren (ohne Zahlung)“ eingeschaltet (vorhandenesadminActivate, auf Schlüssel verallgemeinert).onkommt mit TP2 (Bestellung für alle mit Wartungs-Abo).- Zustände der Funktion wie bei News:
off → active → disabled(TP1);ordered/setupfolgen mit TP2.disabled⇒ Widget zeigt den Ausweichtext, keine neuen Buchungen; bestehende Termine bleiben im Portal sichtbar, Gäste können weiter absagen und widerrufen.
2. Datenmodell (Appwrite, neues Setup-Skript scripts/termine-setup.mjs)
Muster von scripts/analyse-setup.mjs (exportierte Attributlisten, injizierbarer Client, --check,
legt nur Fehlendes an: fehlende Attribute und Indizes auch in schon vorhandenen Collections, R6). Alle Collections permissions: [],
documentSecurity: false (nur Server-Schlüssel). test/support/schema.js liest das neue Skript mit.
termineConfig – eine Einstellung je Funktion; Dokument-ID = ID des portalFeatures-Dokuments.
customerId, projectId (64), einstellung (Text, JSON bis 200 000 Zeichen; nicht data – das Wort
behandelt appwriteAdmin.formatRequestBody als fertig verpackten Body), version (Ganzzahl),
updatedAt, updatedBy (100). Inhalt von einstellung (validiert in services/termine/einstellungen.js):
{
"schema": 1,
"vorlage": "salon", // Branchen-Vorlage, nur Herkunft
"betrieb": { "name": "Salon Müller", "email": "info@salon.de", "telefon": "06301 1234", "kleinunternehmer": false,
"adresse": "Hauptstr. 1, 67722 Winnweiler",
"impressumUrl": "https://…", "datenschutzUrl": "https://…", "agbUrl": null },
"anrede": "sie", // sie | du (Widget, Gästeseiten, Gästemails)
"farbe": "#7c3aed", // Akzent für Gästeseiten und -mails
"bundesland": "RP", "feiertageGeschlossen": true,
"sonderoeffnungen": { "2026-12-24": [["09:00","13:00"]] }, // Datum → Fenster, schlägt Plan/Feiertag
"abwesenheiten": [ { "id": "a1", "ressourceId": null, "von": "2026-12-27", "bis": "2026-12-31", "titel": "Betriebsurlaub" } ],
"ressourcen": [ { "id": "r1", "typ": "person", "name": "Anna", "aktiv": true,
"plan": { "1": [["09:00","12:30"],["13:30","18:00"]], "6": [["09:00","13:00"]] },
"feedToken": "…" } ], // ISO-Wochentag 1=Mo … 7=So; Pausen = Lücken
"leistungen": [ { "id": "s1", "name": "Damenschnitt", "beschreibung": "", "dauer": 45, "pufferNach": 5,
"preisArt": "fest", "preisCent": 4500, // fest | ab | keiner
"buchungsart": "sofort", // sofort | anfrage
"ressourcen": ["r1"], "aktiv": true, "reihenfolge": 1 } ], // reihenfolge: der Server setzt i+1 in Listenreihenfolge
"regeln": { "raster": 15, "vorlaufMin": 120, "horizontTage": 60, "absageBisStd": 24,
"maxOffeneJeEmail": 3, "personWaehlbar": true },
"gastFelder": { "telefon": "optional", "nachricht": "aus", "zusatz": [] }, // aus | optional | pflicht
"erinnerung": { "anbieten": true, "stundenVorher": 24 },
"storno": { "text": "Kostenlose Absage bis 24 Stunden vor dem Termin …", "version": "2026-10-02-1" },
"recht": { "gaesteSind": "verbraucher", // verbraucher | unternehmer | gemischt
"freizeit": false }, // true ⇒ kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9), nur nach Prüfung
"zahlung": { "modus": "vorort", "vorOrtArten": ["bar","ec","kreditkarte"] }, // online/beides erst TP3
"aufbewahrungTage": 90
}
IDs von Ressourcen/Leistungen/Abwesenheiten sind kurze Zufalls-IDs (keine Appwrite-IDs). Änderungen
sind optimistisch gesperrt: PATCH schickt version, der Server schreibt unter
withLocks(['termine-cfg:<featureId>']) und antwortet bei abweichender Version 409 „Die Einstellungen
wurden inzwischen geändert. Bitte neu laden.“
termineBookings – eine Buchung je Dokument (ID randomUUID).
Felder: featureId, customerId, projectId (64); leistungId, leistungName (120, Schnappschuss),
ressourceId, ressourceName (80); startsAt, endsAt, blockEndsAt (datetime UTC; blockEndsAt =
Ende + Puffer); status (16, siehe 4); quelle (16: online | manuell | sperre);
gastName (100), gastEmail (254), gastTelefon (40), gastNachricht (500), gastZusatz (2000, JSON);
erinnerung (bool), erinnerungGesendetAt; heldUntil; verifyHash, manageHash (64, SHA-256 der
Tokens); bestaetigtAt, angenommenAt, abgesagtAt, abgesagtVon (16: gast|betrieb|system),
absageGrund (300), widerrufenAt; nachweis (4000, JSON: Fassung und Wortlaut-Hash von Storno-,
Datenschutz- und Widerrufstext, Zeitpunkt der Zustimmungen, Knopftext, Anrede); preisCent,
preisArt (8), buchungsart (8), verbraucher (bool); zahlart (8: vorort | online),
zahlStatus (16: keine|offen|bezahlt|erstattet|teilerstattet|fehlgeschlagen; TP1 immer
keine), stripeSessionId (128), stripePaymentIntent (64) (TP3); icsSequenz (Ganzzahl);
createdAt, updatedAt.
Indizes: idx_feature_start(featureId, startsAt), idx_status_held(status, heldUntil),
idx_feature_email(featureId, gastEmail), idx_verify(verifyHash), idx_manage(manageHash),
idx_status_start(status, startsAt).
termineClaims – Belegungszellen gegen Doppelbuchung (siehe 5). Dokument-ID deterministisch
c_ + erste 34 Hex von sha1(featureId|ressourceId|zellenStartUtcIso). Felder featureId,
ressourceId, bookingId, zelle (datetime), bis (datetime, = blockEndsAt der Buchung, nie die Haltezeit).
Index idx_booking(bookingId), idx_bis(bis).
termineOutbox – Mail-Warteschlange. Felder featureId, bookingId, art (16), an (254),
fromName (120), replyTo (254), subject (300), html (Text 100 000), text (Text 50 000),
ics (Text 20 000), icsMethod (8), status (wartet|gesendet|fehler), versuche,
naechsterVersuch, gesendetAt, fehler (300), createdAt. Index idx_status_next(status, naechsterVersuch).
Nach dem Versand werden an, html, text, ics geleert (personenbezogene Daten); gesendete Einträge werden 7 Tage nach dem Versand
gelöscht, endgültig gescheiterte (fehler) 48 Stunden nach dem Fehler.
portalFeatures bekommt keine neuen Attribute; featureKey 'termine' nutzt die vorhandenen.
3. Verfügbarkeit (reine Funktionen, server/services/termine/)
- Zeit (
zeit.js, ohne Abhängigkeit,Intl): Wanduhr Europe/Berlin ↔ UTC; Wochentag; Tage addieren. Nicht existierende Zeiten (Umstellung März, 02:00–02:59) werden ausgelassen, doppelte Zeiten (Oktober) nehmen das erste Vorkommen. Tests für 2026-03-29 und 2026-10-25.deadlines.berlinTimewird dafür nicht benutzt (kennt die Lücke nicht). - Feiertage (
feiertage.js): Paketfeiertagejs(MIT, ohne Abhängigkeiten) je Bundesland;sonderoeffnungenschlagen Feiertag und Plan,abwesenheitenschlagen alles, auch eine Sonderöffnung (R11). - Slots (
slots.js,nowwird übergeben): je Ressource und Berliner Kalendertag die Öffnungsfenster ausplan(odersonderoeffnungen), minus Feiertag (wenn geschlossen), minus Abwesenheiten, minus belegte Zeiten (Buchungen mit Statusgehalten|angefragt|bestaetigtund Sperren, jeweils bisblockEndsAt). Startzeiten imrasterab Fensterbeginn; Termin +pufferNachmuss ganz ins Fenster passen; nicht vornow + vorlaufMin; nicht nachhorizontTage. „Egal“ = Vereinigung über alle Ressourcen der Leistung; beim Buchen wird die Ressource mit den wenigsten Buchungen an dem Tag gewählt (bei Gleichstand Reihenfolge der Liste). - Ein Abruf liefert höchstens 31 Tage; der Server lädt dafür die Buchungen des Zeitraums per Indexabfrage
(
featureId+startsAtzwischen, mit Seitenweise-Laden übercursorAfter), nie „alles laden“.
4. Buchungsablauf und Zustände
Zustände (status): gehalten (Formular abgeschickt, wartet auf E-Mail-Bestätigung) → angefragt
(Leistung „Anfrage“, wartet auf den Betrieb) oder bestaetigt → erledigt (automatisch 1 h nach Ende) ·
nichterschienen (Betrieb) · abgesagt (Gast, Betrieb, System) · abgelehnt (Betrieb lehnt Anfrage ab) ·
verfallen (Link nicht innerhalb von 30 Minuten bestätigt). Sperren (quelle: sperre) sind immer
bestaetigt und haben keinen Gast.
Gast im Widget
- Leistung wählen (Name, Dauer, Preis „45 €“ / „ab 45 €“ / ohne, Kurzbeschreibung). Zahlungsart steht schon hier („Bezahlung vor Ort: bar, EC-Karte, Kreditkarte“, § 312j Abs. 1 BGB).
- Person wählen, wenn mehr als eine Ressource infrage kommt und
personWaehlbar(mit „Egal“ als erster Option); bei Plätzen kein Schritt. - Tag (Monatsraster, nur Tage mit freien Zeiten aktiv) und Uhrzeit.
- Daten: Name, E-Mail, Telefon/Nachricht/Zusatzfelder nach
gastFelder. Über der Nachricht steht „Bitte keine Gesundheitsangaben.“ - Zusammenfassung: Leistung, Person/Platz, Datum, Uhrzeit, Preis inkl. USt (bzw. Hinweis Kleinunternehmer
aus dem Einstellungsfeld des Betriebs), Ort, Zahlungsart, Stornoregel im Klartext + Link, Hinweis zum
Datenschutz (Text 11.2 aus
recherche-recht.md, Link aufdatenschutzUrl), bei Verbraucher-Gästen die vollständige Widerrufsbelehrung samt Muster-Widerrufsformular (aufklappbar, bei jeder sichtbaren Leistung, auch bei Anfragen; R14, R55), Haken:- Pflicht: „Ich habe die Stornobedingungen gelesen.“ (nur wenn
storno.textgesetzt) - Pflicht bei verbindlicher Leistung mit Preis für Verbraucher, wenn der Termin vor dem Ende der Widerrufsfrist liegt (Frist nach §§ 187, 188, 193 BGB ab Vertragsschluss, Berliner Zeit), und bei bepreisten Anfragen für Verbraucher immer: Zustimmung zum vorzeitigen Beginn (§ 356 Abs. 5 Nr. 2 BGB, Wortlaut im Rechtsteil).
- Freiwillig, nicht vorangekreuzt (nur wenn
erinnerung.anbieten): „Ich möchte {24} Stunden vorher eine Erinnerung per E-Mail.“ Knopf: „Zahlungspflichtig buchen“ (verbindlich + Preis), „Termin buchen“ (verbindlich, ohne Preis), „Termin anfragen“ (Buchungsart Anfrage).
- Pflicht: „Ich habe die Stornobedingungen gelesen.“ (nur wenn
- Ergebnis: „Fast geschafft: Bitte bestätigen Sie den Termin über den Link, den wir an m***@gmail.com geschickt haben. Der Termin ist 30 Minuten für Sie reserviert.“
Server beim Abschicken (POST …/buchungen): Prüfungen (Abschnitt 8), Slot neu berechnen, unter
withLocks(['termine-res:<featureId>:<ressourceId>'], {waitMs: 5000}) die Buchung gehalten mit
heldUntil = now + 30 min anlegen und danach die Zellen belegen (5; die Gültigkeit einer Zelle hängt allein an ihrer
Buchung), Mail G01 in die Warteschlange (sofort senden).
Bestätigungslink https://project.webklar.com/termin.html#v=<token>: Die Seite liest das Token aus
dem Fragment (geht nicht an Server-Logs), zeigt per POST …/vorschau die Zusammenfassung und einen Knopf
mit demselben Wortlaut wie im Widget (bei Preis „Zahlungspflichtig bestätigen“). Erst POST …/bestaetigen
ändert den Zustand (Mailscanner öffnen nur Links): gehalten → bestaetigt (G02 an Gast mit .ics,
Verwaltungslink und ggf. Widerrufsbelehrung; K20 an den Betrieb mit .ics) bzw. → angefragt (G03 an Gast,
K20-Block „Anfrage“ an den Betrieb). Abgelaufen ⇒ „Diese Reservierung ist abgelaufen“ + Link zur Website.
Ein Token gilt genau einmal.
Verwaltungslink termin.html#m=<token> (gültig bis Terminende + 1 Tag; Token = HMAC über die
Buchungs-ID, damit spätere Mails wie die Erinnerung denselben Link enthalten; gespeichert nur sein Hash):
zeigt den Termin und je nach
Lage
- „Termin absagen“ bis
absageBisStdvor Beginn (danach Text „Absage nur telefonisch: …“) ⇒abgesagt, Zellen frei, G06 an Gast (ICS CANCEL), K21 an den Betrieb; - „Termin verschieben“ bis zur selben Frist ⇒ neue Zeit wählen; neuer Slot wird belegt, alter freigegeben
(eine Sperre über beide Ressourcen), Status bleibt,
icsSequenz + 1, G07 + K21; das gilt auch fürangefragt(R26: G03 enthält nicht nur „Absage“, die Seite bietet auch „Termin verschieben“); - „Vertrag widerrufen“, solange ein Widerrufsrecht besteht (verbindlich, Verbraucher, nicht
freizeit, Statusbestaetigt, Termin noch nicht begonnen und die Widerrufsfrist läuft: 14 Tage ab dem Tag nach dem Vertragsschluss, bei Wochenende oder Feiertag am letzten Tag bis zum nächsten Werktag, bis 23:59:59 Berliner Zeit, §§ 187, 188, 193 BGB; Vertragsschluss istbestaetigtAtbei einer sofort buchbaren Leistung undangenommenAtbei einer vom Betrieb angenommenen Anfrage; R29, R55) – unabhängig von der Stornofrist (§ 361 BGB) ⇒ Formular nach § 356a BGB (Name, Buchungsnummer, E-Mail für die Eingangsbestätigung vorbefüllt, Knopf „Widerruf bestätigen“) ⇒widerrufenAt,abgesagt(vongast), G08 Eingangsbestätigung mit Datum und Uhrzeit, K21-Block „Widerruf“. Nach Ablauf der Frist oder nach Beginn des Termins bleibt das Formular der Weg für eine Erklärung: sie wird nur vermerkt und dem Betrieb gemeldet (G08/K21nach_fristbzw.begonnen), der Betrieb prüft (R27). Die Verwaltungsseite bietet den Knopf nur bis zum Beginn des Termins an; G02 sagt das und nennt danach das Formular auf der Website des Betriebs oder die E-Mail an den Betrieb.
Widerruf ohne Link (§ 356a: auf der Oberfläche, über die der Vertrag geschlossen wurde): Das Widget
zeigt im Fuß dauerhaft „Vertrag widerrufen“ als zweiten Knopf (mindestens 44 px, kein Textlink), sobald
Verbraucher-Gäste bei dem Betrieb buchen oder anfragen können, also bei jeder sichtbaren Leistung, auch bei
Anfragen (R54, R55; Anschrift des Betriebs ist dann Pflicht). Im Modus knopf steht der Fuß nur im geöffneten
Dialog: Dort lädt das Widget schon beim Laden der Seite die Konfiguration (ein einziger Abruf) und zeigt bei
Widerrufsrecht dauerhaft einen zweiten Knopf „Vertrag widerrufen“ neben dem Buchungsknopf, der den Dialog gleich
beim Widerrufsformular öffnet (R54). Formular: Name, E-Mail, Buchungsnummer (steht in G02), Knopf „Widerruf bestätigen“. Der Server
antwortet immer gleich („Ihre Erklärung ist eingegangen“), speichert die Erklärung an der passenden
Buchung (E-Mail + Nummer), schickt G08 an die angegebene Adresse und leitet die Erklärung per K21 an den
Betrieb weiter – auch wenn keine Buchung passt (dann Block „nicht zugeordnet“, der Betrieb prüft).
Betrieb im Portal: Anfrage annehmen (angefragt → bestaetigt, G02) oder ablehnen (abgelehnt, G04,
Zellen frei); bestätigten Termin absagen (Pflicht-Grund-Auswahl + optionale Nachricht, G06) oder verschieben
(G07); „nicht erschienen“; Gastdaten auf Wunsch löschen (anonymisieren: Gastfelder und absageGrund werden
geleert, noch nicht versandte Mails der Buchung aus der Warteschlange gelöscht); Termin von Hand eintragen (quelle: manuell, Gast-E-Mail optional, G02 nur wenn
der Haken „Bestätigung an den Gast senden“ gesetzt ist; ohne Widerrufsbelehrung durch das System, die ist
bei telefonischen Buchungen Sache des Betriebs); Zeit sperren (quelle: sperre, höchstens 500 Minuten am Stück; längeres über Abwesenheiten).
Hintergrund-Jobs (server/index.js, Muster der vorhandenen setInterval-Jobs, je Job eigene Fehlerbehandlung):
- jede Minute: abgelaufene
gehalten⇒verfallen+ Zellen löschen; Warteschlange senden (6). - alle 5 Minuten: Erinnerungen (
bestaetigt,erinnerung, keinerinnerungGesendetAt,now + 1 h < startsAt ≤ now + stundenVorherund Vertragsschluss vorstartsAt − stundenVorher, damit ein Ausfall des Jobs keine Erinnerung verschluckt; erst Marker setzen, dann einreihen);bestaetigtmitendsAt < now − 1 h⇒erledigt;angefragtmitstartsAt < now⇒abgelehnt(vom System, Zellen frei). - täglich: Löschfristen (11), verwaiste Zellen (Buchung fehlt oder nicht mehr belegend).
5. Doppelbuchung verhindern
- Zellen à 5 Minuten von Beginn bis
blockEndsAt. Jede Zelle ist eintermineClaims-Dokument mit deterministischer ID; eine zweite Belegung derselben Zelle scheitert in der Datenbank mit 409. - Alle Zellen einer Buchung werden in einem Aufruf angelegt (Appwrite 1.8.1 Bulk-Create
POST …/documentsmitdocuments: [...], eine Transaktion, höchstens 100 Dokumente ⇒ Termine bis 500 Minuten). 409 ⇒ „Dieser Termin wurde gerade vergeben. Bitte wählen Sie eine andere Zeit.“ - Zusätzlich
withLocksje Ressource im Prozess (Reihenfolge, keine Fehlalarme bei Doppelklick). - Reihenfolge beim Buchen: erst die Buchung (
gehalten), dann die Zellen. Ob eine Zelle noch gilt, entscheidet allein ihre Buchung (fehlt ⇒ frei; Status nicht belegend ⇒ frei;gehaltenmit abgelaufenemheldUntil⇒ frei). Sperrenfolge einheitlich Gast → Buchung → Ressource. - Spike vor dem Bau (Pflicht, Plan-Aufgabe 1): auf einer Wegwerf-Collection in der Produktions-Appwrite
50 parallele Bulk-Creates mit überlappenden IDs ⇒ genau einer 201, Rest 409, keine Teil-Schreibungen;
danach Collection löschen. Braucht die Freigabe des Nutzers. Fällt der Spike negativ aus: Fallback
Transactions-API (
/v1/databases/transactions, ebenfalls in 1.8.1), sonst Einzel-Creates mit Rückbau unter der Prozess-Sperre. - Der Test-Nachbau (
test/support/appwriteMock.js) lernt Bulk-Create mit Alles-oder-nichts und 409,between/greaterThanEqual/lessThanEqual/cursorAfter.
6. Mails
Versand: neue Funktion sendGuestMail in mailer.js (nutzt den vorhandenen Transport): From
"<betrieb.name>" <SMTP_FROM> (Anzeigename = Betrieb, Adresse bleibt noreply@webklar.com, also keine
DNS-/Postfach-Änderung), replyTo = betrieb.email, nie BCC, icalEvent für Kalenderdaten. Alle
Termine-Mails laufen über termineOutbox (Muster „erst speichern, dann senden“): sofortiger Sendeversuch
nach dem Einreihen, sonst Job; höchstens 200 Mails/Stunde für den Terminplaner (Rest des
Hetzner-Budgets bleibt Portal/Abo); Wiederholung nach 1, 5, 30 Minuten, dann fehler und Hinweis im
Portal an der Buchung („Bestätigung konnte nicht zugestellt werden“). Sicherung: bei einem
SMTP-Anmeldefehler pausiert die Warteschlange 15 Minuten (Hetzner sperrt die IP nach mehreren
Fehlversuchen für ~10 Minuten).
Vorlagen (Engine mailTemplates.js; Kennungen um G erweitern, Test mailTemplates.test.js
EXPECTED mitpflegen). Gästemails haben ein neutrales Layout ohne WEBklar-Logo: Kopf mit Betriebsname
in farbe, Fuß mit Betrieb (Name, Adresse, Telefon, E-Mail, Links Impressum/Datenschutz) und dem Satz
„Diese E-Mail wurde im Auftrag von {Betrieb} über den Terminplaner von WEBklar versendet.“ Jede Vorlage hat
Blöcke sie/du.
Der Betreff ist je Vorlage der Platzhalter {{betreff}}; der Server setzt ihn je Anrede und Fall (Du-Form
sinngemäß: „Bitte bestätige deinen Termin …“). G01 und G08 enthalten keinen vom Gast eingegebenen Text
(kein Missbrauch als Mail-Relais).
| ID | an | Betreff (Sie-Form) | Inhalt |
|---|---|---|---|
| G01 | Gast | Bitte bestätigen Sie Ihren Termin bei {betrieb} | Datum, Uhrzeit, Leistung, Knopf „Termin bestätigen“, „Das waren Sie nicht? Dann ignorieren Sie diese E-Mail.“ Nichts Werbliches. |
| G02 | Gast | Ihr Termin bei {betrieb} am {datum} | Zusammenfassung, Buchungsnummer, Zahlungsart, Stornoregel (Wortlaut), Verwaltungslink, .ics (REQUEST); bei Widerrufsrecht: Widerrufsbelehrung + Muster-Widerrufsformular + Hinweis auf die Widerrufsfunktion (§ 312f Abs. 2 BGB): Bis zum Beginn des Termins über den Knopf „Vertrag widerrufen“, danach über das Formular auf der Website des Betriebs oder per E-Mail an den Betrieb |
| G03 | Gast | Ihre Terminanfrage bei {betrieb} | „Der Betrieb meldet sich“, Verwaltungslink (Absage und Verschieben, R26) |
| G04 | Gast | Ihre Terminanfrage bei {betrieb} – leider abgelehnt | abgelehnt, optionale Nachricht des Betriebs |
| G05 | Gast | Erinnerung: Ihr Termin am {datum} um {zeit} | Termin, Verwaltungslink; sonst nichts |
| G06 | Gast | Ihr Termin am {datum} wurde abgesagt | wer abgesagt hat, Grund/Nachricht, .ics (CANCEL) |
| G07 | Gast | Ihr Termin wurde verschoben | alte/neue Zeit, .ics (REQUEST, Sequenz + 1) |
| G08 | Gast | Eingangsbestätigung Ihres Widerrufs | Inhalt der Erklärung, Datum und Uhrzeit des Eingangs |
| K20 | Betrieb | Neue Buchung: {leistung} am {datum} um {zeit} (Block „anfrage“: Neue Terminanfrage …) | Gastdaten, Link ins Portal, .ics (PUBLISH) |
| K21 | Betrieb | Termin abgesagt / verschoben / widerrufen: … | Block je Fall, Link ins Portal |
K20/K21 im WEBklar-Layout (Du-Form wie K15–K19), Empfänger betrieb.email, ohne Team-BCC (enthalten
Gästedaten).
ICS (ics.js, eigener Schreiber, ~50 Zeilen, Zeilenfaltung/Escaping nach RFC 5545): UID = <bookingId>@termine.webklar.com (stabil), SEQUENCE = icsSequenz, DTSTART/DTEND in UTC, SUMMARY =
„{Leistung} – {Betrieb}“ (an den Betrieb: „{Leistung}: {Gastname}“), LOCATION = Adresse, ORGANIZER =
Betrieb, ATTENDEE = Gast (RSVP=FALSE), METHOD REQUEST/CANCEL. Keine „In Google eintragen“-Links.
Kalender-Abo für den Betrieb: je Ressource geheimer Link …/v1/kalender/<siteKey>/<feedToken>.ics (nur
bestätigte Termine und Sperren der nächsten 90 Tage, X-PUBLISHED-TTL: PT15M; Google aktualisiert nur
alle paar Stunden – steht so im Portal). Link neu erzeugen macht den alten ungültig.
7. Website-Teil
Einbau (vom Mitarbeiter, je Kunde gestaltet):
<div data-webklar-termine="SITEKEY" data-api="https://project.webklar.com" data-modus="inline">
<p>Termine telefonisch unter 06301 1234.</p> <!-- Ausweichtext, bleibt ohne JS/bei Fehlern stehen -->
</div>
<script src="https://project.webklar.com/embed/termine.js" defer></script>
data-modus: inline (Buchungsstrecke direkt in der Seite) oder knopf (Knopf mit Text aus
data-knopf, öffnet ein <dialog>; beim Laden der Seite ruft das Widget einmal die Konfiguration ab, für den
dauerhaften Knopf „Vertrag widerrufen“ (R54), alles Weitere erst beim Klick). Optional
data-leistung="s1" (vorausgewählt, z. B. Knopf neben einer Leistung).
public/embed/termine.js (ohne Abhängigkeiten, ohne Build, IIFE wie news.js):
- Light DOM mit Klassenpräfix
wkt-, Standard-Stile in@layer wkt, Gestaltung über CSS-Variablen der Seite:--wkt-akzent,--wkt-akzent-text,--wkt-hintergrund,--wkt-text,--wkt-rand,--wkt-radius,--wkt-schrift,--wkt-schrift-titel; Containerabfragen für schmale Spalten/Handy. - Kein Cookie, kein Web-Storage, kein Auslesen von Referrer/Bildschirm/Zeitzone, keine Drittanfragen;
Text nur per
textContent. - Barrierefrei nach WCAG 2.1 AA: echte Formularelemente, sichtbarer Fokus, Monatsraster nach dem
APG-Muster „Date Picker Dialog“ (Pfeiltasten,
aria-selected, Tage ohne Zeitenaria-disabled), Fehler überaria-live, Zielgröße ≥ 24 px, kein Zeitlimit im Widget (die 30 Minuten gelten erst nach dem Absenden). - Deutsche Texte in Sie- oder Du-Form nach
anrede. Fuß: „Vertrag widerrufen“ als zweiter Knopf (wenn nötig, siehe 4; im Modusknopfzusätzlich dauerhaft neben dem Buchungsknopf) und „Datenschutz“ (LinkdatenschutzUrl), klein „Terminbuchung über WEBklar“. - Zeitfalle (R53):
GET /confignenntformMinSekunden. Vor jedem Absenden wartet das Widget, bis das Formular-Token (gemessen mitperformance.now()ab Eintreffen der Konfiguration) so alt ist plus 0,5 Sekunden (Anzeige „Einen Moment …“, Knopf bleibt gesperrt); sonst würde der Server eine zu junge Buchung wie einen Automaten stillschweigend verwerfen. Gilt auch nach dem Neuladen der Konfiguration und beim Wiederholen nachneu_laden. - Die Telefonnummer des Betriebs (freier Text) wird nur zum
tel:-Link, wenn sie nur aus Ziffern, Leerzeichen und+ - ( ) / .besteht; die nationale Null „(0)“ hinter der Landesvorwahl entfällt. Sonst bleibt sie Text (Widget und Gästeseite, dieselbe Regel). - Fehler/inaktiv/Abo beendet: Ausweichtext bleibt sichtbar; bei Netzfehler mitten in der Strecke „Gerade nicht erreichbar. Bitte rufen Sie an: {telefon}“.
- Demo
public/embed/termine-demo.html, Einbauanleitung für Mitarbeiterdocs/funktionen/termine-einbau.md(Snippet, Variablen-Tabelle, Beispiele für drei Stile, Datenschutz-Absatz, Abnahme-Checkliste).siteKeyund fertigen Einbau-Code finden Mitarbeiter im Portal (Kundenansicht → Terminplaner → Einstellungen, Kasten „Einbau auf deiner Website“) und densiteKeyim WOMS (Kunde → Funktionen).
Gästeseiten public/termin.html + public/termin.js + public/termin.css: Bestätigen, Verwalten,
Widerrufen (Token im Fragment). Kopf mit Betriebsname in farbe, Fuß mit Links auf Impressum und
Datenschutz des Betriebs und auf das Impressum von WEBklar (https://webklar.com/impressum, R46; das
Portal hat kein /impressum.html). Der Widerruf steht dort als gleichrangiger Knopf (R47). noindex, keine Cookies.
8. Öffentliche Schnittstelle /api/public/termine/v1/:siteKey/…
Eigener Router (server/routes/publicTermine.js), in server/index.js vor dem globalen
express.json eingehängt (eigener Parser limit: '16kb'). Unbekannter oder inaktiver Schlüssel ⇒
immer dieselbe Antwort { aktiv: false } (200), nie ein Hinweis, ob der Schlüssel existiert. Antworten
enthalten nie customerId, projectId, featureId, Appwrite-, Stripe- oder Ticket-IDs; nur
Leistungs-/Ressourcen-IDs aus der Einstellung und die Buchungsnummer (8 Zeichen, aus der Buchungs-ID
abgeleitet, nur zur Anzeige).
| Methode, Pfad | Zweck | Cache |
|---|---|---|
GET /:siteKey/config |
Betriebsname, Anrede, Farbe, Leistungen (ohne interne Felder), wählbare Ressourcen, Regeln für die Anzeige, Zahlungsarten, Stornotext + Version, Rechtsflags (Widerruf ja/nein, mit Belehrung), Formular-Token (HMAC über siteKey + Zeit), formMinSekunden (R53) |
public, max-age=60, kein ETag (das Formular-Token ist bei jedem Abruf neu) |
GET /:siteKey/tage?leistung=&ressource=&monat=YYYY-MM |
Tage mit freien Zeiten | public, max-age=15 |
GET /:siteKey/zeiten?leistung=&ressource=&tag=YYYY-MM-DD |
freie Startzeiten; bei „egal“ ohne Angabe der Person | no-store |
POST /:siteKey/buchungen |
Formular abschicken ⇒ gehalten, G01 |
– |
POST /:siteKey/widerruf {name, email, nummer} |
Widerrufsfunktion ohne Link (tokenlos) | – |
POST /termin/vorschau, /termin/bestaetigen, /termin/absagen, /termin/verschieben, /termin/widerruf, /termin/tage, /termin/zeiten {token, …} |
Gästeseiten termin.html: Zusammenfassung, E-Mail bestätigen, Verwaltung durch den Gast (Widerruf mit Link), Tage/Zeiten zum Verschieben; der Token kommt aus dem Link der Mail; der Betrieb wird über den Token gefunden, nicht über den siteKey |
– |
GET /kalender/:siteKey/:feedToken.ics |
Kalender-Abo des Betriebs (Token je Ressource, konstantzeitig verglichen) | private, max-age=300 (enthält Gastnamen) |
Schutz (ohne Captcha-Dienst):
- CORS: GETs
Access-Control-Allow-Origin: *; POSTs nur von erlaubten Origins = Hosts des Projekts (Subdomain unterproject.webklar.com,liveDomain+ www-Gegenstück, wieanalyseHosts) undhttps://project.webklar.com(Gästeseiten); Preflight erlaubtContent-Type. - Honeypot (
kontakt_url, unsichtbar; R38) und Zeitfalle (Formular-Token muss ≥formMinSekunden, Vorgabe 3 s, und ≤ 2 h alt sein) ⇒ bei Treffer dieselbe Erfolgsantwort, aber nichts gespeichert, keine Mail. Ein Token älter als 2 h ⇒ 400 mit Codeneu_laden; das Widget holt dann eine neue Konfiguration. Das Widget wartet vor dem Absenden selbst, bis das Token alt genug ist (7). - Mengen (im Speicher, mit Obergrenze der Schlüssel wie
pushTime.js): je IP (IPv6 je /64) 10 Buchungen/10 min und 120 GET/min; je Funktion 60 gehaltene Buchungen/h; je E-Mail-Adresse und Funktion höchstensmaxOffeneJeEmailoffene Termine und 5 Bestätigungsmails/Tag;widerruf10/h je IP. - Eingaben streng prüfen (Längen, E-Mail-Syntax, Telefon nur Ziffern/Leerzeichen/+/-/(), Zusatzfelder
nur bekannte Schlüssel); Fehler als Feldfehler
{ fehler: { feld: text } }. - Appwrite nicht erreichbar ⇒ 503
{ fehler: { _: 'Gerade nicht erreichbar' } }, nie Rohfehler.
9. Verwaltung im Portal (für alle gleich)
Neue Ansicht #view-termine (Hash #termine), Kachel „Terminplaner“ im Funktionen-Tab mit Pille Aus /
Aktiv / Abgeschaltet (TP1) und Knopf „Termine öffnen“. In der App-Ansicht Unterseite von Funktionen
(APP_PARENT.termine = 'funktionen'). Mehrere Website-Projekte ⇒ Projektauswahl oben (wie News). Eigenes
Modul public/termine.js + public/termine.css (nicht in das schon 794 Zeilen lange funktionen.js).
Reiter:
- Termine – Liste nach Tagen (heute, morgen, …; „Woche“ / „Monat“ / Datumssprung), Filter offen/angefragt/bestätigt/abgesagt; Zeile: Uhrzeit, Leistung, Person/Platz, Gast, Status, Hinweise („Bestätigung nicht zugestellt“). Aktionen siehe 4; „+ Termin eintragen“, „Zeit sperren“. Zähler „{n} Anfragen warten“ auf der Kachel.
- Leistungen – Liste mit Reihenfolge; Felder wie im Datenmodell; Hinweis, dass Leistungsnamen keine Gesundheitsangaben enthalten dürfen.
- Team & Plätze – Ressourcen mit Wochenplan-Editor (je Tag mehrere Zeitfenster), Zuordnung der Leistungen, Abwesenheiten/Urlaub, Bundesland + „an Feiertagen geschlossen“, Sonderöffnungen, Kalender-Abo-Link je Ressource (Kopieren, Neu erzeugen).
- Einstellungen – Betriebsangaben für Gäste (vorbefüllt aus dem Kundenkonto), Anrede, Farbe, Regeln,
Gastfelder, Erinnerung, Stornotext (Mustertext als Vorschlag, deutlich als Muster gekennzeichnet),
Rechtliches („Ihre Gäste sind: Privatpersonen / Unternehmen / beides“; Hinweis, dass der Widerruf die
Stornofrist schlägt), Zahlung (TP1: nur „vor Ort“ mit Zahlarten; „online“ ausgegraut „kommt bald“),
Aufbewahrung, Einbau auf deiner Website (Einbau-Code aus
GET /einstellungeneinbau.snippet, nur lesen, mit Kopierknopf), Export (CSV der Buchungen), Status „aktiv seit …“.
- Erstes Öffnen ohne Einstellung: Auswahl einer Branchen-Vorlage (
salon,kosmetik,beratung,werkstatt,handwerk,tattoo,fotograf,hundesalon,sonstiges; Daten inserver/services/termine/vorlagen.js, die TP2 für den Fragebogen wiederverwendet), danach Bearbeiten.
Kunden-API server/routes/termine.js unter /api/termine (requireSession, assertOwnProject, Funktion
active außer für reines Lesen, Schreiblimit 120/h je Kunde): GET/PATCH /einstellungen (die Antwort nennt auch
siteKey, einbau.snippet, feeds, fehlt, anfragenOffen), POST /einstellungen/vorlage,
GET /buchungen?von&bis&status, GET /zeiten (freie Zeiten für Termine von Hand und Verschieben),
POST /buchungen (manuell/Sperre), DELETE /buchungen/:id (Sperre aufheben), POST /buchungen/:id/aktion
(annehmen|ablehnen|absagen|verschieben|nichterschienen|anonymisieren), POST /ressourcen/:id/feed (neu erzeugen),
GET /export.csv.
Kundenansicht (Mitarbeiter als Kunde): Lesen und Schreiben auf /api/termine/* erlaubt (GET, POST auf alles
unter /api/termine/, PATCH /einstellungen, DELETE /buchungen/:id; Einstellungen und Termine kosten nichts,
sind keine Erklärungen im Namen des Kunden); Einträge in
KUNDENANSICHT_ALLOWED + test/kundenansichtGuard.test.js, Protokoll termin_einstellungen,
termin_aktion, termin_manuell (Portal VIEW_ACTIONS, WOMS src/lib/viewLog.js).
10. Ticketsystem (WOMS) in TP1
- Portal:
FEATURE_KEYS = ['news','termine'];findActiveBySiteKey(siteKey, featureKey);activateClosedTicketsbleibt in TP1 bei News (TP2 verallgemeinert es samt Mails);funktionenAdmin.jsRoutePOST /api/team/funktionen/:customerId/:key(key∈FEATURE_KEYS;/newsbleibt gleichwertig) undadminViewmit allen Schlüsseln (Ticket-Lookups gebündelt statt N+1). - WOMS:
src/lib/funktionenView.js(Register der Funktionen, Zustandstexte, Bestätigungstexte),CustomerFunktionen.jsxzeigt je Projekt News und Terminplaner (eigene Zeilen), Aktionen „Aktivieren (ohne Zahlung)“ / „Abschalten“ je Funktion, Ticketnummer als Link/tickets?woid=, beim Terminplaner zusätzlich dersiteKeyin kleiner Schrift mit Kopierknopf (für den Einbau);teamApi.setFeature(customerId, key, …).viewLog.jsum die neuen Aktionen.
11. Recht und Datenschutz in TP1 (Pilot)
TP1 läuft nur im Pilot mit dem eigenen Testkunden; echte Gäste fremder Betriebe erst nach TP2
(AVV + Leistungsbeschreibung + Freigabe der Texte durch den Nutzer). TP1 liefert als Entwürfe
(Kennzeichnung „Entwurf“, Dateien in docs/rechtliches/ bzw. docs/funktionen/):
- Datenschutz-Absatz „Terminbuchung“ für die Website des Betriebs (Sie/Du), Hinweis unter dem Formular,
Stornotext-Muster, Widerrufsbelehrung + Muster-Widerrufsformular für Dienstleistungen (Anlage 1/2
EGBGB, Platzhalter des Betriebs) – Grundlage
recherche-recht.md11.x undrecherche-recht-zahlung.md7.3. - Abschnitt „6c Terminplaner“ für
public/datenschutz.html(WEBklar als Auftragsverarbeiter; was das Portal selbst verarbeitet) – nicht veröffentlicht vor TP2. - Zitat-Korrektur im Bestand („§ 356 Abs. 4“ ⇒ „§ 356 Abs. 5 Nr. 2 BGB“ in
config.funktionen.consentText,funktionen-leistungsbeschreibung.html, Test) erfolgt in TP2 mit dem neuen Einwilligungstext je Funktion.
Löschfristen (täglicher Job im Terminplaner selbst, unabhängig von retention.js, weil die Fristen je
Buchung laufen und nicht am Vertragsende des Kunden hängen): verfallen und unbestätigte nach 48 h; abgesagt/abgelehnt nach 30 Tagen;
erledigt/nichterschienen nach aufbewahrungTage (Standard 90, erlaubt 14–365) ab Terminende;
Warteschlange: gesendet 7 Tage nach dem Versand, gescheitert 48 Stunden nach dem Fehler; Zellen mit der Buchung.
Funktion disabled ⇒ neue Buchungen aus, nach 30 Tagen (Zeit für den Export) Löschung aller Buchungen, Zellen,
Einstellung. Kunde gelöscht ⇒ Funktion disabled (Haken in customerAdmin.deleteCustomerWithPortalAccess, wie
Analyse). In TP1 löst nur disabled die Löschung aus (WOMS „Abschalten“, Kunde gelöscht); ein Haken für das Ende des
Wartungs-Abos kommt erst mit TP2, obwohl der 6c-Entwurf das Abo-Ende schon nennt.
12. Fehlerfälle
- Slot inzwischen weg (409 beim Belegen) ⇒ Meldung im Widget, Zeiten neu laden.
- Bestätigungslink nach Ablauf ⇒ „abgelaufen“; zweiter Klick auf denselben Link ⇒ „bereits bestätigt“ + Verwaltungslink-Hinweis.
- Mail scheitert endgültig ⇒ Hinweis an der Buchung im Portal; Buchung bleibt gültig.
- Einstellung ungültig (z. B. Leistung ohne Ressource) ⇒ Speichern mit Feldfehler abgelehnt; Leistung ohne aktive Ressource wird im Widget nicht angezeigt.
- Leistung oder Ressource gelöscht, während Termine bestehen ⇒ nur deaktivieren erlaubt, solange künftige Termine daran hängen (Meldung mit Anzahl).
- Funktion abgeschaltet ⇒ öffentliche Schreibrouten
{ aktiv: false }, Gästeseiten erlauben weiter Absagen/Widerruf bestehender Termine. - Uhr-Umstellung ⇒ keine Slots in der fehlenden Stunde; Termine über die Umstellung behalten ihre echte Dauer.
13. Tests TP1
npm test(node --test): Zeit (beide Umstellungstage), Feiertage je Land (Stichproben 2026/2027), Slots (Pausen, Puffer, Vorlauf, Horizont, Abwesenheit, Feiertag, Sonderöffnung, „egal“), Zellen/Bulk (Mock: Alles-oder-nichts), Tokens (einmalig, Ablauf), ICS (Faltung, Escaping, CANCEL), Vorlagen (alle G/K-Vorlagen rendern ohneproblems,EXPECTED), Einstellungs-Validierung, öffentliche Routen (keine internen IDs,{aktiv:false}einheitlich, CORS/Origin, Honeypot, Zeitfalle, Mengen, Knopftexte je Fall, Widerruf mit/ohne Treffer), Kunden-Routen (Rechte, fremdes Projekt 403, Versionskonflikt 409, Kundenansicht-Liste), Jobs (Verfall, Erinnerung genau einmal, Erledigt, Löschfristen), Warteschlange (Rate, Wiederholung, Pause nach Anmeldefehler).- Playwright (Fixture-Server, wie
test/ui/*.ui.mjs): Widget inline und Knopf auf einer Testseite (Desktop, Handy, nur Tastatur, Ausweichtext bei Fehler), Gästeseiten (bestätigen, absagen, verschieben, widerrufen), Portal-Ansicht (alle Reiter, Vorlage wählen, Wochenplan-Editor) – Screenshots nach/root/reports/terminplaner-ui/. - WOMS:
npm test(Register, Zustände),vite build. - Live-Spike Bulk-Create (5) vor dem Bau.
14. Livegang TP1 (je Schritt Freigabe des Nutzers)
- L1 Portal: Push
feature/terminplaner→main(der Nutzer pusht), Schalter bleibtoff; Abhängigkeitfeiertagejskommt mit dem Deploy (deploy.shführtnpm installaus). - L2 Schema:
termine-setup.mjs --check, dann anlegen (im Container; das Skript kommt mit L1). Danach Compose:TERMINE_ENABLED=pilot,TERMINE_PILOT_EMAILS=kenso.gri@gmail.com, Container neu erstellen (docker compose up -d --no-deps kundenbereich-server, niepreview-portal). - L3 WOMS: Push auf
test. - L4 Pilot: im WOMS beim Kunden „test“ den Terminplaner für „Test-Website“ aktivieren; im Portal (Login
des Testkunden) Vorlage wählen, Leistungen/Zeiten pflegen; Widget in
WEBklar/test-websiteeinbauen (siteKeyim WOMS, fertiger Einbau-Code im Portal unter Einstellungen) (Push → Deploy) inkl. Datenschutz-Absatz-Entwurf; Testbuchungen mit kenso.gri@gmail.com: bestätigen, Erinnerung, absagen, verschieben, widerrufen, Anfrage annehmen/ablehnen, Kalender-Abo in Google. - L5 Nutzertest; Aufräumen der Testbuchungen nach Wunsch.
Betrieb und Livegang – Hinweise
SESSION_SECRETdarf nie rotiert werden: Verwaltungs-, Formular- und Feed-Links hängen daran (token.js, Schlüssel mit Zweckbindung; Compose fällt aufPREVIEW_SESSION_SECRETzurück). Eine Rotation entwertet alle schon versandten Links, auch den Widerrufs-Link in G02.- Der Modus
offstoppt auch die Löschjobs (die Aufbewahrungsfristen laufen nicht mehr). Pilotbuchungen vor dem endgültigen Abschalten löschen (Funktion abschalten und Löschfrist abwarten, oder die Daten von Hand löschen); alternativTERMINE_PILOT_EMAILS=leer lassen, stattoffzu setzen. - Die tägliche Zellbereinigung liest alle Zellen (Deckel 100 000,
idx_bisungenutzt). Vor TP2 muss sie überidx_bis(lessThan('bis', …)) und seitenweise begrenzt werden, sonst steht sie bei vielen Betrieben still (list_limit_exceeded). Gleiches Muster:GET /export.csvundloescheAbgeschaltete(Standarddeckel 20 000). verarbeite(Warteschlange) listet jede Minute alle fälligen Einträge samt Inhalt, auch wenn das Stundenbudget leer ist. In TP1 unkritisch; vor TP2 Frühausstieg bei leerem Budget.
TP2 Verkauf (eigener Plan nach TP1)
- Fragebogen im Portal (Kachel „Terminplaner einrichten · 149 €“, nur mit Wartungs-Abo): 8 Schritte
(Branche/Vorlage mit Gesundheits-Ausschluss · wer wird gebucht + Namen · Leistungen · Zeiten, Bundesland,
Urlaub · Regeln + sofort/Anfrage · Gastfelder · Benachrichtigungen, Anrede, Erinnerung · Website: wo
buchen, Knopf/inline, Gestaltungswunsch, Ausweichtelefon, Links Impressum/Datenschutz), Zwischenspeichern
als Entwurf in
termineConfig. Ergebnis = dieselbe Einstellung wie TP1 + Einbauwünsche. - Bestellung: Zusammenfassung, Haken Leistungsbeschreibung + AVV Terminplaner (Fassung), „kein
Gesundheitsbereich“, Widerrufs-Zustimmung für Verbraucher (§ 356 Abs. 5 Nr. 2 BGB), Knopf
„Zahlungspflichtig bestellen · 149 €“ ⇒ Stripe Checkout (einmalig, Katalogeintrag
termineSetup14900 Cent). Verallgemeinerung des Funktionen-Checkouts: Registerconfig.features(Preis, Einwilligungstext, Zeiteinschätzung, Ticket-Text, Mails, Rücksprung-Hash) statt fester News-Werte; K15/K16/T16 mit Platzhalter{{funktion}}; Widerrufsformular-Beschriftung. - Ticket mit Einbauanleitung (
services/termine/einbauanleitung.js, deterministisch aus der Einstellung): Kurzfassung inticket.details(≤ 8000 Zeichen), vollständig im WOMS-Reiter Funktionen (Kopieren-Knopf) überGET /api/team/funktionen/:customerId/termine/anleitung. Inhalt: Modell, Leistungen, Ressourcen + Zeiten, Regeln, Gastfelder, Benachrichtigungen, Einbau (Snippet mit siteKey, Modus, CSS-Variablen-Vorschlag, Stelle auf der Website), Datenschutz-Absatz, Abnahme-Checkliste, Abrechnung („Einrichtung bezahlt, nicht auf Abo-Stunden“). - Abo-Kopplung: Bestellen nur mit laufendem Wartungs-Abo (gemeinsame Prüfung aus
analyse.jsherausgelöst); täglicher Abgleich: Abo beendet ⇒disabled, Mail an den Kunden, Löschung nach 30 Tagen. - Rechtstexte: Leistungsbeschreibung Terminplaner, AVV Terminplaner (Geschwister des AVV
Website-Analyse), Datenschutz 6c, Gästetexte final, Karten in WOMS „Rechtliches“; Prüfrunde wie bei der
Analyse; danach
TERMINE_ENABLED=on.
TP3 Online-Zahlung (eigener Plan nach TP2)
- Stripe Connect, verbundene Konten mit Standard-Verhalten (
controller: volles Dashboard,losses.payments=stripe,fees.payer=account,requirement_collection=stripe), Fähigkeitcard_payments, Rahmenvertragfull; Einrichtung per Account Link (von Stripe gehostet); Status überaccount.updated+ Rücksprung. Sandbox-Plattformeinrichtung ist erledigt (Testkonto zeigt Negativsalden/Anforderungen/Gebühren = Stripe); Live-Plattformeinrichtung vor dem Livegang. - Direct Charges über von Stripe gehostetes Checkout auf dem Konto des Betriebs (
Stripe-Account),mode: payment, nur Festpreise ≥ 0,50 €, keine Plattformgebühr, SEPA ausgeschlossen (verzögert), PayPal nicht verfügbar (Stripe unterstützt es bei Direct Charges nicht),expires_at≥ 30 Minuten ⇒ Haltezeit bei Online-Zahlung = Sitzungsdauer + 2 Minuten. - Zahlungsmodus je Betrieb
vorort | online | beides(beides ⇒ Frage „Wie möchten Sie bezahlen?“ ohne Vorauswahl, gleicher Preis); online nur, wenn das verbundene Kontocharges_enabledhat, sonst automatisch zurück auf vor Ort + Hinweis. - Ablauf online (Vorschlag, in TP3 zu bestätigen): Widget „Zahlungspflichtig buchen“ ⇒ Bestätigungslink
⇒ Weiterleitung zu Stripe ⇒ Zahlung ⇒
bestaetigtper Webhook; späte Zahlung bei vergebenem Slot ⇒ sofortige Erstattung. - Eigener Connect-Webhook (eigenes Secret,
event.account+livemodeprüfen, idempotent je Sitzung), nicht inroutes/abo.js. - Erstattungsregeln (
recherche-recht-zahlung.md3.5): Betrieb sagt ab ⇒ voll; Gast in der Frist ⇒ voll; Widerruf ⇒ voll binnen 14 Tagen; zu spät/nicht erschienen ⇒ nach Regel des Betriebs; fehlendes Guthaben ⇒ „Erstattung ausstehend“ im Portal + Mail. - Rechtliches: Stripe-Absatz für die Datenschutzerklärung des Betriebs, AVV/Leistungsbeschreibung ergänzen, keine Kassenfunktion, kein Rechnungsdruck.
Nicht Teil (V1)
Gruppen/Kurse/Tischreservierung · Person + Platz gleichzeitig · Mehrere Leistungen in einer Buchung · Varianten (30/60/90) · Warteliste · SMS · Zwei-Wege-Kalender-Sync (Google/Outlook) · Anzahlungen · Kundenkonten für Gäste · Bewertungs- oder Werbemails · Mehrsprachigkeit · Gesundheitsbereich · Wechsel des Portal-Containers auf Node 24 (eigene Aufgabe; Node 20 ist seit 30.04.2026 ohne Pflege).
Offene Punkte
- Spike Bulk-Create (TP1, Aufgabe 1) – Freigabe für Wegwerf-Collection in der Produktions-Appwrite.
- Rechtstexte: alle als Entwurf; Prüfung vor TP2-Livegang (wie Website-Analyse), u. a. Einstufung Massage/Fotograf/Coaching als Widerrufsfälle und ob der Bestätigungslink die Bestellung ist.
- TP3: Stripe-Live-Plattformeinrichtung durch den Nutzer; Ablauf „Bestätigungslink vor Zahlung“ bestätigen.