Terminplaner: Abschluss – Spezifikation und Rechtshinweise an den Stand des Codes angeglichen
- Spezifikation: Abweichungen aus den Abschlussreviews nachgezogen (kontakt_url, kein ETag und formMinSekunden, Token-Endpunkte unter /termin/*, Zellen bis blockEndsAt, Outbox-Fristen, Reihenfolge Buchung vor Zellen, Widerrufsfrist nach §§ 187/188/193 BGB, Abwesenheit schlägt Sonderöffnung, Kunden-API vollständig, Impressum-Adresse) sowie R53 (Wartezeit vor dem Absenden), R54 (Widerruf als Knopf, im Knopfmodus dauerhaft) und R55 (Widerruf bei jeder Verbraucher-Buchung, auch Anfragen). - Neuer Abschnitt "Betrieb und Livegang – Hinweise" in 14 (SESSION_SECRET nie rotieren, Modus off stoppt die Löschjobs, Zellbereinigung vor TP2 begrenzen, Warteschlange listet jede Minute alles Fällige). - Plan: Impressum-Link auf https://webklar.com/impressum. - Rechtshinweise: "offen 1" durch R14 erledigt, Haken für vorzeitigen Beginn nach R29/R31, Widerrufshinweis nach R55 und G02; offene Punkte zu Abo-Ende und Knopfmodus in den Datenschutz-Entwürfen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
794a63c0d7
commit
f134ed0032
5 files changed
+107
-49
No files matched your search
@@ -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“
|
||||
|
||||
|
||||
@@ -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:<featureId>:<ressourceId>'], {waitMs: 5000})` Zellen belegen (5), Buchung
|
||||
`gehalten` mit `heldUntil = now + 30 min` anlegen, Mail G01 in die Warteschlange (sofort senden).
|
||||
`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
|
||||
@@ -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 `<dialog>`; Daten werden erst beim Klick geladen). Optional
|
||||
`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`):
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user