diff --git a/docs/rechtliches/termine-datenschutz-6c-entwurf.md b/docs/rechtliches/termine-datenschutz-6c-entwurf.md index 3c685ea..d049801 100644 --- a/docs/rechtliches/termine-datenschutz-6c-entwurf.md +++ b/docs/rechtliches/termine-datenschutz-6c-entwurf.md @@ -33,3 +33,5 @@ Fassung 2026-10-02. Entwurf für `public/datenschutz.html`, **nicht eingebaut un 4. **Sicherungen**: Die Datensicherung bei Hetzner behält gelöschte Daten noch einige Zeit; die Frist steht nicht im Text und ist vor dem Livegang zu klären (AVV Terminplaner Nr. 9). 5. **Zahlung (TP3)**: Wenn Online-Zahlung kommt, braucht dieser Abschnitt einen Satz zu Stripe (Abschnitt 7 und `recherche-recht-zahlung.md` 7.3 G). 6. **Geschäftsbriefe**: Bestätigungen mit Preis sind keine Rechnungen; die Aufbewahrung nach Abschnitt 9 betrifft den Terminplaner nicht. +7. **Ende des Wartungs-Abos**: Der Satz „Endet das Wartungs-Abo oder schaltet der Kunde die Funktion ab, löschen wir alle Termindaten des Kunden 30 Tage danach“ gilt in TP1 nur für das Abschalten (WOMS „Abschalten“, Kunde gelöscht). Ein Haken für das Ende des Wartungs-Abos kommt erst mit TP2; bis dahin den Satz anpassen oder den Haken liefern. +8. **Knopfmodus**: Dort lädt das Widget die Konfiguration (Texte und Leistungen, nicht die Termine) schon beim Laden der Seite, nicht erst beim Öffnen (wegen des dauerhaften Knopfs „Vertrag widerrufen“, R54). Die Formulierung „beim Öffnen der Terminbuchung zusätzlich die Termine und Texte“ danach prüfen; es geht an denselben Server wie das Skript, ein neuer Empfänger entsteht nicht. diff --git a/docs/rechtliches/termine-datenschutz-absatz.md b/docs/rechtliches/termine-datenschutz-absatz.md index 0600384..12be453 100644 --- a/docs/rechtliches/termine-datenschutz-absatz.md +++ b/docs/rechtliches/termine-datenschutz-absatz.md @@ -37,6 +37,7 @@ Zwei Fassungen weiter unten: Sie und Du. Nimm die Fassung, die zur Datenschutzer 3. **Löschlauf**: „48 Stunden“ und „30 Tage“ sind die frühesten Zeitpunkte, an denen der Löschlauf löscht; er läuft in Abständen. 4. **Kontakt der Gäste**: Anfragen nach Auskunft oder Löschung gehen an den Betrieb; WEBklar leitet Anfragen, die bei ihr eingehen, als Auftragsverarbeiter weiter (in der Datenschutzerklärung von WEBklar, 6c). 5. **Gesundheitsbereich**: Der Terminplaner ist dafür nicht bestimmt; der Satz „Bitte keine Angaben zur Gesundheit“ steht auch im Formular. +6. **Knopfmodus**: Das Widget lädt die Konfiguration (Texte und Leistungen) im Knopfmodus schon beim Laden der Seite (R54); „Aufruf“ (Abweichung 3) stimmt also auch für diesen Abruf. Prüfen, ob der Text zwischen Skript und Konfiguration unterscheiden muss; beides geht an denselben Server. ## Fassung Sie diff --git a/docs/rechtliches/termine-hinweise.md b/docs/rechtliches/termine-hinweise.md index 7aefe56..1713c4d 100644 --- a/docs/rechtliches/termine-hinweise.md +++ b/docs/rechtliches/termine-hinweise.md @@ -30,7 +30,7 @@ Wortlaut immer in der Sie-Form, auch bei Betrieben, die ihre Gäste duzen: Es si | Haken | Wann | Pflicht | Wortlaut | |---|---|---|---| -| Vorzeitiger Beginn | verbindlich, mit Preis, Verbraucher, Terminbeginn vor Ablauf von 14 Tagen nach der Buchung | ja | Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB). | +| Vorzeitiger Beginn | Verbraucher, mit Preis: bei sofort buchbaren Leistungen, wenn der Terminbeginn nicht nach dem Ende der Widerrufsfrist liegt (14 Tage ab dem Tag nach dem Vertragsschluss, Berliner Kalender, §§ 187, 188, 193 BGB; gerechnet mit dem Zeitpunkt der Buchung plus Haltezeit; R29); bei bepreisten Anfragen immer, weil der Tag der Annahme nicht feststeht (R31) | ja | Ich verlange ausdrücklich, dass {{Firma}} mit der Leistung vor Ablauf der Widerrufsfrist beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB). | | Stornobedingungen | der Betrieb hat Stornobedingungen hinterlegt | ja | Ich habe die Stornobedingungen gelesen. | | Erinnerung | der Betrieb bietet Erinnerungen an | nein | Ich möchte {{n}} Stunden vor dem Termin eine Erinnerung per E-Mail erhalten. | @@ -51,12 +51,14 @@ Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt (Spezifikation, ## 5. Widerrufshinweis im Widget und in der Mail (Entwurf, Rahmen) -Er ersetzt nicht die Widerrufsbelehrung (`termine-widerrufsbelehrung-muster.md`), sondern führt zu ihr. Gilt nur für verbindliche Buchungen von Verbrauchern. +Er ersetzt nicht die Widerrufsbelehrung (`termine-widerrufsbelehrung-muster.md`), sondern führt zu ihr. Gilt für Buchungen von Verbrauchern, auch für Anfragen: Der Vertrag kommt mit der Annahme zustande, ist aber ebenfalls ein Fernabsatzvertrag, und die Frist läuft ab der Annahme (R55, R29). Die Widerrufsfunktion steht im Widget als zweiter Knopf im Fuß und im Knopfmodus dauerhaft neben dem Buchungsknopf (R54). **Mit Widerrufsrecht** (Zusammenfassung vor dem Knopf und G02 vor der Belehrung): > **Widerrufsrecht.** Wenn Sie Verbraucher sind, können Sie diesen Vertrag innerhalb von 14 Tagen ab Vertragsschluss ohne Angabe von Gründen widerrufen. Dafür nutzen Sie die Schaltfläche „Vertrag widerrufen“ im Terminbereich auf unserer Website oder den Link „Termin verwalten“ in Ihrer Bestätigungs-E-Mail. Die ausführliche Widerrufsbelehrung und das Muster-Widerrufsformular stehen in Ihrer Bestätigungs-E-Mail. +Maßgeblich für die Mail ist die Vorlage G02: Bis zum Beginn des Termins geht der Widerruf über den Knopf „Vertrag widerrufen“; danach über das Formular „Vertrag widerrufen“ auf der Website des Betriebs oder per E-Mail an den Betrieb. Die Verwaltungsseite bietet den Knopf nur bis zum Beginn des Termins an. + **Freizeit-Dienstleistung mit festem Termin** (§ 312g Abs. 2 Nr. 9 BGB; `recht.freizeit`), kein Widerrufsrecht, keine Belehrung, kein Formular, keine Funktion: > **Für diese Buchung besteht kein Widerrufsrecht.** Ihre Buchung ist verbindlich; Absagen richten sich nach den Stornobedingungen. @@ -77,7 +79,7 @@ Der Eingang muss mit Datum und Uhrzeit bestätigt werden (§ 356a BGB); das ist ## Für die Rechtsprüfung und das Widget offen -1. **Belehrung vor dem Vertragsschluss**: Das Muster verlangt die Information, bevor der Gast seine Erklärung abgibt (Art. 246a § 4 Abs. 1 EGBGB). Hier steht der Hinweis (Abschnitt 5) in der Zusammenfassung, die vollständige Belehrung in der Bestätigungs-E-Mail. Prüfen, ob die vollständige Belehrung (`widerrufsbelehrung()`) schon in der Zusammenfassung stehen muss; der Text ist dafür geeignet (reiner Text mit Zeilenumbrüchen). +1. **Belehrung vor dem Vertragsschluss** (erledigt durch R14, R55): Das Widget zeigt die vollständige Belehrung samt Muster-Widerrufsformular (`texte.widerrufsbelehrung`, aufklappbar) in der Zusammenfassung vor dem Knopf, bei jeder Verbraucher-Buchung auch bei Anfragen; der Nachweis der Buchung hält ihren Hash fest. Die Bestätigungs-E-Mail wiederholt sie. Offen bleibt nur, ob der Rechtsprüfer die aufklappbare Darstellung genügen lässt. 2. **Widerruf der Einwilligung zur Erinnerung**: Der feste Wortlaut des Hakens (Abschnitt 2) enthält den Hinweis auf den Widerruf nicht; er steht im Datenschutz-Absatz (`termine-datenschutz-absatz.md`). Prüfen, ob ein kurzer Zusatz unter dem Haken nötig ist („Sie können diese Einwilligung jederzeit widerrufen.“). 3. **Link zur Datenschutzerklärung** neben dem Hinweis: `betrieb.datenschutzUrl` ist in der Einstellung optional. Ohne Link verweist der Hinweis auf eine Datenschutzerklärung, die der Gast nicht findet; das Portal sollte die Adresse vor dem Einschalten verlangen. 4. **Bestätigungsfrist**: Der Hinweis nennt „48 Stunden nach Ablauf der Bestätigungsfrist“. Der Löschlauf läuft in Abständen; „48 Stunden“ ist die früheste Löschung, nicht eine Sekundengarantie. diff --git a/docs/superpowers/plans/2026-10-02-terminplaner-tp1-kern.md b/docs/superpowers/plans/2026-10-02-terminplaner-tp1-kern.md index 345781e..1c4aed7 100644 --- a/docs/superpowers/plans/2026-10-02-terminplaner-tp1-kern.md +++ b/docs/superpowers/plans/2026-10-02-terminplaner-tp1-kern.md @@ -212,7 +212,7 @@ ### Task 20: Gästeseiten **Files:** Create `public/termin.html`, `public/termin.js`, `public/termin.css`, `test/ui/termin-seiten.ui.mjs`. -**Produces:** liest `#v=`/`#m=`, entfernt den Hash per `history.replaceState`, ruft `/termin/vorschau`; Ansichten Bestätigen (Zusammenfassung + `knopf`), Bestätigt/Angefragt, Abgelaufen (Link `websiteUrl`, Telefon), Verwalten (Absagen mit Rückfrage in der Seite, Verschieben über `/termin/tage|zeiten`, „Vertrag widerrufen“ → Formular nach § 356a mit vorbefülltem Namen/Buchungsnummer/E-Mail, Knopf „Widerruf bestätigen“, Eingang mit `eingang.datum`/`uhrzeit`); Kopf in `farbe`; Fuß: Impressum/Datenschutz des Betriebs + „Impressum WEBklar“ (`/impressum.html`); `noindex`; keine Cookies; Sie/Du. +**Produces:** liest `#v=`/`#m=`, entfernt den Hash per `history.replaceState`, ruft `/termin/vorschau`; Ansichten Bestätigen (Zusammenfassung + `knopf`), Bestätigt/Angefragt, Abgelaufen (Link `websiteUrl`, Telefon), Verwalten (Absagen mit Rückfrage in der Seite, Verschieben über `/termin/tage|zeiten`, „Vertrag widerrufen“ → Formular nach § 356a mit vorbefülltem Namen/Buchungsnummer/E-Mail, Knopf „Widerruf bestätigen“, Eingang mit `eingang.datum`/`uhrzeit`); Kopf in `farbe`; Fuß: Impressum/Datenschutz des Betriebs + „Impressum WEBklar“ (`https://webklar.com/impressum`); `noindex`; keine Cookies; Sie/Du. - [ ] Playwright: G01-Link aus dem Sink öffnen ⇒ bestätigen ⇒ G02 im Sink; zweiter Aufruf „bereits bestätigt“; abgelaufen; absagen; verschieben; widerrufen (`timezoneId: 'America/New_York'` zeigt Berliner Eingangszeit); 390 px. - [ ] FAIL → implementieren → PASS → Commit „Terminplaner: Gaesteseiten Bestaetigen, Verwalten, Widerrufen“ diff --git a/docs/superpowers/specs/2026-10-02-terminplaner-design.md b/docs/superpowers/specs/2026-10-02-terminplaner-design.md index 17dfd1b..a775fe0 100644 --- a/docs/superpowers/specs/2026-10-02-terminplaner-design.md +++ b/docs/superpowers/specs/2026-10-02-terminplaner-design.md @@ -70,7 +70,7 @@ Vorgaben aus dem Entwurf, denen der Nutzer nicht widersprochen hat: ### 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, Indizes nur bei neuen Collections). Alle Collections `permissions: []`, +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. @@ -96,7 +96,7 @@ behandelt `appwriteAdmin.formatRequestBody` als fertig verpackten Body), `versio "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 } ], + "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 @@ -133,14 +133,15 @@ Indizes: `idx_feature_start(featureId, startsAt)`, `idx_status_held(status, held **`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, Haltezeit oder Terminende). +`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)`. -Gesendete und endgültig gescheiterte Einträge werden nach 7 Tagen gelöscht (enthalten personenbezogene Daten). +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. @@ -151,7 +152,7 @@ Gesendete und endgültig gescheiterte Einträge werden nach 7 Tagen gelöscht (e (Oktober) nehmen das **erste** Vorkommen. Tests für 2026-03-29 und 2026-10-25. `deadlines.berlinTime` wird dafür **nicht** benutzt (kennt die Lücke nicht). - **Feiertage** (`feiertage.js`): Paket `feiertagejs` (MIT, ohne Abhängigkeiten) je Bundesland; - `sonderoeffnungen` und `abwesenheiten` schlagen Feiertage/Plan. + `sonderoeffnungen` schlagen Feiertag und Plan, `abwesenheiten` schlagen alles, auch eine Sonderöffnung (R11). - **Slots** (`slots.js`, `now` wird übergeben): je Ressource und Berliner Kalendertag die Öffnungsfenster aus `plan` (oder `sonderoeffnungen`), minus Feiertag (wenn geschlossen), minus Abwesenheiten, minus belegte Zeiten (Buchungen mit Status `gehalten|angefragt|bestaetigt` und Sperren, jeweils bis @@ -180,7 +181,9 @@ Zustände (`status`): `gehalten` (Formular abgeschickt, wartet auf E-Mail-Bestä „Bitte keine Gesundheitsangaben.“ 5. 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 auf `datenschutzUrl`), Haken: + Datenschutz (Text 11.2 aus `recherche-recht.md`, Link auf `datenschutzUrl`), 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.text` gesetzt) - 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 @@ -194,8 +197,9 @@ Zustände (`status`): `gehalten` (Formular abgeschickt, wartet auf E-Mail-Bestä 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::'], {waitMs: 5000})` Zellen belegen (5), Buchung -`gehalten` mit `heldUntil = now + 30 min` anlegen, Mail G01 in die Warteschlange (sofort senden). +`withLocks(['termine-res::'], {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=`: Die Seite liest das Token aus dem Fragment (geht nicht an Server-Logs), zeigt per `POST …/vorschau` die Zusammenfassung und einen Knopf @@ -212,23 +216,35 @@ Lage - „Termin absagen“ bis `absageBisStd` vor 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; + (eine Sperre über beide Ressourcen), Status bleibt, `icsSequenz + 1`, G07 + K21; das gilt auch für `angefragt` + (R26: G03 enthält nicht nur „Absage“, die Seite bietet auch „Termin verschieben“); - **„Vertrag widerrufen“**, solange ein Widerrufsrecht besteht (verbindlich, Verbraucher, nicht `freizeit`, - innerhalb von 14 Tagen nach `bestaetigtAt`) – 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` (von `gast`), G08 Eingangsbestätigung mit Datum und Uhrzeit, - K21-Block „Widerruf“. + Status `bestaetigt`, 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 ist `bestaetigtAt` bei einer sofort buchbaren Leistung und + `angenommenAt` bei 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` (von `gast`), 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/K21 `nach_frist` bzw. `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“, sobald der Betrieb verbindliche Leistungen für Verbraucher -anbietet. Formular: Name, E-Mail, Buchungsnummer (steht in G02), Knopf „Widerruf bestätigen“. Der Server +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); Termin von Hand eintragen (`quelle: manuell`, Gast-E-Mail optional, G02 nur wenn +(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). @@ -249,6 +265,9 @@ bei telefonischen Buchungen Sache des Betriebs); Zeit sperren (`quelle: sperre`, `POST …/documents` mit `documents: [...]`, 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 `withLocks` je 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; `gehalten` mit abgelaufenem `heldUntil` ⇒ 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 @@ -282,8 +301,8 @@ sinngemäß: „Bitte bestätige deinen Termin …“). G01 und G08 enthalten ke | 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) | -| G03 | Gast | Ihre Terminanfrage bei {betrieb} | „Der Betrieb meldet sich“, Verwaltungslink (nur Absage) | +| 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} | 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) | @@ -316,7 +335,8 @@ alle paar Stunden – steht so im Portal). Link neu erzeugen macht den alten ung ``` `data-modus`: `inline` (Buchungsstrecke direkt in der Seite) oder `knopf` (Knopf mit Text aus -`data-knopf`, öffnet ein ``; Daten werden erst beim Klick geladen). Optional +`data-knopf`, öffnet ein ``; 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`): @@ -329,16 +349,27 @@ alle paar Stunden – steht so im Portal). Link neu erzeugen macht den alten ung APG-Muster „Date Picker Dialog“ (Pfeiltasten, `aria-selected`, Tage ohne Zeiten `aria-disabled`), Fehler über `aria-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“ (wenn nötig, siehe 4) und - „Datenschutz“ (Link `datenschutzUrl`), klein „Terminbuchung über WEBklar“. +- Deutsche Texte in Sie- oder Du-Form nach `anrede`. Fuß: „Vertrag widerrufen“ als zweiter Knopf (wenn nötig, + siehe 4; im Modus `knopf` zusätzlich dauerhaft neben dem Buchungsknopf) und „Datenschutz“ (Link + `datenschutzUrl`), klein „Terminbuchung über WEBklar“. +- Zeitfalle (R53): `GET /config` nennt `formMinSekunden`. Vor jedem Absenden wartet das Widget, bis das + Formular-Token (gemessen mit `performance.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 nach `neu_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 Mitarbeiter `docs/funktionen/termine-einbau.md` - (Snippet, Variablen-Tabelle, Beispiele für drei Stile, Datenschutz-Absatz, Abnahme-Checkliste). + (Snippet, Variablen-Tabelle, Beispiele für drei Stile, Datenschutz-Absatz, Abnahme-Checkliste). `siteKey` und + fertigen Einbau-Code finden Mitarbeiter im Portal (Kundenansicht → Terminplaner → Einstellungen, Kasten „Einbau auf + deiner Website“) und den `siteKey` im 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. `noindex`, keine Cookies. +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/…` @@ -351,28 +382,28 @@ abgeleitet, nur zur Anzeige). | Methode, Pfad | Zweck | Cache | |---|---|---| -| `GET /config` | Betriebsname, Anrede, Farbe, Leistungen (ohne interne Felder), wählbare Ressourcen, Regeln für die Anzeige, Zahlungsarten, Stornotext + Version, Rechtsflags (Widerruf ja/nein), Formular-Token (HMAC über siteKey + Zeit) | `public, max-age=60`, ETag | -| `GET /tage?leistung=&ressource=&monat=YYYY-MM` | Tage mit freien Zeiten | `max-age=15` | -| `GET /zeiten?leistung=&ressource=&tag=YYYY-MM-DD` | freie Startzeiten; bei „egal“ ohne Angabe der Person | `no-store` | -| `POST /buchungen` | Formular abschicken ⇒ `gehalten`, G01 | – | -| `POST /vorschau` `{token}` | Zusammenfassung für Bestätigen/Verwalten | – | -| `POST /bestaetigen` `{token}` | E-Mail bestätigt | – | -| `POST /absagen`, `POST /verschieben` `{token, …}` | Verwaltung durch den Gast | – | -| `POST /widerruf` `{name, email, nummer, token?}` | Widerrufsfunktion | – | -| `GET /kalender/:siteKey/:feedToken.ics` | Kalender-Abo des Betriebs (Token je Ressource, konstantzeitig verglichen) | `max-age=300` | +| `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 unter `project.webklar.com`, `liveDomain` + www-Gegenstück, wie `analyseHosts`) und `https://project.webklar.com` (Gästeseiten); Preflight erlaubt `Content-Type`. -- **Honeypot** (`website`, unsichtbar) und **Zeitfalle** (Formular-Token muss ≥ 3 s und ≤ 2 h alt sein) ⇒ - bei Treffer dieselbe Erfolgsantwort, aber nichts gespeichert, keine Mail. +- **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 Code `neu_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öchstens `maxOffeneJeEmail` offene Termine und 5 Bestätigungsmails/Tag; `widerruf` 10/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. +- Appwrite nicht erreichbar ⇒ 503 `{ fehler: { _: 'Gerade nicht erreichbar' } }`, nie Rohfehler. ### 9. Verwaltung im Portal (für alle gleich) @@ -395,19 +426,23 @@ Reiter: 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, **Export** (CSV der Buchungen), Status „aktiv seit …“. + Aufbewahrung, **Einbau auf deiner Website** (Einbau-Code aus `GET /einstellungen` `einbau.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 in `server/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`, -`GET /buchungen?von&bis&status`, `POST /buchungen` (manuell/Sperre), `POST /buchungen/:id/aktion` -(`annehmen|ablehnen|absagen|verschieben|nichterschienen`), `POST /ressourcen/:id/feed` (neu erzeugen), +`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 -(Einstellungen und Termine kosten nichts, sind keine Erklärungen im Namen des Kunden); Einträge in +**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`). @@ -419,7 +454,8 @@ Kunden-API `server/routes/termine.js` unter `/api/termine` (`requireSession`, `a `adminView` mit allen Schlüsseln (Ticket-Lookups gebündelt statt N+1). - WOMS: `src/lib/funktionenView.js` (Register der Funktionen, Zustandstexte, Bestätigungstexte), `CustomerFunktionen.jsx` zeigt je Projekt **News** und **Terminplaner** (eigene Zeilen), Aktionen - „Aktivieren (ohne Zahlung)“ / „Abschalten“ je Funktion, Ticketnummer als Link `/tickets?woid=`; + „Aktivieren (ohne Zahlung)“ / „Abschalten“ je Funktion, Ticketnummer als Link `/tickets?woid=`, beim Terminplaner + zusätzlich der `siteKey` in kleiner Schrift mit Kopierknopf (für den Einbau); `teamApi.setFeature(customerId, key, …)`. `viewLog.js` um die neuen Aktionen. ### 11. Recht und Datenschutz in TP1 (Pilot) @@ -438,9 +474,11 @@ TP1 läuft nur im Pilot mit dem eigenen Testkunden; echte Gäste fremder Betrieb **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 nach 7 Tagen; 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). +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 @@ -484,10 +522,25 @@ Tagen (Zeit für den Export) Löschung aller Buchungen, Zellen, Einstellung. Kun 3. L3 WOMS: Push auf `test`. 4. 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-website` einbauen + (`siteKey` im 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. 5. L5 Nutzertest; Aufräumen der Testbuchungen nach Wunsch. +**Betrieb und Livegang – Hinweise** + +- `SESSION_SECRET` darf nie rotiert werden: Verwaltungs-, Formular- und Feed-Links hängen daran + (`token.js`, Schlüssel mit Zweckbindung; Compose fällt auf `PREVIEW_SESSION_SECRET` zurück). Eine Rotation + entwertet alle schon versandten Links, auch den Widerrufs-Link in G02. +- Der Modus `off` stoppt 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); + alternativ `TERMINE_PILOT_EMAILS=` leer lassen, statt `off` zu setzen. +- Die tägliche Zellbereinigung liest alle Zellen (Deckel 100 000, `idx_bis` ungenutzt). Vor TP2 muss sie über + `idx_bis` (`lessThan('bis', …)`) und seitenweise begrenzt werden, sonst steht sie bei vielen Betrieben still + (`list_limit_exceeded`). Gleiches Muster: `GET /export.csv` und `loescheAbgeschaltete` (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)