Compare commits

...
425 Commits
Author SHA1 Message Date
ClaudeandClaude Opus 5.5 594aa9196a Datenschutz Weg B: PDF aus der freigegebenen Fassung (Stand 22.10.2026) gedruckt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:01:19 +00:00
ClaudeandClaude Sonnet 5.5 3b5e1d559a Datenschutz Weg B (E4): Forensatz sagt "sind angewiesen" statt "nehmen auf"
Der Hinweis in der E-Mail zu einem Forenbeitrag ist eine Arbeitsanweisung. Der Dienst legt den Baustein nicht in den Pult-Eintrag und
erfasst keine von Hand geschriebene Mail; der Text sagt es darum wie im Absatz davor: Die Mitarbeiter sind angewiesen, den Hinweis
aufzunehmen. Der Test verlangt den neuen Satz und verbietet den alten. Kommentar zur Forenerkennung des Dienstes nachgezogen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:41:20 +00:00
ClaudeandClaude Sonnet 5.5 c6e3c47be7 Datenschutz Weg B (E4): Beiträge aus Frage-Foren beantworten wir nicht im Forum
Abschnitt 11 sagt, was der Dienst seit E4 tut: Für Beiträge in den Frage-Foren (gutefrage.net,
forum-hilfe.de, wer-weiss-was.de, wpde.org) entwirft die KI keine Antwort; ein Mitarbeiter sieht
den Beitrag an und meldet sich höchstens per E-Mail, wenn der Beitrag ausdrücklich um Angebote
per E-Mail bittet (mit dem Hinweis am Ende), sonst nicht. Der Satz über öffentlich sichtbare
Antworten in Frage-Foren entfällt, Abschnitt 14 sagt "höchstens eine Antwort". Der Test pinnt
die Sätze, die Forenliste (gleich in Quelle und Regel) und dass nirgends steht, wir antworteten
im Forum.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:26:18 +00:00
ClaudeandClaude Sonnet 5.5 b73cd340bc Datenschutz Weg B (E2): aussortierte Anfragen spätestens am Tag nach dem Fund kürzen
Abschnitt 11 sagt jetzt, was der Dienst seit Regel C tut: Anfragen, die die KI als nicht
passend aussortiert, werden spätestens am Tag nach dem Fund gekürzt (erster täglicher
Kürzlauf nach dem Berliner Kalendertag des Fundes). 30 Tage gelten nur noch nach einer
Absage, gerechnet ab dem Lesen. Der Zweck "Mitarbeiter sehen nach, ob die KI zu Unrecht
aussortiert hat" und der Satz "Bis dahin speichern wir sie" entfallen. Die Fristenliste und
der Test legal_datenschutz_anfragen sind angepasst (Liste wörtlich, nur noch ein Eintrag mit
30 Tagen, entfallene Sätze verboten).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:24:28 +00:00
ClaudeandClaude Sonnet 5.5 53ae593860 Datenschutz Weg B: zwei Sätze lesbarer (Plattform-Hinweise, Firmenadresse im Widerspruchskasten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:47:06 +00:00
ClaudeandClaude Sonnet 5.5 fd5994c243 Datenschutz Weg B nach zweiter Prüfung: Firmen-Domain, Postfach, Zwecke, verständlichere Fristen
- Prüfwerte nennen die Firmen-Domain (bei Firmenadressen, nicht bei Freemail); der Widerspruchskasten sagt, dass die Sperre dann auch die Domain trifft
- Postfach: der Dienst lädt jede neue Mail zur Prüfung des Absenders, gespeichert werden nur die Mails der Plattformen
- Name: "Wir lesen Ihren Namen nicht gesondert aus"; Zweck der 30 Tage (Fehlurteile der KI prüfen) und der Reste (Auswertung) genannt
- Hinweis: Mitarbeiter nehmen ihn in jede Antwort auf, auch in selbst geschriebene; Frage-Foren: Antwort öffentlich sichtbar
- Widerspruchskasten nennt die Plattform-Mail im Postfach (bis 90 Tage nach Eingang)
- Verständlich: "Fund" erklärt, Fristen als Liste, robots.txt umschrieben, Absätze höchstens 130 Wörter; Hinweis im Dienst folgt (02a0b3c, Nachfolger)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:45:32 +00:00
ClaudeandClaude Sonnet 5.5 5c49f63616 Datenschutz Weg B: Alarm-Postfach bei Hetzner, Sicherungen bis zu sieben Tage, keine [OFFEN]-Markierung mehr
Abschnitt 11: Das Alarm-Postfach ist support@webklar.com bei der Hetzner Online GmbH (Abschnitt 8); der Dienst liest dort nur den Posteingang
und übernimmt nur die E-Mails der Plattformen; diese löschen wir im Postfach spätestens 90 Tage nach dem Eingang (keine "Postfach-Regel"
versprochen). Gekürzte Daten bleiben in den Datensicherungen bis zu sieben Tage, wie im übrigen Text. Der Hinweis in der Antwort heißt jetzt
"Am Ende unserer Antwort steht ein Hinweis" und nennt, dass die Mitarbeiter angewiesen sind, ihn beim Bearbeiten nicht zu entfernen; er ist
wörtlich auf Claras Hinweis im Dienst abgestimmt. Abschnitt 12: Hetzner nennt das Postfach, der eigene Listenpunkt entfällt.

Tests: jede [OFFEN]-Markierung (auch im Kommentar) und jeder Span class="offen" macht legalTexts rot; Pins für die neuen Sätze. Das Upload-Skript
bricht weiter ab, solange eine Markierung steht. pdf_pruefsumme bleibt rot bis zum Neudruck des PDF am Pushtag.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:18:18 +00:00
ClaudeandClaude Sonnet 5.5 23ab43f9fe Datenschutz Weg B nach Prüfung: Zusagen auf den Dienst begrenzt, [OFFEN]-Markierungen, Upload-Sperren
Abschnitt 11: Sperre und Kürzung ab dem Lesen unserer Antwort, nur je Beitrag und E-Mail-Adresse (neues Gesuch = neuer Beitrag);
aussortierte Anfragen 30 Tage; feste Ausschlussliste der Websuche samt robots.txt; Kriterium für die Dauer der Reste; Kopie der
Standardvertragsklauseln; Hinweis in der ersten Nachricht nennt den Widerspruch; Abschnitt 11 und 14: die KI sortiert aus, ein
Mitarbeiter entscheidet über jede Antwort. Alarm-Postfach-Anbieter und Aufbewahrung der Server-Sicherungen stehen als sichtbare
[OFFEN]-Markierung im Text (auch in Abschnitt 12).

Tests: Pins angepasst, Markierungen nur die zwei erlaubten, neuer Test für Zusagen in Reichweite des Dienstes. Upload-Skript bricht ab,
solange eine [OFFEN]-Markierung steht oder die Prüfsumme neben dem PDF nicht zur HTML passt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:57:18 +00:00
ClaudeandClaude Sonnet 5.5 b3ca84a8e5 Datenschutz-Upload: Vermerk "Abschnitt 11 Interessenten (Antworten auf öffentliche Anfragen)", Test prüft den vollen Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
ClaudeandClaude Sonnet 5.5 6a25df22fb Tests: legal_datenschutz_anfragen (Abschnitt 11 Weg B, Stand 22.10.2026), Titelliste und Stand-Pin angepasst
Drei neue Tests: tragende Wendungen von Abschnitt 11, keine Stufe-2-Reste und keine offenen Markierungen, Abschnitt 2, 12 und 14 passend zu 11.
Der alte Pin "Stand 17.10.2026" prüft nur noch "nicht vor der Reichweitenmessung". Die Titelliste kennt die neuen Überschriften von 11 und 14.
pdf_pruefsumme bleibt rot, bis das PDF neu gedruckt ist.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
ClaudeandClaude Sonnet 5.5 e554071739 Datenschutz Stand 22.10.2026: Abschnitt 11 Interessenten nur für den Anfrage-Weg (Weg B), Anker #interessenten
Abschnitt 11 beschreibt nur, was heute läuft: öffentliche Gesuche (Alarm-E-Mails der Plattformen, Websuche), KI bewertet und entwirft, ein
Mitarbeiter sendet einmal von Hand. Daten, Rechtsgrundlage, Anthropic, Fristen (90/30 Tage), Hash-Werte, Kopien und Widerspruch.
Abschnitt 2 (auch Privatpersonen aus Foren), 12 (Anthropic-Zweck, Plattformen) und 14 (keine Einzelentscheidung nach Art. 22) angepasst.
Kein Resend, Cloudflare, Overpass und kein Kaltmail-Weg: die volle Fassung bleibt auf feature/workers-datenschutz für Stufe 2.
PDF und Prüfsumme sind noch nicht neu gedruckt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:10:22 +00:00
Claude 0fbeadd0c1 Rechtliches: Skript zum Hochladen der Datenschutzerklärung als neue Version
Der Ablauf des Vorbilds (hochladen) ist jetzt eine exportierte, parametrisierte Funktion; datenschutz-hochladen.mjs ruft sie mit Karte datenschutz, PDF und HTML auf und bricht ab, wenn der Stand in der HTML nicht der Freigabe entspricht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 06676c459d0ac4f534aed7d616e2c0d40ac97b97)
2026-10-22 13:03:30 +00:00
ClaudeandClaude Opus 5.5 dd062b1f15 Analyse-Skript: bei SPA-Seitenwechseln die Verweildauer der vorigen Seite senden
Router-Links (z. B. React Router auf webklar.com) verhindern den Standard-Klick, dann griff onLinkClick nicht
und das en der vorigen Seite ging verloren. Jetzt sendet der Seitenwechsel-Timer zuerst en (nur mit
Einwilligung, einmal je Seite) und dann das pv der neuen Seite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:37:55 +00:00
ClaudeandClaude Opus 5.5 07a4e153bf Datenschutzerklärung Stand 17.10.2026: Abschnitt 4a Reichweitenmessung auf webklar.com, PDF neu
- Abschnitt 4: eigene Website-Analyse auf webklar.com statt "keine Analyse-Werkzeuge", Eintrag wk-consent.
- Abschnitt 4a: Rolle (WEBklar selbst Verantwortlicher), Daten ohne und mit Einwilligung, Rechtsgrundlagen,
  Widerruf, Fristen, Verweis auf webklar.com/datenschutz-analyse.html.
- Abschnitt 5: Kontaktformular-Anfragen gehen seit 17.10.2026 auch per Mail an das eigene Postfach.
- Abschnitt 13: Einwilligung nur für die erweiterte Reichweitenmessung.
- Archiv: Banner-Fassung eigen 2026-10-17 und Kopie der Zusatzseite von webklar.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:34:06 +00:00
ClaudeandClaude Opus 5.5 9ed295946c Website-Analyse für webklar.com: Sie-Fassung des Banners, fester Messdienst-Eintrag, Auswertung fürs WOMS
- Messskript: data-variant="eigen" zeigt eine eigene Banner-Fassung (Sie-Form, WEBklar misst selbst,
  BANNER_VERSION_EIGEN 2026-10-17); Kunden-Websites bleiben bei der Fassung 2026-10-02.
- config.analyse.eigeneWebsite: fester Schlüssel und Domains von webklar.com; ensureEigeneWebsite zieht den
  Eintrag im Messdienst täglich nach (aktiv: false schaltet ab). Kein Kunde, kein Abo, keine Tickets.
- /api/admin/analyse-eigen (jeder Mitarbeiter): Kennzahlen 7/30/90 Tage und Heatmap-Link auf webklar.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:34:06 +00:00
ClaudeandClaude Sonnet 5.5 f3f8ac3777 Kundenansicht neu: doppelte Klasse .kn-nur-lesen (ansichten.css) durch die vorhandene .kn-nur-leser (huelle.css) ersetzt
Die Stufennamen im Fortschrittsbalken der Zeitleiste (nur für Vorlesegeräte) tragen jetzt .kn-nur-leser; die zweite,
gleichwertige Regel .kn-nur-lesen in ansichten.css ist gestrichen. Tests: DOM- und Quelltext-Test (keine Spur von
.kn-nur-lesen in Stil und Modul), Stil-Test sucht die Klassenregeln auch in huelle.css.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:48:56 +00:00
ClaudeandClaude Sonnet 5.5 ace4c85e72 Kundenansicht neu (Aufträge): Zeitleiste barrierefrei, "mehr anzeigen" mit Auftragstitel im Namen und aria-controls, "Im Chat antworten" bricht nicht um
Der Knopf "mehr anzeigen"/"weniger anzeigen" bekommt einen aria-label mit dem Auftragstitel (der sichtbare Text steht vorn im
Namen, der Titel nur als Text) und aria-controls auf die Liste seines Auftrags (eindeutige Id je Liste); beides zieht beim Umschalten mit.
Der Linktext "Im Chat antworten" trägt white-space: nowrap und bleibt in einer Zeile beisammen.
Tests: DOM-Test für Name, Verknüpfung, Umschalten, fremden Titel und Ersatztitel; CSS-Test für nowrap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:48:25 +00:00
ClaudeandClaude Sonnet 5.5 14dc2ad1bf Kundenansicht neu (Aufträge): Aufträge mit Rückfrage vor den übrigen laufenden, nie hinter "Ältere Aufträge anzeigen"
sortiereAuftraege stellt unter den laufenden Aufträgen die mit Rückfrage (braucheInfo) nach vorn, danach
wie bisher die jüngste Aktivität, dann die Reihenfolge des Servers. Erledigte und Stornierte bleiben dahinter.
Tests: Reihenfolge mit Rückfragen und Gruppierung (die Rückfrage mit der ältesten Aktivität bleibt sichtbar).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:47:21 +00:00
ClaudeandClaude Sonnet 5.5 6bfcaa892c Kundenansicht neu (Meine): Kurzinfo nur auf Anfrage (GET /api/funktionen?kurzinfo=1)
Der Server berechnet kurzinfo (Terminplaner, Webshop, News, Buchungskalender, Öffnungszeiten) nur noch,
wenn die neue Ansicht ?kurzinfo=1 sendet. Das tut allein die Seite "Meine"; Etagen, Einschalt-Ablauf und
Module fragen ohne den Parameter, dann fehlt das Feld und es gibt keine Abfrage auf Termine, Bestellungen,
Beiträge und Anfragen. Mit dem Parameter bleibt die Antwort wie bisher (Text oder null je Funktion).
Nur der Wert "1" gilt; doppelte oder als Liste gesendete Parameter heißen: ohne. Alte Ansicht unverändert.

Tests: kundenansichtKurzinfo (neuer Fall ohne/mit Parameter, Abfragen und Feld), knFunktionenAnsicht
(die Seite lädt genau einmal ?kurzinfo=1, auch nach "Nochmal versuchen").

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:46:48 +00:00
ClaudeandClaude Opus 5.5 d7724afc02 Merge kn/meine-pilot (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Opus 5.5 5f4e158660 Merge kn/auftraege-pilot (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Opus 5.5 85808fc115 Merge kn/chat-fix (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:43:07 +00:00
ClaudeandClaude Sonnet 5.5 1703dabaaf Kundenansicht neu (Aufträge): Rückfrage-Link als Block, bündige Datumsspalte bei Einträgen aus dem Vorjahr
- Browserlauf: display:flex am Link zerlegte den Satz in zwei Spalten; der Link ist jetzt ein Block, die 44 px Tippfläche kommen aus dem Innenabstand.
- Trägt ein Datum das Jahr (TT.MM.JJ), bekommt die Datumsspalte des Auftrags die Breite dafür (Klasse kn-az-mit-jahr), die Texte bleiben bündig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:31:46 +00:00
ClaudeandClaude Sonnet 5.5 d62f8b805e Kundenansicht neu (Aufträge): Rückfrage-Zeile als ganze Link-Zeile, Einklappen ohne verschachtelte Regel, Balken im Pseudo-Element
- Die Zeile "Wir brauchen eine Info von dir · Im Chat antworten" ist ein Link (#chat) mit großer Tippfläche am Handy; unterstrichen ist nur der zweite Teil.
- Browserlauf: .kn-einklappbar:not(.is-open) [data-extra] verbarg die Zeilen einer aufgeklappten Zeitleiste, wenn der äußere Wrapper zu war.
  Die Wrapper haben jetzt eigene Klassen und Regeln nur für direkte Kinder (mit Regressionstest).
- Die Fläche der Balken-Abschnitte liegt im Pseudo-Element, der Lesetext darunter bleibt auf Weiß (Gate low-contrast).
- Das Modul steht in den Listen der Quelltext- und Stil-Tests der Ansicht-Module.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:28:43 +00:00
ClaudeandClaude Sonnet 5.5 ec79defb2a Plan Kundenansicht neu: Nachtrag N52 (Kurzinfo je Funktion in Meine) und Vertrag V5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:28:00 +00:00
ClaudeandClaude Sonnet 5.5 670ead3091 Kundenansicht neu Chat-Fix: Chat als Vollbild-Ansicht (Verlauf scrollt in sich, Eingabe unten, Spalte zentriert, "Chat" klein im Kopf)
Pilot-Feedback 17.10.: Der Chat wirkte durchsichtig und verrutscht.
- Handy: Inhalt steht fest zwischen Kopf und Leiste (wie das Blatt); die Eingabe sitzt höchstens 6 px über der Leiste bzw. dem Widerrufsstreifen und bei
  offener Tastatur am unteren Rand des sichtbaren Bereichs. "Chat" steht klein im Kopf (die H1 bleibt nur für Screenreader), der Kopf ist deckend mit Haarlinie,
  der Verlauf beginnt direkt darunter und scrollt in sich.
- Desktop: Spalte in Fensterhöhe, Chat höchstens 760 px breit und mittig, Verlauf füllt die Höhe, Eingabe am unteren Spaltenrand.
- Verlauf steht unten (neueste Nachricht sichtbar), kurze Verläufe unten bei der Eingabe; Blasen fremd #f8f9fb, eigen #ebeef1.
- "Senden" bleibt schwarz, auch bei leerem Feld (gesperrt im Ansichtsmodus weiter gedämpft).
- haltFokus: Druck auf Senden, Anhang oder Mikrofon nimmt dem Textfeld den Fokus nicht; sonst kommt die Leiste zurück, schiebt die feste Eingabe hoch und der
  Tipp auf "Senden" geht bei offener Tastatur verloren (im Browser nachgestellt). public/chat-composer.js und die alte Ansicht bleiben unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:17:26 +00:00
ClaudeandClaude Sonnet 5.5 9aeb9872be Kundenansicht neu (Meine, Live-Detail): Zeile einer abgeschalteten Funktion kürzer ("Im Chat wieder anfragen")
Die Detailzeile umbrach bei mehreren Websites neben der Pille auf drei Zeilen. Der Abo-Ende-Text mit dem
Löschtag bleibt unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:17:13 +00:00
ClaudeandClaude Sonnet 5.5 db62e320ff Kundenansicht neu (Meine, Live-Detail): Zeile mit Detailzeile und Pfeil, Pille nur wenn die Funktion nicht läuft
Jede Zeile in "Meine" zeigt den Funktionsnamen fett, darunter die Kurzinfo des Servers ("3 Termine heute ·
nächster 10:30") oder "Aktiv", rechts einen Pfeil. Eine Status-Pille steht nur noch bei Funktionen, die nicht
laufen (In Einrichtung, Zahlung offen, Angefragt, Abgeschaltet, Abgelehnt, Gesperrt ...); "Aktiv seit ..." entfällt.
Bei mehreren Websites steht der Websitename vorne in der Detailzeile. Die Zeilen für Funktionen, die nicht laufen,
sind kürzer ("Wir richten sie gerade ein", "Anfrage abgelehnt · Im Chat nachfragen"). Die Kurzinfo gilt nur bei
aktiver Funktion, nur als kurzer nicht leerer Text und wird nie als HTML gelesen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:10:43 +00:00
ClaudeandClaude Sonnet 5.5 53a191219e Kundenansicht neu (Meine, Live-Detail): GET /api/funktionen liefert je Projekt und Funktion die Kurzinfo
In der neuen Ansicht trägt jede Funktion unter projects[].features.<key> zusätzlich kurzinfo (Text oder null).
Nur aktive Funktionen bekommen einen Text: Terminplaner "3 Termine heute · nächster 10:30", Webshop "2 neue
Bestellungen", News-Pinnwand "4 Beiträge sichtbar", Buchungskalender "1 offene Anfrage" bzw. "Nächste Anreise
24.10.", Öffnungszeiten live der Status der Website (ohne eigene Abfrage). Berliner Zeit, je Funktion höchstens
zwei schmale Abfragen (limit, select, Index-Felder), alles gleichzeitig; jede Funktion einzeln begrenzt (1,5 s)
und fehlertolerant: Fehler oder Zeitüberschreitung ergibt null. Ohne neue Ansicht bleibt die Antwort wie sie
war und es wird nichts gelesen. Die Analyse hat keine Kurzinfo (Zahlen kommen aus dem Auswertungsdienst).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:09:25 +00:00
ClaudeandClaude Sonnet 5.5 2064bddba6 Kundenansicht neu (Aufträge): "Was wir zuletzt erledigt haben" als Zeitleiste auf der Website-Seite
Je Auftrag eine Überschrift (Titel fett, Status-Pille), ein schmaler Balken mit vier
Abschnitten (Stufennamen als Text für Screenreader), bei Rückfrage die Zeile "Wir brauchen
eine Info von dir · Im Chat antworten" und eine Zeitleiste mit Haarlinie (Datum TT.MM. und
Kurztext, drei sichtbar, "mehr anzeigen"). Fünf Aufträge offen, der Rest hinter
"Ältere Aufträge anzeigen". Die Auftragsnummer steht nicht mehr im Text (ruhiger), bleibt
als data-auftrag. Die alte baueAktivitaet und ihre toten CSS-Regeln entfallen; die
Projektdetails (baueAuftrag, baueArbeiten) bleiben unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:07:34 +00:00
ClaudeandClaude Sonnet 5.5 f231f4b887 Kundenansicht neu (Aufträge): Logik der Zeitleiste (Fortschritt, Reihenfolge, Gruppierung, Kürzen, Kurztexte)
Reine Funktionen in public/neu/auftrags-zeitleiste.js: laufende Aufträge vor erledigten
(jeweils jüngste Aktivität zuerst, Storniertes zuletzt), höchstens fünf Aufträge offen,
vier Fortschrittsabschnitte aus status.step, Zeilen der Zeitleiste neueste zuerst mit
Datum TT.MM. und Kurztext. Test zuerst (rot: Modul fehlte), dann grün.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:05:48 +00:00
ClaudeandClaude Sonnet 5.5 5cd49d6a4a Kundenansicht neu E-Fix F1 (Nachkorrektur): Widerrufsstreifen auf den Bestellseiten ausgeblendet
Auf Abo bestellen, Wechsel, Zusatzspeicher und Übernahmegutschrift fehlt der feste Widerrufsstreifen, damit Kerninfo und Knopf "Zahlungspflichtig bestellen"
auch bei Privatkunden in laufender Frist auf 375x667 auf einem Bildschirm stehen (Spec Abschnitt 4). Auf den Hauptseiten bleibt er (Spec 5.2),
"Vertrag widerrufen" bleibt im Konto und unter Verträge erreichbar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:44:20 +00:00
ClaudeandClaude Sonnet 5.5 c5f1d9545c Kundenansicht neu E-Fix F1 (Nachkorrektur): Wechsel nennt "mit 1 Paket" / "mit 2 Paketen" Zusatzspeicher (Dativ)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:40:53 +00:00
ClaudeandClaude Sonnet 5.5 0ee5c86809 Kundenansicht neu E-Fix F1 (Politur): "Fällig: erst am TT.MM.JJJJ (50,00 €)" statt doppeltem Doppelpunkt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:20:43 +00:00
ClaudeandClaude Sonnet 5.5 be34f1be1d Kundenansicht neu E-Fix F1 (Rechtsrunde): R18 nennt den gekürzten Block der Bestellseiten
Die Wortlaute des Blocks "Was du bestellst" sind seit E-Fix F1 knapp, die ausführlichen Sätze stehen unter dem Knopf. Als Entscheidung R18
(Vorschlag und Alternative, Kanzlei-Frage KQ6) in die Liste der Rechtsrunde aufgenommen; der Pin zählt jetzt R1 bis R18.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:07:07 +00:00
ClaudeandClaude Sonnet 5.5 fa79e6e10c Kundenansicht neu E-Fix F1 (Nachtrag): Wechsel mit gebuchtem Zusatzspeicher passt auf 375x667
Mit gebuchtem Zusatzspeicher (eine Block-Zeile mehr) maß der Wechsel 598 px gegen 558 nutzbare. Jetzt steckt der Zusatzspeicher im Preis
("52,00 € pro Monat mit 2 Paketen Zusatzspeicher (+10 GB)") und die gutgeschriebenen Tage in "Fällig"; die Einzelposten stehen ausführlich unter dem
Knopf. Gemessen auf 375x667 (Blockanfang bis Knopfunterkante, nutzbar 558 px): Wechsel Betrieb 448, Wechsel privat mit Häkchen 516, mit
Zusatzspeicher 536 px.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:06:36 +00:00
ClaudeandClaude Sonnet 5.5 c92ddd9363 Kundenansicht neu E-Fix W1 (Portal): Team-Endpunkt stundenkonto liefert zusätzlich active
GET /api/team/customers/:id/stundenkonto kannte nur Produkt und Konto; WOMS (stundenkontoView.js) kann bei beendetem Abo nicht "verfällt, wird
nicht berechnet" (V8, E17) statt "wird mit den nächsten Gutschriften verrechnet" zeigen, weil das Feld active fehlte. Jetzt trägt jede Antwort
active (Abo läuft: Status active, trialing oder past_due, auch mit vorgemerkter Kündigung; ohne Abo false). WOMS liest antwort.active schon,
nur ein ausdrückliches false ändert den Satz. Test in teamStundenkonto.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:50 +00:00
ClaudeandClaude Sonnet 5.5 6ff63a7a11 Kundenansicht neu E-Fix M10: hatLoeschVermerk prüft den strengen Vermerk der Löschjobs
hatLoeschVermerk erkannte gelöschte Daten an /gelöscht am/ in adminNote. Der Vermerk der Jobs ist strenger (Shopdaten bzw. Buchungsdaten
gelöscht am JJJJ-MM-TT), freier Text des Teams ("Domain gelöscht am Kundenwunsch") traf das lockere Muster: das Abo kam zurück, der Abgleich hielt
die Daten für gelöscht und setzte das Dokument auf off statt die Funktion wieder einzuschalten. Jetzt je Funktion das Muster des Jobs mit Datum.
Test in knLoeschVermerk.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:18 +00:00
ClaudeandClaude Sonnet 5.5 f6fc291dfe Kundenansicht neu E-Fix M9: featureView nennt den Löschtag auch für News, Webshop und Buchungskalender im Abo-Modell
featureView lieferte loeschungAm nur für den Terminplaner und Öffnungszeiten live. Im Abo-Modell werden aber auch News (30 Tage), Webshop und
Buchungskalender (90 Tage) nach dem Ende des Abos tatsächlich gelöscht (K39, Löschjobs); "Meine" und WOMS zeigten kein Datum, der Kommentar
"bei News verspräche das Datum eine Löschung, die nie kommt" stimmte nicht mehr. Jetzt: abo_ende bei einem Dokument des Abo-Modells (istAboModell)
von news, shop und unterkunft ergibt loeschTagNach(disabledAt, loeschFristTage(key)), derselbe Berliner Tag wie in K39. Team-Sperre, Bestand ohne
Abo-Modell und der Terminplaner bleiben unverändert. Test in knLoeschfristAnzeige.test.js. (Der Text in WOMS gehört zum WOMS-Teil W2.)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:49:18 +00:00
ClaudeandClaude Sonnet 5.5 3e32ddfe86 Kundenansicht neu E-Fix M8: Wechselangebot beim Einschalten nur, wenn der Wechsel möglich ist
aboDeckt nennt falsches_abo vor gekuendigt, beginnt_spaeter und nicht_aktiv (die Reihenfolge bleibt gepinnt). Die 409-Antwort setzte wechsel: true
allein aus grund === 'falsches_abo': ein Hosting-Abo in der Probezeit, mit offener Zahlung oder vorgemerkter Kündigung bekam das Angebot, landete auf
#wechsel bei "Ein Wechsel ist derzeit nicht möglich" und verlor den eigentlichen Grund. Jetzt prüft pruefeAboDeckung bei falsches_abo zusätzlich
wechselMoeglich (aboFunktionen.js, dieselben Bedingungen wie canSwitch und assertCanSwitch: Hosting, aktiv, nicht gekündigt, kein offener Wechsel);
ein Lesefehler dabei nimmt nur das Angebot weg. Test in funktionEinschalten.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:47:36 +00:00
ClaudeandClaude Sonnet 5.5 29ca12a409 Kundenansicht neu E-Fix M7: Bewegung reduziert ohne Rest-Fahrt der Leisten-Zeilen, #fortschritt unter dem haftenden Kopf
- Die Regel der zwei Fahrstuhl-Zeilen (#kn-zeile-haupt, #kn-zeile-etage, Spezifität 1-2-0) schlug das .kn-rw { transition: none } der Reduced-Motion-
  Regel: mit "Bewegung reduzieren" glitt die alte Zeile noch 550 ms durch die neue (beide Texte überlagert). Jetzt steht im Reduced-Motion-Block
  dieselbe Selektorliste mit transition: none.
- #fortschritt (scrollIntoView block start) ließ den Abschnitt "Was wir zuletzt erledigt haben" hinter dem haftenden Kopf beginnen: scroll-margin-top
  mit Kopfhöhe, Bannerhöhe, Safe-Area und 8 px (Desktop 16 px). Die Gutschrift-Etage scrollt im Blatt, dessen Oberkante schon unter dem Kopf liegt: dort
  ist nichts nötig.
Prüfung: Werkzeug-Ansichten kn-huelle-reduziert (jetzt auch die Zeilen bei offener Etage) und kn-p2-website-fortschritt, Pins in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:46:15 +00:00
ClaudeandClaude Sonnet 5.5 140275022d Kundenansicht neu E-Fix M6: Zahlungsknöpfe sind nach der Rückkehr von Stripe wieder bedienbar
Nach "Zahlungspflichtig bestellen" setzt die Seite den Knopf auf "Weiter zur Zahlung…" (gesperrt) und leitet zu Stripe. Bricht der Kunde per
Zurück-Wischgeste ab, stellt Safari die Seite aus dem Bfcache her: der Knopf blieb gesperrt, ein neuer Versuch ging nur nach Neuladen. Nur
ansicht-wechsel.js hatte einen pageshow-Handler. Neuer Helfer beiRueckkehr(el, fn) in ansicht-hilfen.js (pageshow mit persisted, entfernt sich,
sobald das Element nicht mehr im Dokument steht); genutzt von der Bestellseite und von "Zahlungsart aktualisieren" (Verträge).
Speicher und Gutschrift leiten nicht zu Stripe, dort ist nichts zu tun.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:45:15 +00:00
ClaudeandClaude Sonnet 5.5 8bb02f3949 Kundenansicht neu E-Fix M5: Chat hält die Eingabe über der Tastatur (revealChatComposer aus app.js)
Der Chat der neuen Ansicht übernahm nur das Ausblenden der Leiste, nicht das Nachschieben der Eingabe: auf kleinen Handys (iPhone SE, Android mit
hoher Tastatur) konnte die Eingabezeile unter der Tastatur stehen bleiben, und die letzte Nachricht verschwand beim Schrumpfen des Verlaufs.
Jetzt wie in app.js: 350 ms nach dem Fokus im Composer und bei jedem visualViewport-Resize wird die Eingabe in den sichtbaren Bereich geschoben
(composerVersatz, nur wenn verdeckt, 8 px Luft, nur am Handy), und ein ResizeObserver hält die neueste Nachricht sichtbar, wenn man unten war.
Hörer und Beobachter werden beim Neuaufbau abgebaut. Tests für die Rechnung und die Verdrahtung in knAnsichtChat.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:44:20 +00:00
ClaudeandClaude Sonnet 5.5 f8962ffa50 Kundenansicht neu E-Fix M4: Mitarbeiter-Banner verschiebt Blatt und Menü nicht mehr unter den Kopf
Der Banner stand im Fluss (relative) und schob den Kopf nach unten, das am Handy feste Blatt begann aber fest bei der Kopfhöhe: es deckte den
unteren Teil des Kopfs samt Profilbild ab (Konto und Abmelden nicht erreichbar); am Desktop blieb das Menü bei fest 36 px stehen, der Banner
ist aber 40 px hoch und beim Scrollen blieb eine Lücke. Jetzt bleibt der Banner oben stehen (sticky), huelle.js setzt --kn-banner-h auf seine
Höhe (auch bei Umbruch am Handy), und Kopf, Menü, Blatt und Konto-Popover beginnen darunter. Werkzeug-Ansicht kn-huelle-banner-blatt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:43:20 +00:00
ClaudeandClaude Sonnet 5.5 4c64edc057 Kundenansicht neu E-Fix M3: Fehlermeldungen der Bestellseiten und Formulare als Live-Region (role="alert")
abo-error, switch-error (und der Fehler der Wechsel-Vorschau), storage-error, tk-error samt Fehlerzeile der Rückfrage-Antwort, der Fehler von
"Zahlungsart aktualisieren" sowie login-error und form-error auf Login, Kündigen und Widerrufen waren einfache Absätze: Screenreader lasen
sie nicht vor, obwohl der Knopf wieder frei wurde. Jetzt role="alert", gepinnt in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:42:07 +00:00
ClaudeandClaude Sonnet 5.5 ddaf2879c4 Kundenansicht neu E-Fix M2: aboModellFuerKunde liest bei Modus off kein Kundendokument
aboModellFuerKunde las das Kundendokument auch bei KUNDENANSICHT_NEU=off, obwohl neueAnsichtFuerKunde dort sofort false liefert. Alt-Kunden
bekamen so eine zusätzliche Lesung und einen neuen Fehlerpfad (ein 503 beim Widerruf des Wechsels ließ die Beenden-Mail der Analyse
entfallen, die Team-Aktivierung und GET /api/admin/analyse/:id antworteten mit 500). Jetzt sofort false im Modus off; Pilot und on
lesen wie bisher (kein Dokument: false). Test in analyseAboModell.test.js (eingeplanter 503 wird im Modus off nicht verbraucht).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:41:28 +00:00
ClaudeandClaude Sonnet 5.5 306c53871d Kundenansicht neu E-Fix M1: Widerrufsstreifen und Pille nach Kauf, Wechsel und Speicher-Plus neu laden
ladeFristen lief nur einmal beim Start; eine neue 14-Tage-Frist nach einem Wechsel oder Speicher-Plus erschien erst nach Neuladen, ein
abgelaufener Streifen blieb in einem langen PWA-Tab stehen. Die Hülle lädt die Fristen jetzt bei kn:zaehler-neu (vertraege, alle) und beim
Zurückkehren in den Tab neu (ladeFristenNeu: ein Fehler lässt den Stand stehen, die jüngste Antwort gilt); das Speicher-Plus meldet
kn:zaehler-neu vertraege nach Erfolg, der Wechsel tat es schon.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:40:29 +00:00
ClaudeandClaude Sonnet 5.5 4cac03a2bd Kundenansicht neu E-Fix F6: Pin auf build-legal-pdfs.sh kennt die Modi abo6 und funktionen6
P8 hat dem Skript die Fassungsfunktion funktionen6 und die Entwurfsmodi abo6 und funktionen6 ergänzt; der Pin zählte noch fünf Node-Aufrufe.
Jetzt sechs Aufrufe (60 s, 256 MB, je Fassungsfunktion einer), die zwei Entwurfsmodi sind gepinnt und `all` druckt sie weiterhin nicht mit;
bestehende Modi, Chrome-Grenze (120 s), Usage-Zeile und das Verbot paralleler Läufe bleiben unverändert geprüft.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:39:17 +00:00
ClaudeandClaude Sonnet 5.5 72b5ac5265 Kundenansicht neu E-Fix F5: Fassung 6 ohne PDF behauptet keinen Anhang, das Team bekommt den Hinweis
Die PDFs der Fassung 6 entstehen erst nach der Freigabe. Bis dahin gingen K01, K12 und K13 ohne Anhang raus, sagten aber "im Anhang" bzw.
"beigefügt", und das Team erfuhr es nicht (nur K38/T24 hatten dafür den Block anhang_fehlt).

- Neuer Helfer vertragsAnhang.js: lädt das PDF der zugestimmten Fassung und meldet fehlt nur bei Fassung 6 ohne lesbare Datei.
- K01, K12, K13: Block anhang (Anhangsaussagen) und ohne_anhang (Satz, dass die Unterlagen gesondert kommen).
- T02, T12, T13: Block anhang_fehlt "Unterlagen von Hand nachsenden" (T12 auch beim späteren Wirksamwerden, T13 auch ohne Aufgabe).
- Alte Fassungen unverändert: Hash-Pins auf die Wiedergabe der Vorlagen vor der Änderung, Anhang und Texte wie bisher.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:38:54 +00:00
ClaudeandClaude Sonnet 5.5 9c04f806eb Kundenansicht neu E-Fix F4: Team-Wiedereinschalten ohne gesendete Abrechnung nimmt die gespeicherte (Aufbauphase)
WOMS fragt nach einer Team-Sperre die Abrechnung nicht mehr ab und sendet keine; der Server setzte dann pauschal 'abo', pruefeAboDeckung warf
409 abo_fehlt, und eine in der Aufbauphase ('projekt', noch kein Abo) eingebaute Funktion liess sich nicht wieder einschalten.
Jetzt gilt bei disabled/team die am Dokument gespeicherte einrichtungAbrechnung, sonst wie bisher 'abo'; eine ausdrücklich gesendete
Abrechnung geht vor. Test in funktionenAdminAbo.test.js (Aufbauphase: ein, aus, ohne Angabe wieder ein = 200 mit warnung; mit abo hart).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:32:28 +00:00
ClaudeandClaude Sonnet 5.5 3962074af6 Kundenansicht neu E-Fix F3b: Safe-Area links und rechts in Hülle, öffentlichen Seiten und Chat-Vollbild
dashboard.html und die öffentlichen Seiten setzen viewport-fit=cover, aber kein Rand nutzte env(safe-area-inset-left|right): Im Querformat
am iPhone rutschten die ersten Zeichen unter Notch und Insel. Kopf, Hauptseite, Blatt, Streifen, Konto-Sheet, Fehlerzeile, Leiste, Banner und die
Sperre sowie Login, Rechtsseiten, Auftrag, Offline-Seite und die Kopfzeile des Chat-Vollbilds bauen ihre Ränder jetzt mit
max(Wert, env(safe-area-inset-left|right)). Am Desktop und ohne Notch ändert sich nichts (Inset 0). Pins in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:31:33 +00:00
ClaudeandClaude Sonnet 5.5 8e349f6eca Kundenansicht neu E-Fix F3c: 44-px-Tippfläche für Kündigen, Widerrufen, Vertragsbedingungen und Zahlen am Handy
"Verträge hier kündigen", "Vertrag widerrufen", die Vertragsbedingungen und das "Zahlen" der Zeile "Rechnung offen" waren 14-px-Textlinks
von etwa 16 px Höhe. Am Handy jetzt inline-flex mit 44 px Mindesthöhe; das "Zahlen" steht rechts in der Zeile (der Trenner davor ist ein
eigenes Element und am Handy ausgeblendet, textContent unverändert). Pin in test/knEfixCss.test.js, Tippflächen-Prüfung in den
Werkzeug-Ansichten kn-p2-abo-privat und kn-p2-website-rechnung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:30:45 +00:00
ClaudeandClaude Sonnet 5.5 92d7d247e9 Kundenansicht neu E-Fix F3a: Fokusring der Shell-Reiter liegt innen und ist sichtbar
.kn [tabindex]:focus-visible (module.css, 0-3-0, später geladen) und .kn button:focus-visible setzten outline-offset +2px und schlugen
.kn-t:focus-visible und .kn-reiter-t:focus-visible (0-2-0). Der Ring lag außerhalb und wurde von Balken und Reiterzeile abgeschnitten.
Jetzt .kn .kn-t[role="tab"] und .kn .kn-reiter-t[role="tab"] (0-4-0) mit Versatz -2px; der aktive Reiter auf dem schwarzen Gleiter
bekommt vom Gleiter einen hellen Innenring (Rückfall ohne Overlay: weißer Ring am Reiter). Pin in test/knEfixCss.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:30:06 +00:00
ClaudeandClaude Sonnet 5.5 112475e873 Kundenansicht neu E-Fix F2: Sperre der Mitarbeiter-Kundenansicht als echte Sperre in der neuen Seite
public/kundenansicht.js hängt bei 409 view_replaced und 401 view_expired .kb-view-overlay an den Body; die Regeln dafür standen nur in der
alten kundenbereich.css. In der neuen Seite entstand ein unformatierter Absatz mit nacktem Knopf unter der Leiste. Jetzt: fixiert, deckend weiß,
über Konto und Leiste, Text und Knopf "Tab schließen" (44 px, schwarz). Pin in test/knEfixCss.test.js, Ansicht kn-huelle-sperre im Werkzeug.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:27:07 +00:00
ClaudeandClaude Sonnet 5.5 d4050f870a Kundenansicht neu E-Fix F1: Kerninfo und Knopf der Bestellseiten passen in den nutzbaren Bereich (390x844, 375x667)
Die Messung im Werkzeug nahm innerHeight; nutzbar ist am Handy nur der Bereich zwischen Kopf und fester Leiste (735 px bei 844, 558 px bei 667).
Gemessen vor der Korrektur (Blockanfang bis Knopfunterkante, 375x667): Abo bestellen privat 799 px, Wechsel privat 888 px, Zusatzspeicher 567 px.

- Block knapp: Leistung als eine Zeile mit Link "Leistungen ansehen" auf die Leistungsbeschreibung, Preis, Fällig, Beginn, Laufzeit und Zahlung in je ein bis zwei Zeilen.
- Die ausführlichen Sätze stehen unter dem Knopf und der Kundenart-Zeile (baueEinzelheiten), ohne Klappe.
- Handy: Beschriftung läuft in der Zeile mit, kleinere Überschrift, knappere Abstände.
- Die Wortlaute des Blocks sind gekürzt und gehören in die Rechtsrunde.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:25:48 +00:00
ClaudeandClaude Sonnet 5.5 779320c080 Kundenansicht neu P7: Block der Bestellseiten kompakter, damit Kerninfo und Knopf auf ein Handy-Fenster passen
Die Wechselseite brauchte bei 390 x 844 bis zu 952 px vom Anfang des Blocks bis zum Knopf. Auf dem Handy stehen die Beschriftungen jetzt klein über dem Wert (weniger Abstand), und die Zeile Wartungs-Abo wiederholt die Leistungen nicht mehr, die schon in der Zeile Leistung stehen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:33:59 +00:00
ClaudeandClaude Sonnet 5.5 036bf4b0b9 Kundenansicht neu P7: Kündigen und Widerrufen in du (Fassung 6, C14)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:24:31 +00:00
ClaudeandClaude Sonnet 5.5 9166e023c5 Kundenansicht neu P7: bekannte Fehlertexte der Dienste in der neuen Ansicht in du
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:22:24 +00:00
ClaudeandClaude Sonnet 5.5 d17520c4f5 Kundenansicht neu P7: Gutschrift-Antrag schlank, Bedingungen als eigene Ansicht statt Klappe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:20:57 +00:00
ClaudeandClaude Sonnet 5.5 9b00771d11 Kundenansicht neu P7: E-Mail-Speicher schlank, Plus mit Linkzeile, Warnung als eine Zeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:15:36 +00:00
ClaudeandClaude Sonnet 5.5 e7ac625f26 Kundenansicht neu P7: Wechselseite schlank, Sofortbeginn-Häkchen nur privat
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:13:49 +00:00
ClaudeandClaude Sonnet 5.5 c6ecb5242c Kundenansicht neu P7: Bestellseite schlank (Block, Linkzeile, Häkchen nur privat)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:11:44 +00:00
ClaudeandClaude Sonnet 5.5 fcf357c0b7 Kundenansicht neu P7: gemeinsame Bausteine der Bestellseiten (Block, Linkzeile, Kundenart-Zeile)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:09:04 +00:00
ClaudeandClaude Sonnet 5.5 841bc76bbb Kundenansicht neu P7: Gutschrift-Antrag in Fassung 6 mit Nachweis, Pin gegen Kundenart aus dem Body
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:07:05 +00:00
ClaudeandClaude Sonnet 5.5 da2fe83d47 Kundenansicht neu P7: Zusatzspeicher-Plus in Fassung 6 mit Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:06:04 +00:00
ClaudeandClaude Sonnet 5.5 528ca0875e Kundenansicht neu P7: K01 und K12 behaupten für Betriebe kein Sofortbeginn-Verlangen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:05:06 +00:00
ClaudeandClaude Sonnet 5.5 4e0e42f633 Kundenansicht neu P7: Wechsel Fassung 6, Betrieb ohne Sofortbeginn-Zeitstempel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:03:46 +00:00
ClaudeandClaude Sonnet 5.5 99e1987bc4 Kundenansicht neu P7: Bestellung Fassung 6 mit Kundenart vom Server und Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 20:02:04 +00:00
ClaudeandClaude Sonnet 5.5 bf1e968a83 Kundenansicht neu P7: GET /api/abo liefert Fassung 6 und Wortlaut nur der neuen Ansicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:59:43 +00:00
ClaudeandClaude Sonnet 5.5 4a3b0030d2 Kundenansicht neu P7: Fassungsweiche und Vertragsnachweis (V12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:59:06 +00:00
ClaudeandClaude Sonnet 5.5 71b2543e95 Kundenansicht neu P7: Wortlaut-Entwurf und Nachweistexte der Bestellseiten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:58:05 +00:00
ClaudeandClaude Sonnet 5.5 bfe3895a50 Kundenansicht neu P7: Einschalten zeigt Linkzeile, Häkchen und Unterlagen der Fassung 6 aus P8
Verdrahtet P5 Task 15 und P7 Task 0 (N33): consentAbo liest Linkzeile und Hinweis aus WORTLAUT_V6, unterlagenFuer die fünf Funktionen aus FUNKTIONEN_UNTERLAGEN_V6 (Analyse bleibt bei den Alt-Unterlagen bis zur Freigabe), config.aboFunktionen.texte.hinweis entfällt, pdfAnhaenge liest PDFs der Fassung 6, sichereAdresse lässt /neu/rechtliches/ zu. Der Analyse-Kundenweg setzt wie der Team-Weg den Abo-Marker (unlockedBy und einrichtungAbrechnung abo).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:56:40 +00:00
ClaudeandClaude Sonnet 5.5 447129d4e2 zeitenBestellen: Pin auf die Listen in app.js an den Buchungskalender angepasst
VIEW_HASHES, APP_PARENT und FEATURE_LABELS tragen seit dem Buchungskalender
einen Eintrag zwischen Webshop und Zeiten. Die Regexe folgen den echten Listen;
app.js bleibt unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:30:54 +00:00
ClaudeandClaude Sonnet 5.5 524ac64d20 Kundenansicht der Mitarbeiter: Einschalten gesperrt statt 403-Text nach dem Tipp
Die Hülle reicht viewAs der Sitzung an die Einschalt-Option der Module durch
(News, Terminplaner, Webshop, Buchungskalender, Zeiten), auch wenn ein Modul es
nicht nennt. In Entdecken ist der Einschalten-Knopf gesperrt, daneben steht
"In der Kundenansicht nicht möglich." Meine bleibt bei gesperrtem Knopf auf der
Bestätigungsseite (jetzt gepinnt).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:30:26 +00:00
ClaudeandClaude Sonnet 5.5 0df96611dc module.css: Korrekturen aus der Sichtprüfung, Bild der Analyse in neuer Optik
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:23:05 +00:00
ClaudeandClaude Sonnet 5.5 f76749133f module.css: Analyse hell (an-, hs-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:58:57 +00:00
ClaudeandClaude Sonnet 5.5 a9b50a5542 module.css: Öffnungszeiten und News (zt-, fn-, nw-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:57:29 +00:00
ClaudeandClaude Sonnet 5.5 b20ac71750 module.css: Webshop und Buchungskalender (sh-, bk-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:56:07 +00:00
ClaudeandClaude Sonnet 5.5 cbd36c0ae6 module.css: Terminplaner und gemeinsame Bestellbausteine (tm-)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:54:49 +00:00
ClaudeandClaude Sonnet 5.5 a6a656f18a module.css: Gerüst, Prüftest und gemeinsame Bausteine unter .kn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:53:14 +00:00
ClaudeandClaude Sonnet 5.5 5534655671 Analyse: im Abo-Modell Linkzeile und Zielgruppen-Häkchen, Hosting berechtigt, Reiter in der Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:50:49 +00:00
ClaudeandClaude Sonnet 5.5 c37faabcd3 Öffnungszeiten live: im Abo-Modell Einschalten und kein Behalten-Weg
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:49:08 +00:00
ClaudeandClaude Sonnet 5.5 5f3d8f3e96 Buchungskalender: im Abo-Modell Anfrage ohne Preis und Einschalten nach der Zusage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:46:42 +00:00
ClaudeandClaude Sonnet 5.5 28da4180bd Webshop: im Abo-Modell Anfrage ohne Unternehmer-Haken, Privat-Hinweis, Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:45:17 +00:00
ClaudeandClaude Sonnet 5.5 1cabab2826 News: im Abo-Modell Einschalten statt Bestellen, Projektwahl über dataset
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:40:59 +00:00
ClaudeandClaude Sonnet 5.5 b6f70a502a Terminplaner: im Abo-Modell Einschalten statt Bestellen, ohne Häkchen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:39:54 +00:00
ClaudeandClaude Sonnet 5.5 364ded1662 Funktionsmodule: gemeinsame Einschalt-Seite für das Abo-Modell
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:38:51 +00:00
ClaudeandClaude Sonnet 5.5 a50c5ef6c9 Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:37:12 +00:00
ClaudeandClaude Sonnet 5.5 b91ba1d5cb Funktionen neu: Korrekturen aus den Gates (Hover des Knopfes bleibt #111, Ticket auch am Feature)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:31:08 +00:00
ClaudeandClaude Sonnet 5.5 c87c87e76b Funktionen neu: Bilder für Entdecken aus Testdaten (Einbettungen auf Beispiel-Website)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:25:45 +00:00
ClaudeandClaude Sonnet 5.5 9ca7900213 Funktionen neu: Aussehen (weiß, Pillen, 44-px-Zeilen) mit Quelltext-Prüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:14:08 +00:00
ClaudeandClaude Sonnet 5.5 36890c6734 Funktionen neu: Bestätigung als Overlay der Zurück-Geste, Erfolgsmeldung, Rückkehr nach dem Wechsel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:11:36 +00:00
ClaudeandClaude Sonnet 5.5 f30e72ebbb Funktionen neu: Ansicht mit Meine und Entdecken, Website-Auswahl, Hinweise je Abo-Lage, Rechnungszeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:10:02 +00:00
ClaudeandClaude Sonnet 5.5 57112cc51f Funktionen neu: Einschalten senden, 409-Folgen, Wechsel mit gemerkter Funktion, zeigeEinschalten für die Module
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:07:30 +00:00
ClaudeandClaude Sonnet 5.5 906c9ec1f0 Funktionen neu: Bestätigungsseite zum Einschalten (Linkzeile, Häkchen nur für privat in der Frist)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:05:18 +00:00
ClaudeandClaude Sonnet 5.5 c72c9d9f5a Funktionen neu: Modell für Meine und Entdecken (mehrere Websites, Anfragestatus)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:03:22 +00:00
ClaudeandClaude Sonnet 5.5 437947cdf6 Funktionen neu: Einschalt-Logik (Vorprüfung, Fristen, 409-Codes) und Testdaten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:01:27 +00:00
ClaudeandClaude Opus 5.5 1cd16ff293 Merge feature/kundenansicht-neu (P5, P6, P8, P6b) in kn/ui vor P3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:55:13 +00:00
ClaudeandClaude Sonnet 5.5 01c4f22a84 Kundenansicht neu P2: Test der Dashboard-Weiche läuft den Fall ohne Datei gegen einen leeren Nachbau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:53:36 +00:00
ClaudeandClaude Sonnet 5.5 04a81a1d46 Kundenansicht neu P2: Feinschliff nach der Sichtprüfung (Kontrast der Menüzahl, Links, Anmelde-Hinweise)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:43:21 +00:00
ClaudeandClaude Sonnet 5.5 eb7c44ac11 Kundenansicht neu P2: Adressen auf derselben Herkunft zulassen (Vorschau, Zahlungsseiten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:24:27 +00:00
ClaudeandClaude Sonnet 5.5 2ac7d3f64c Kundenansicht neu P2: Aussehen der Ansichten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:21:31 +00:00
ClaudeandClaude Sonnet 5.5 3c2450dd2e Kundenansicht neu P2: öffentliche Seiten und Auftragsbestätigung im neuen Stil, Text unverändert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:17:45 +00:00
ClaudeandClaude Sonnet 5.5 ade7e872d9 Kundenansicht neu P2: Verträge mit Reitern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:14:28 +00:00
ClaudeandClaude Sonnet 5.5 861b74f70e Kundenansicht neu P2: Passwort ändern als Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:12:32 +00:00
ClaudeandClaude Sonnet 5.5 d99c29b6c6 Kundenansicht neu P2: Übernahmegutschrift im Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:11:15 +00:00
ClaudeandClaude Sonnet 5.5 1b66f6aa81 Kundenansicht neu P2: Wechsel im Wortlaut der Fassung 5 mit gemerkter Funktion
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:09:08 +00:00
ClaudeandClaude Sonnet 5.5 27e6c8fe4f Kundenansicht neu P2: Bestellseite im Wortlaut der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:07:22 +00:00
ClaudeandClaude Sonnet 5.5 4473a746ee Kundenansicht P6b: Team-Weg der Analyse im Abo-Modell (WOMS)
POST/GET /api/admin/analyse/:projectId kennt jetzt das Abo-Modell:
- Gilt aboModellFuerKunde, prüft das Einschalten das Abo über aboDeckt
  (Hosting und Wartung decken die Analyse); fehlt es, 409 mit code
  abo_fehlt, grund, wechsel (und ab) wie bei der Team-Aktivierung der
  anderen Funktionen, bevor etwas geschrieben wird. Lesefehler: 503.
- Das neu eingeschaltete Dokument trägt den Abo-Marker (unlockedBy und
  einrichtungAbrechnung 'abo', istAboModell); activateAnalyse nimmt dazu
  den Parameter abrechnung, setzt ihn im selben Schritt wie den Zustand
  und stellt ihn bei der Rücknahme wieder her.
- GET: Eignung und Text folgen dem Abo-Modell (Hosting-oder-Wartungs-Text).
- Alle anderen Kunden und das Ausschalten laufen unverändert.

Tests: test/adminAnalyseAboModell.test.js (9 Fälle).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:06:33 +00:00
ClaudeandClaude Sonnet 5.5 f9c4fbe9bb Kundenansicht neu P2: E-Mail-Speicher im Wortlaut der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:05:45 +00:00
ClaudeandClaude Sonnet 5.5 8a23652e07 Kundenansicht neu P2: Stundenkonto mit Vorschuss-Anzeige
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:02:59 +00:00
ClaudeandClaude Sonnet 5.5 989e7be640 Kundenansicht neu P2: Reiter Abo und Abo-Auswahl
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:02:10 +00:00
ClaudeandClaude Sonnet 5.5 cf391eb117 Kundenansicht neu P2: Reiter Rechnungen mit Aufträgen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:00:28 +00:00
ClaudeandClaude Sonnet 5.5 8c8d29657d Kundenansicht neu P2: Ansicht Chat mit dem Composer und lilafreier CSS-Kopie
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:59:28 +00:00
ClaudeandClaude Opus 5.5 72c5fa9fea Merge kn/recht (P8 Rechtsrunde Fassung 6) in feature/kundenansicht-neu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:59:27 +00:00
ClaudeandClaude Sonnet 5.5 9a3ce7c8bf Kundenansicht P6: Zusage-Mails K42 und K43 für Webshop- und Buchungskalender-Anfrage ohne Preis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:56:52 +00:00
ClaudeandClaude Sonnet 5.5 a3831040ba Kundenansicht neu P2: Ansicht Website mit Projektdetails als Etage
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:55:16 +00:00
ClaudeandClaude Sonnet 5.5 bb1efa5c9e Kundenansicht P6: Ticket-Texte zum Stundenkonto je Abrechnung (Abo, Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:54:52 +00:00
ClaudeandClaude Sonnet 5.5 e036d24ae5 Kundenansicht neu P2: Bausteine für Projektdetails und Aktivität
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:53:22 +00:00
ClaudeandClaude Sonnet 5.5 df53cd039b Kundenansicht P6: Widerruf des Wechsels beendet Wartungs-Funktionen, die Analyse bleibt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:52:18 +00:00
ClaudeandClaude Sonnet 5.5 166188c069 Kundenansicht neu P2: Installationshinweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:51:44 +00:00
ClaudeandClaude Sonnet 5.5 3c44aaea77 Kundenansicht neu P2: Zeile Rechnung offen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:51:10 +00:00
ClaudeandClaude Sonnet 5.5 b77c3291c5 Kundenansicht neu P2: Wortlaut-Orakel für die Texte der Fassung 5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:50:49 +00:00
ClaudeandClaude Sonnet 5.5 d5a7312896 Kundenansicht P6: Wechsel merkt sich die gewünschte Funktion (details.funktion, wechselFunktion)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:50:24 +00:00
ClaudeandClaude Sonnet 5.5 91e5bca1c5 Kundenansicht neu P2: Hilfen, Testdaten und Quelltext-Regeln
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:49:18 +00:00
ClaudeandClaude Sonnet 5.5 db47f13510 Kundenansicht P6: Abo-Ende-Abgleich im Tagesjob
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:47:12 +00:00
ClaudeandClaude Sonnet 5.5 7a03f073bc Kundenansicht P6: alte Abgleiche lassen Abo-Modell-Dokumente dem neuen Abgleich, Zeiten ohne Behalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:46:15 +00:00
ClaudeandClaude Sonnet 5.5 d8c952ac2a Kundenansicht P6: News-Löschschritt und Zurücksetzen nach der Löschung im Abo-Ende-Abgleich
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:44:08 +00:00
ClaudeandClaude Sonnet 5.5 385844192d Kundenansicht neu P1: Vollständigkeitstest der Dateiübersicht
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:43:58 +00:00
ClaudeandClaude Sonnet 5.5 5297a05125 Kundenansicht neu P1: Korrekturen aus der Sichtprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:43:19 +00:00
ClaudeandClaude Sonnet 5.5 92ac61dffd Kundenansicht P6: Abo-Ende-Abgleich für News, Terminplaner, Webshop und Buchungskalender
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:42:06 +00:00
ClaudeandClaude Sonnet 5.5 de8b0c0fca Kundenansicht P6: Team-Aktivierung mit Abo-Prüfung und Abrechnung (V10), Projektstart ohne Abo nur mit Warnung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:39:07 +00:00
ClaudeandClaude Sonnet 5.5 408f7d0cc2 Kundenansicht P6: K38 an den Kunden und T24 ans Team beim Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:34:22 +00:00
ClaudeandClaude Sonnet 5.5 cdc8f5c424 Kundenansicht P6: K02 und K03 nennen das Ende der Abo-Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:31:06 +00:00
ClaudeandClaude Sonnet 5.5 9489a70f0c Kundenansicht P6: Mail-Builder für K38, K39, T24 und die Zeiten-Varianten K40, K41
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:29:34 +00:00
ClaudeandClaude Sonnet 5.5 664043e8e9 Kundenansicht P6: Mailvorlagen K38 bis K41 und T24 für das Abo-Modell
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:28:15 +00:00
ClaudeandClaude Sonnet 5.5 142aa6e9d6 Kundenansicht neu P1: weiße Seite, Manifest und leere Ansichten für P2 und P3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:26:23 +00:00
ClaudeandClaude Sonnet 5.5 3d4d49de82 Kundenansicht P6: Team-Endpunkt Stundenkonto mit Vorschuss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:24:31 +00:00
ClaudeandClaude Sonnet 5.5 7595844170 Kundenansicht neu P1: Hülle mit Start über /api/auth/me und Modul-Lader
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:24:16 +00:00
ClaudeandClaude Sonnet 5.5 f016d7ae6c Kundenansicht P6: Vorschuss im Stundenkonto (Einrichtung statt Mehraufwand)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:23:39 +00:00
ClaudeandClaude Sonnet 5.5 710c57cf87 Kundenansicht neu P1: Fahrstuhl-Zeile, Desktop-Menü und Tastaturzustand
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:14:55 +00:00
ClaudeandClaude Sonnet 5.5 57d5db2344 Kundenansicht P5: Analyse im Abo-Modell (Hosting, Zielgruppe erzwungen, Nachweis, Tagesprüfung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:14:35 +00:00
ClaudeandClaude Sonnet 5.5 0432b7e01d Rechtstexte: Hochladen-Skript für WOMS Rechtliches (ein Dokument je Aufruf, nur mit Freigabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:13:20 +00:00
ClaudeandClaude Sonnet 5.5 23252df359 Kundenansicht P5: Shop-Anfrage im Abo-Modell (Unternehmer aus Kundenart)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:11:13 +00:00
ClaudeandClaude Sonnet 5.5 99d7e2bdca Rechtstexte: PDF-Bau für Fassung 6 (Modi abo6 und funktionen6, nach Freigabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:10:31 +00:00
ClaudeandClaude Sonnet 5.5 b6f2df0d4a Kundenansicht P5: Katalog, Abo-Stand und Anfragestatus in GET /api/funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:51 +00:00
ClaudeandClaude Sonnet 5.5 4953c8fd0f Kundenansicht neu P1: Konto mit Avatar, Sheet und Popover
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:40 +00:00
ClaudeandClaude Sonnet 5.5 b4b18792c3 Rechtsrunde Fassung 6: Entscheidungsliste, Kanzlei-Fragen, Datenschutz-Prüfpunkte, Ablauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:09:34 +00:00
ClaudeandClaude Sonnet 5.5 e30318a470 Kundenansicht P5: POST /api/funktionen/:key/einschalten (nur neue Ansicht)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:07:30 +00:00
ClaudeandClaude Sonnet 5.5 3241aedc3c Kundenansicht neu P1: Widerrufsstreifen mit GET /api/fristen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:06:51 +00:00
ClaudeandClaude Sonnet 5.5 a8b8838d9e AVV Fassung 6: Terminplaner, Webshop, Buchungskalender mit Klick auf die Linkzeile (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:06:02 +00:00
ClaudeandClaude Sonnet 5.5 e144a9611d Kundenansicht neu P1: Zähler für Chat und Verträge nach V3
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:05:51 +00:00
ClaudeandClaude Sonnet 5.5 f25b7608b4 Website-Analyse Fassung 6: Hosting- und Wartungs-Abo, Leistungsbeschreibung und AVV (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:04:58 +00:00
ClaudeandClaude Sonnet 5.5 748226d631 Kundenansicht neu P1: Etage 2 mit Zurück, Reiterzeile und Brotkrume
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:04:46 +00:00
ClaudeandClaude Sonnet 5.5 9e7db59326 Leistungsbeschreibungen Fassung 6: Webshop und Buchungskalender (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:03:46 +00:00
ClaudeandClaude Sonnet 5.5 f5b43c55a9 Kundenansicht P5: generisches Einschalten im Abo (einschalten, Heilung, Ticket-Marker)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:03:17 +00:00
ClaudeandClaude Sonnet 5.5 d112fe2326 Leistungsbeschreibungen Fassung 6: Terminplaner, News-Pinnwand, Öffnungszeiten live (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:55 +00:00
ClaudeandClaude Sonnet 5.5 a51643c4a5 Kundenansicht neu P1: gemeinsame Reiter mit Balken, Overlay-Rückfall und Zählern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:39 +00:00
ClaudeandClaude Sonnet 5.5 1d149f50b1 Kundenansicht P5: Einrichtungs-Ticket mit Abrechnung, unlockedBy für alle Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:00:00 +00:00
ClaudeandClaude Sonnet 5.5 819725a3c1 Kundenansicht neu P1: Router mit Anker-Kette, Overlay und Verlaufsstempel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:56:10 +00:00
ClaudeandClaude Sonnet 5.5 b000dba698 Kundenansicht P5: Kundenart im Zustimmungsnachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:56:00 +00:00
ClaudeandClaude Sonnet 5.5 33b7a4ce0f Rechtstexte: Generator-Satz funktionen6 im neuen Stil
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:55:41 +00:00
ClaudeandClaude Sonnet 5.5 4bf1b67e67 Kundenansicht P5: Katalogfelder, consentAbo mit Hinweis und Unterlagen-Quelle im Funktionen-Register
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:55:19 +00:00
ClaudeandClaude Sonnet 5.5 90b7f10f1a Abo-Bedingungen Fassung 6: Seite im neuen Stil (Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:53:49 +00:00
ClaudeandClaude Sonnet 5.5 8e0d2b2736 Kundenansicht P5: gesammelte Widerrufsfristen (sammleFristen, GET /api/fristen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:53:29 +00:00
ClaudeandClaude Sonnet 5.5 9b177a15a3 Kundenansicht neu P1: Routentabelle mit allen alten Hashes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:43 +00:00
ClaudeandClaude Sonnet 5.5 882438df38 Kundenansicht P5: Abo-Zuordnung der Funktionen (aboProdukt, aboDeckt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:38 +00:00
ClaudeandClaude Sonnet 5.5 7f4e84e0b1 Abo-Bedingungen Fassung 6: Entwurf der Änderungen im Wortlaut
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:51:35 +00:00
ClaudeandClaude Sonnet 5.5 6f0808647f Kundenansicht neu P1: h(), api() mit ApiFehler und Quelltext-Regel
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:50:12 +00:00
ClaudeandClaude Sonnet 5.5 5c21023e28 Kundenansicht P5: Kundenart beim Anlegen Pflicht (ab Schalter pilot), beim Ändern optional
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:49:25 +00:00
ClaudeandClaude Sonnet 5.5 46edb7c418 Kundenansicht neu P1: Tokens und Basis-CSS im weißen Stil
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:54 +00:00
ClaudeandClaude Sonnet 5.5 214d698d6f Abo-Fassung 6: Wortlaute für Linkzeile und Häkchen, Unterlagen-Tabelle der Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:24 +00:00
ClaudeandClaude Sonnet 5.5 8d3eb73db7 Kundenansicht P5: Kundenart-Helfer und Felder in /api/auth/me
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:48:07 +00:00
ClaudeandClaude Sonnet 5.5 2a8db582cb Abo-Bedingungen Fassung 5: Archiv vorbereitet (erst mit P10 aktiv)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:37 +00:00
ClaudeandClaude Sonnet 5.5 65b1389b55 Abo-Fassung 6: Fassungstabelle, aktuelleAboFassung, termsVersionNeu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:05 +00:00
ClaudeandClaude Sonnet 5.5 c5b1333953 Kundenansicht P5: Setup-Skript für Kundenart und Einrichtungs-Marker
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 15:47:04 +00:00
ClaudeandClaude Sonnet 5.5 c2b7073d99 Kundenansicht neu: Einstieg für Sichtprüfungen über das Werkzeug
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 14:24:06 +00:00
ClaudeandClaude Sonnet 5.5 c6a9a2c4a6 Kundenansicht neu: /api/auth/me meldet ansicht neu oder alt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 13:01:40 +00:00
ClaudeandClaude Sonnet 5.5 f2c9eb107a Kundenansicht neu: Dashboard-Weiche vor den statischen Dateien, Platzhalter-Seite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 12:59:48 +00:00
ClaudeandClaude Sonnet 5.5 7c4a4a567d Kundenansicht neu: Pilot-Schalter (off/pilot/on) mit Randfall-Tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 12:56:13 +00:00
ClaudeandClaude Opus 5.5 f573d49f0a Plan: Kundenansicht neu – elf Paket-Pläne P0–P10 und Nachträge der Planrunde
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 12:21:04 +00:00
ClaudeandClaude Opus 5.5 08dd766e9c Plan: Kundenansicht neu – Übersicht und Verträge V1–V13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:48:32 +00:00
ClaudeandClaude Opus 5.5 d3d28492c0 Spec: Kundenansicht neu (weiß, Fahrstuhl-Zeile, Funktionen im Abo, schlanke Bestätigungen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 08:52:02 +00:00
knsoandClaude Opus 5.5 d33a483fd8 Buchungskalender: Freigeben zeigt bei 409 die richtige Meldung
Bisher stand bei jedem 409 „Der Zeitraum ist inzwischen belegt.“, auch wenn der Rechnungsauftrag fehlte (zustimmung_fehlt) oder sich der Stand geändert hatte (status_ungueltig). Jetzt nur bei zeitraum_belegt; sonst der Text aus buchungen-fehler.js. Aufgefallen beim ersten Test mit Donau Home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:35:40 +00:00
knsoandClaude Opus 5.5 a45f436124 Buchungskalender: Zustimmung per WhatsApp zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:13:33 +00:00
knsoandClaude Opus 5.5 1e0eb9f569 Buchungskalender: Preise je Personenzahl (Portal-Editor)
Neuer Haken "Preis hängt von der Personenzahl ab" im Preis-Editor (nur bei Höchstzahl ab 2 Personen). Angeschaltet zeigt jede Staffel k Euro-Felder (1 Person bis k Personen, k = min(Höchstzahl, 10)) statt eines Preises; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" sind dann ausgeblendet, ihre Werte gehen unverändert mit. Speichern sendet personenCent und preisProNachtCent = personenCent[0]. Beim Laden ist der Haken an, wenn personenCent gespeichert sind. Kundenansicht (nur lesen) sperrt Haken und Felder. Felder brechen auf dem Handy um. Tests im Fake-DOM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:06:04 +00:00
knsoandClaude Opus 5.5 02a3732e4a Funktionen: Zustimmung in Textform auch per WhatsApp
Terminplaner, Webshop und Buchungskalender nehmen beim Aktivieren durch das Team den Weg whatsapp an (Nachweis: „per WhatsApp in Textform“). Die Analyse bleibt bei Chat oder E-Mail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:02:49 +00:00
knsoandClaude Opus 5.5 a4ade5f649 Buchungskalender: Preise je Personenzahl (Server)
Jede Staffel darf personenCent tragen (Preis pro Nacht für 1 bis k Personen, k = 2 bis 10), ohne Schemaänderung im JSON-Feld staffeln. pruefePreise prüft Länge, Werte und Gleichheit über alle Staffeln und setzt preisProNachtCent = personenCent[0]. berechnePreis wählt die Spalte der Gästezahl (Posten mit Personenzahl), bei mehr Gästen als Spalten gilt die letzte plus Zusatzperson-Aufpreis. preiseVollstaendig prüft die Konsistenz. Ohne personenCent rechnet alles wie bisher. Tests mit Mimis Preismatrix, Doku in unterkunft-api.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:02:45 +00:00
knsoandClaude Opus 5.5 b264d7f734 Buchungskalender: große Felder als TEXT (Zeilenbreite in Appwrite)
ukAnfragen überschritt mit gast/posten/stornobedingungenText als VARCHAR die MariaDB-Zeilenbreite von 65535 Byte; das Setup brach in Produktion nach 22 Attributen ab. Test sichert die Breite aller Collections ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:57:10 +00:00
knsoandClaude Opus 5.5 69df9e7046 Tests: Aufträge-Rechtstexttests kennen den Satz unterkunft (Merge-Folge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:49:42 +00:00
knsoandClaude Opus 5.5 a1400ac315 Datenschutz: Abschnitt 6f Buchungskalender
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:47:59 +00:00
knsoandClaude Opus 5.5 3526909d61 Buchungskalender: Mail-Nummern K36, K37, T23 (K31/K32/T22 gehören den Aufträgen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:47:29 +00:00
knsoandClaude Opus 5.5 fd971309a8 Buchungskalender: origin/main (Aufträge) zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:46:31 +00:00
knsoandClaude Opus 5.5 59783ce685 Buchungskalender: Livegang – für Mimi kostenlos
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:41:48 +00:00
knsoandClaude Opus 5.5 1c6b6bf8d6 Buchungskalender: Abschluss-Fixwelle B zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:34:30 +00:00
knsoandClaude Opus 5.5 a6c6dabaa3 Buchungskalender: Abschluss – Storno-Muster-Zitat bleibt bis zur Angleichung mit dem Portal-Mustertext (Fixwelle B)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:33:49 +00:00
knsoandClaude Opus 5.5 3f5b030a50 Buchungskalender: Abschluss – L5 Livegang-Notiz (Reihenfolge Datenbank/Deploy/Pilot, Indizes, Textform, WOMS-Push, Paragraf 6f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:26 +00:00
knsoandClaude Opus 5.5 592ac20503 Buchungskalender: Abschluss – Mail-Texte G16 und K33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:02 +00:00
knsoandClaude Opus 5.5 f4bc752553 Buchungskalender: Abschluss – L2-L4 Rechtstexte (Hinweis-Mail mit Loeschdatum, Linkgueltigkeit, Kontoloeschung), HTML/PDF/sha256 neu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:32:02 +00:00
knsoandClaude Opus 5.5 1d9515823c Buchungskalender: Abschluss – Kleinigkeiten Server (Betrag/Waehrung, Erstattungs-Webhook, Obergrenze, Zaehler, Cache, Kommentare, Setup-Check)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:29:55 +00:00
knsoandClaude Opus 5.5 6d35674c1e Buchungskalender: Abschluss – L1 Textform-Zustimmung fuer die Aktivierung ohne Zahlung (Server, GET-Feld unterkunft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:27:50 +00:00
knsoandClaude Opus 5.5 4f9fb6b109 Buchungskalender: Abschluss – S7 Buchungslink-Gueltigkeit (90 Tage nach Abreise, 30 Tage nach Absage), tokenHash beim Anonymisieren leeren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:26:12 +00:00
knsoandClaude Opus 5.5 17400cac1f Buchungskalender: Abschluss – S6 Belegung laedt nur Relevantes, bis-Indizes, 10-s-Speicher
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:25:17 +00:00
ClaudeandClaude Sonnet 5.5 1cbec5b32d Kontrolle: Betreff-Zusaetze Erinnerung und Kopie, Mail-Protokolle ohne Meldung und Adresse
Die Erinnerung beginnt mit 'Erinnerung: ', die Teamkopie mit 'Kopie: ' (der Mailer
nimmt dafuer ein optionales subjectPrefix). Die Protokollzeilen in mails.js nennen
nur Nummer und Fehlerkennung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:24:59 +00:00
knsoandClaude Opus 5.5 ab838d07bc Buchungskalender: Abschluss – S5 Hinweis-Mail K35 vor der Loeschung, Rechnungen im Zustand abgeschaltet lesbar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:23:56 +00:00
knsoandClaude Opus 5.5 1fe1ae31bf Buchungskalender: Abschluss – Preis-Editor löscht Feldfehler beim Tippen, Kalenderraster volle Breite, Export-Link-Feld
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:22:00 +00:00
knsoandClaude Opus 5.5 e2fc4eb676 Buchungskalender: Abschluss – S4 CSS-Variablen im Ticket-Text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:43 +00:00
knsoandClaude Opus 5.5 5bf1504ce8 Buchungskalender: Abschluss – Portal-Fehlertexte für alle Servercodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:33 +00:00
knsoandClaude Opus 5.5 54310d9bae Buchungskalender: Abschluss – S2 Setup-Test kennt den unique-Index
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:29 +00:00
knsoandClaude Opus 5.5 727613d0e2 Buchungskalender: Abschluss – S2 Rechnungsnummer unter einer Sperre, unique-Index; S3 Storno mit Ursprungs-Aussteller
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:19 +00:00
knsoandClaude Opus 5.5 392b85c1f0 Buchungskalender: Abschluss – Buchungsseite zeigt 409-Codes, nur https-Zahlung, Feldfehler barrierefrei
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:21:13 +00:00
knsoandClaude Opus 5.5 a071e57fb7 Buchungskalender: Abschluss – Widget-CSS ohne @layer, alles unter .wku-root, Reset, Fenster 600 Tage, Serverfehlertexte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:20:27 +00:00
ClaudeandClaude Sonnet 5.5 b6c1d81342 Kontrolle: Bestaetigen per Link prueft Token und Ablauf in der Sperre erneut
Ein Token-Tausch durch die Erinnerung zwischen Pruefung und Sperre ergibt 404 wie bei
einem ungueltigen Link; das Portal ist nicht betroffen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:20:24 +00:00
knsoandClaude Opus 5.5 435dd0b746 Buchungskalender: Abschluss – S1 keine doppelte Erstattung nach Heilung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:20:23 +00:00
ClaudeandClaude Sonnet 5.5 c2245b6905 Kontrolle: Projektkonto und Rechnungsbau waehlen dieselbe Aufwands-AB
Gemeinsame Auswahl juengsteAufwandAb (gueltig, jueengste nach gesendetAm) im AB-Speicher.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:19:48 +00:00
ClaudeandClaude Sonnet 5.5 4177b76d82 Kontrolle: Schlussrechnung setzt die AB aus dem Projektdatensatz auf abgerechnet
Nach dem Umhaengen gilt die neue AB, nicht die aus den Stripe-Metadaten.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:19:04 +00:00
ClaudeandClaude Sonnet 5.5 1bb172ad91 Kontrolle: Storno einer Anzahlung bei bestehendem Schluss-Entwurf der AB abgewiesen
409 'Erst den Entwurf der Schlussrechnung löschen.'; das Storno einer Anzahlung laeuft
unter der Kundensperre.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:18:46 +00:00
ClaudeandClaude Sonnet 5.5 e3ae152f12 Kontrolle: Senden eines Anzahlungs- oder Schluss-Entwurfs prueft die AB unter der Kundensperre erneut
Unter sperreRechnung(kundenId): AB noch versendet oder bestaetigt, bei Schluss keine
andere festgeschriebene Schlussrechnung, bei Anzahlung Summe aller festgeschriebenen
Rechnungen plus diese nicht ueber der AB-Summe, sonst 409 und nichts wird finalisiert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:18:22 +00:00
ClaudeandClaude Sonnet 5.5 57d1ab82c6 Kontrolle: Anzahlung und Schlussrechnung nehmen das Zahlungsziel der AB
Ohne daysUntilDue gilt bei Anzahlung und Schluss auftrag.zahlungszielTage (Route
und createProjectInvoice), sonst weiterhin 14 Tage; eine Angabe gilt vor der AB.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:17:06 +00:00
ClaudeandClaude Sonnet 5.5 d96d09f589 Kontrolle: Steuernummer steht im Rechnungsfuss der Projektrechnungen
Die Steuernummer aus den Finanz-Einstellungen kommt als Zeile direkt nach dem
§ 19-Satz in den Fuss; ohne Wert bleibt der Fuss unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:15:56 +00:00
knsoandClaude Opus 5.5 2df56eccc5 Buchungskalender: UI-Durchlauf – Screenshots wie im Webshop, Sichtbarkeitsprüfung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:10:00 +00:00
knsoandClaude Opus 5.5 838bc96d1b Buchungskalender: UI-Durchlauf – Text „null“ in der Summe, Knopf- und Feldgestaltung im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 f5e304ad7a Buchungskalender: UI-Durchlauf – Platzhalter in Stornobedingungen blockieren das Speichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 196921fdf0 Buchungskalender: UI-Durchlauf – Feldfehler der Buchungsseite verschwinden beim Ausfüllen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:09:57 +00:00
knsoandClaude Opus 5.5 03ae8c77cd Buchungskalender: UI-Durchlauf (Playwright)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:06:33 +00:00
knsoandClaude Opus 5.5 cb0799a35c Buchungskalender: UI-Durchlauf – Zustimmungs-Hinweis verschwindet nicht
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:05:45 +00:00
knsoandClaude Opus 5.5 e5793fd151 Buchungskalender: Abgleich – G22/K33-Platzhalter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:58:50 +00:00
knsoandClaude Opus 5.5 74543b75c3 Buchungskalender: Abgleich – API-Doku mit den bindenden Formen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:56:03 +00:00
knsoandClaude Opus 5.5 c3eef4747d Buchungskalender: Abgleich – T22/K34 passen zu den Vorlagen, Config-Werte geprüft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:55:32 +00:00
knsoandClaude Opus 5.5 fbd07b3d8c Buchungskalender: Abgleich – iCal-Export (Escape, aktive Unterkünfte, ab gestern, Rate-Limit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:54:16 +00:00
knsoandClaude Opus 5.5 95cbd473d0 Buchungskalender: Abgleich – gleichzeitige Zweitzahlung wird erstattet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:36 +00:00
knsoandClaude Opus 5.5 f01e6def59 Buchungskalender: Abgleich – art monteurzimmer, Zurückziehen-409 mit Meldung, Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:13 +00:00
knsoandClaude Opus 5.5 54442a4682 Buchungskalender: Abgleich – deutsche Fehlertexte der Fachcodes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:53:13 +00:00
knsoandClaude Opus 5.5 b992dd2388 Buchungskalender: Abgleich – Stornomuster wortgleich zur Doku
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:52:42 +00:00
knsoandClaude Opus 5.5 2eb38032d3 Buchungskalender: Abgleich – Rechnungs-PDF 200-Pfad getestet, 501 nur bei fehlendem Modul
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:52:13 +00:00
knsoandClaude Opus 5.5 d27f6d5ddb Buchungskalender: Abgleich – Testprotokoll der Mails und Config-Vorgaben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:51:44 +00:00
knsoandClaude Opus 5.5 83cdab405b Buchungskalender: Strang ui zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:50:03 +00:00
knsoandClaude Opus 5.5 cebde0e070 Buchungskalender: Strang widget zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:50:03 +00:00
knsoandClaude Opus 5.5 97dd84fbb8 Buchungskalender: Strang mail zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:49:33 +00:00
knsoandClaude Opus 5.5 7b978ad2e4 Buchungskalender: Strang server zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:49:27 +00:00
knsoandClaude Opus 5.5 35b0438d00 Buchungskalender: Team-API wie Webshop (anfrage null, Stripe-Status)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:46:30 +00:00
knsoandClaude Opus 5.5 9d01d38a45 Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:44:21 +00:00
knsoandClaude Opus 5.5 827ee54875 Buchungskalender: Aufbewahrung – Sperre wie Ablauf, Restpunkte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:43:44 +00:00
knsoandClaude Opus 5.5 c019d3c3b0 Buchungskalender: Aufbewahrung und Löschfristen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:38:35 +00:00
knsoandClaude Opus 5.5 f57a3d477e Buchungskalender: iCal-Import und -Export
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:32:34 +00:00
knsoandClaude Opus 5.5 e3b399e003 Buchungskalender: Zustimmungs-Test streng, Online-Zahlung nur beim Einschalten prüfen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:32:20 +00:00
knsoandClaude Opus 5.5 32cffb3518 Buchungskalender: Verwaltungs-API – Einstellungen, Zustimmung, Online-Zahlung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:30:20 +00:00
knsoandClaude Opus 5.5 5541e0cb60 Buchungskalender: Rechtstexte – automatische Erstattung, Stripe-Rücksprung, Kartenzahlung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:27:19 +00:00
knsoandClaude Opus 5.5 d0ab92658a Buchungskalender: Verwaltungs-API für Vermieter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:26:46 +00:00
knsoandClaude Opus 5.5 05006f1480 Buchungskalender: Stornobedingungen je Unterkunft, Zustimmung und Bestellstatus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:26:22 +00:00
ClaudeandClaude Sonnet 5.5 aad0a8b807 Tests: Wächter der Kundenansicht sperrt auch die Routen der Auftragsbestätigung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:24:19 +00:00
ClaudeandClaude Sonnet 5.5 d9058ef1a6 Aufträge: Durchlauf-Test über alle Endpunkte (Festbetrag, Aufwand, AB bis Schlussrechnung, Privatperson, Kundenansicht, Schalter)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:23:52 +00:00
knsoandClaude Opus 5.5 b25689a015 Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:21 +00:00
knsoandClaude Opus 5.5 efeb99e214 Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:19 +00:00
knsoandClaude Opus 5.5 a94b50fbc1 Buchungskalender: Zahlung – Sitzung wiederverwenden, Zahlung nach Storno erstatten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:23:06 +00:00
ClaudeandClaude Sonnet 5.5 f3b6e00bf7 Merge auftraege/l6-kundenportal: Kundenportal (Strang L6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:21:18 +00:00
ClaudeandClaude Sonnet 5.5 5d5f6333a4 Merge auftraege/l5-bestaetigung: Bestätigung und Mails (Strang L5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:21:11 +00:00
ClaudeandClaude Sonnet 5.5 8f53b560d5 Merge auftraege/l7-rechtstexte (ohne Datenschutz-Commit): Rechtstexte (Strang L7)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:51 +00:00
ClaudeandClaude Sonnet 5.5 bd12892ed2 Merge auftraege/l4-ab-pdf: Auftragsbestätigung PDF (Strang L4)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:45 +00:00
ClaudeandClaude Sonnet 5.5 095694bc44 Merge auftraege/l3-ab-kern: Auftragsbestätigung Kern (Strang L3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:43 +00:00
ClaudeandClaude Sonnet 5.5 e5fc5e2c74 Merge auftraege/l2-rechnungen: Rechnungen (Strang L2)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:36 +00:00
ClaudeandClaude Sonnet 5.5 743af802c1 Merge auftraege/l1-projektkonto: Projektkonto (Strang L1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:20:30 +00:00
ClaudeandClaude Sonnet 5.5 d2c48ab99b Rechnungen: Routen für Projektrechnungen und Vorschau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:18:36 +00:00
ClaudeandClaude Sonnet 5.5 d990155df9 Kundenportal Aufträge: Playwright-Prüfung mit Bildschirmfotos (Handy 390 px, Desktop 1440 px, dunkel)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:18:23 +00:00
ClaudeandClaude Sonnet 5.5 88a7305e41 Rechnungen: Rechnungsnummer buchen, AB abgerechnet, Storno und Löschen geben frei
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:17:58 +00:00
ClaudeandClaude Sonnet 5.5 ab3e6938da Rechnungen: Sperre je Kunde gegen doppelte Abrechnung (Aufwand, Anzahlung, Schluss)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:17:11 +00:00
ClaudeandClaude Sonnet 5.5 d264f2af5a Rechnungen: Projektrechnungen bei Stripe (Zusatzfelder, A4, Minuszeilen, Projektdatensatz) und Vorschau
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:48 +00:00
ClaudeandClaude Sonnet 5.5 3beba876d6 Aufträge L5: Widerruf eines Projektauftrags auf der Widerrufsseite (nur Privatpersonen in der Frist, Nummer optional)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:45 +00:00
ClaudeandClaude Sonnet 5.5 60a51c0f68 Widerrufsseite: Vertragsart Projektauftrag mit freiwilliger Nummer der Auftragsbestätigung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:27 +00:00
knsoandClaude Opus 5.5 10fe9a2ae0 Buchungskalender: Portal-Tests mit fester Uhr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:24 +00:00
knsoandClaude Opus 5.5 ca0f762272 Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:17 +00:00
ClaudeandClaude Sonnet 5.5 e680ea3266 AB-Kern: Admin-Routen zum Senden, Erinnern, für neue Fassungen, zum Stornieren und zum Zuordnen eines Projekts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:09 +00:00
knsoandClaude Opus 5.5 4553bc8e92 Buchungskalender: Mails – Link-Pflicht und Testprotokoll nur im Test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:16:04 +00:00
ClaudeandClaude Sonnet 5.5 7d3114d7a3 AB-Kern: Admin-Routen zum Lesen, Anlegen, Ändern, Löschen und für das PDF
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:02 +00:00
ClaudeandClaude Sonnet 5.5 c72b252e63 Aufträge L5: K04 und T09 mit Block projekt für den Widerruf eines Projektauftrags
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:16:01 +00:00
ClaudeandClaude Sonnet 5.5 73c5446cf8 Kundenportal Aufträge: Abschnitt im Reiter Rechnungen, Projekt unter der Rechnungsnummer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:56 +00:00
ClaudeandClaude Sonnet 5.5 c116c3cb4f Rechnungen: mapInvoice liefert Projekt, Art und Auftrag
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:44 +00:00
ClaudeandClaude Sonnet 5.5 66ec8e0a44 AB-Kern: Projekt nachträglich einer Auftragsbestätigung zuordnen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:37 +00:00
ClaudeandClaude Sonnet 5.5 22a7c8123d Aufträge L5: Test – in der Kundenansicht ist das Bestätigen von Aufträgen gesperrt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:33 +00:00
ClaudeandClaude Sonnet 5.5 b53f9f67ed Kundenportal Aufträge: Abschnitt „Aufträge“ im Portal (Karten, Bestätigen, Ansichtsmodus gesperrt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:29 +00:00
ClaudeandClaude Sonnet 5.5 4f8e956574 Rechnungen: Anzahlung und Schlussrechnung bauen (Minuszeilen, Entwurfsschutz, Selbstheilung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:25 +00:00
ClaudeandClaude Sonnet 5.5 503337d7ae Aufträge L5: Portal-Routen für Aufträge (eigene gesendete ABs, PDF, Bestätigen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:22 +00:00
ClaudeandClaude Sonnet 5.5 c791146af4 AB-Kern: Stornieren versendeter und bestätigter Aufträge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:21 +00:00
ClaudeandClaude Sonnet 5.5 cf87a7e1e0 AB-Kern: Neue Fassung und Ersetzen der alten AB beim Senden
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:15:11 +00:00
ClaudeandClaude Sonnet 5.5 6cb5bfa6c7 Aufträge L5: öffentliche Bestätigung per Link (idempotent, Frühbeginn nur bei Privatpersonen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:59 +00:00
ClaudeandClaude Sonnet 5.5 11531cacb6 Rechnungen: Aufwandsrechnung bauen (Auswahl, Satz aus der AB, Doppelabrechnung, 20 Zeilen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:48 +00:00
ClaudeandClaude Sonnet 5.5 0da5b30119 AB-Kern: Erinnerung mit neuem Token und geprüfter PDF-Datei
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:46 +00:00
ClaudeandClaude Sonnet 5.5 aca393d564 Aufträge L5: öffentliche Link-Routen (ansehen, PDF) mit Ratenbegrenzung, Schalter und Statusprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:46 +00:00
ClaudeandClaude Sonnet 5.5 cad9ba7e40 Kundenportal Aufträge: auftrag.html und auftrag.css (noindex, Mail-Stil, Fußzeile, Standalone)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:45 +00:00
knsoandClaude Opus 5.5 8c8fa05591 Buchungskalender: Preis-Editor prüft Grenzen wie der Server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:14:38 +00:00
ClaudeandClaude Sonnet 5.5 c74f7eeea7 AB-Kern: Senden unter Sperre, Fehlerpfade, Dateirest und Mail-Fehler
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:29 +00:00
ClaudeandClaude Sonnet 5.5 576a17bfa4 Aufträge L5: Auftrag widerrufen (nur Privatpersonen innerhalb der Frist, unter Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:22 +00:00
ClaudeandClaude Sonnet 5.5 335ac60e54 Aufträge L5: Auftrag bestätigen (idempotent, unter Sperre, Frühbeginn nur bei Privatpersonen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:08 +00:00
ClaudeandClaude Sonnet 5.5 ed7567397d AB-Kern: Senden mit Prüfungen vor der Nummer, Nummer, PDF, Ablage, Token und Mail
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:14:02 +00:00
ClaudeandClaude Sonnet 5.5 38734e99aa Kundenportal Aufträge: Seitenlogik der öffentlichen Bestätigungsseite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:59 +00:00
ClaudeandClaude Sonnet 5.5 b78ca971af Rechnungen: baueRechnung für freie Rechnung und Festbetrag, Testhilfe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:52 +00:00
ClaudeandClaude Sonnet 5.5 3464808a40 AB-PDF: Entwurfs-Kennzeichnung für die Vorschau und Hinweis auf die ersetzte AB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:51 +00:00
ClaudeandClaude Sonnet 5.5 19abac0373 Aufträge L5: Mails zur Auftragsbestätigung (K31 ohne BCC mit Teamkopie ohne Link, K32, T22)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:46 +00:00
ClaudeandClaude Sonnet 5.5 3a46210e9c AB-Kern: Vorschau-PDF und austauschbare Abhängigkeiten (deps)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:45 +00:00
knsoandClaude Opus 5.5 51a36be36a Buchungskalender: Buchungsseite – Ablauf, bezahlt und Wiederholung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:13:40 +00:00
ClaudeandClaude Sonnet 5.5 6f213dca25 AB-PDF: erzeugeAuftragPdf zeichnet Briefkopf, Tabelle über mehrere Seiten, Belehrung und Fuß mit pdfkit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:34 +00:00
ClaudeandClaude Sonnet 5.5 0fc2eb4516 AB-Kern: Entwürfe anlegen, ändern und löschen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:26 +00:00
ClaudeandClaude Sonnet 5.5 bd87bb115c Kundenportal Aufträge: DOM-Bausteine (Zusammenfassung, Bestätigen mit Häkchen, Sperre)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:24 +00:00
ClaudeandClaude Sonnet 5.5 fd189b085d Stripe-Nachbau: Zusatzfelder, Seitengröße und Minuszeilen wie bei Stripe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:17 +00:00
ClaudeandClaude Sonnet 5.5 01406c82e5 AB-PDF: Textleser für PDF-Tests (ToUnicode, nur Node-Bordmittel)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:14 +00:00
ClaudeandClaude Sonnet 5.5 f0402a7ea9 Aufträge L5: Mailvorlagen K31 Auftragsbestätigung (mit Teamkopie), K32 Auftrag bestätigt, T22 Hinweis ans Team
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:10 +00:00
ClaudeandClaude Sonnet 5.5 f71e3e8345 AB-PDF: Bausteine der Auftragsbestätigung mit Höhen und Zusammenhalt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:13:02 +00:00
ClaudeandClaude Sonnet 5.5 8e643160ec build-legal-pdfs: Modus auftraege, PDFs und Prüfsummen der Widerrufsbelehrung der Projektaufträge und der AGB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:56 +00:00
ClaudeandClaude Sonnet 5.5 2d91a54e4a Rechnungen: Anzahlung und Schlussrechnung mit Minuszeilen (Positionen)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:52 +00:00
ClaudeandClaude Sonnet 5.5 4706f232b5 AB-PDF: Maße und Seitenaufteilung mit wiederholtem Tabellenkopf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:48 +00:00
knsoandClaude Opus 5.5 e15486263f Buchungskalender: Mails an Gäste, Vermieter und Team
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:12:47 +00:00
ClaudeandClaude Sonnet 5.5 ef646f12de AB-Kern: Eingabeprüfung für Auftragsbestätigungen (pruefeEingabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:47 +00:00
ClaudeandClaude Sonnet 5.5 9fa965521a Projektkonto: Team-Route für das Abzeichen abgerechnet am Arbeitsblatt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:45 +00:00
ClaudeandClaude Sonnet 5.5 846059b455 Projektkonto: Admin-Routen für Konten je Kunde und Detail je Projekt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:40 +00:00
knsoandClaude Opus 5.5 37cd312549 Buchungskalender: Anfragen-Reiter – Erstattung und Fehlermeldungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:12:37 +00:00
ClaudeandClaude Sonnet 5.5 e6a0fc9acf Kundenportal Aufträge: reine Ansichtslogik (Token, Beträge, Zustände, Status, AB-Nummer)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:34 +00:00
ClaudeandClaude Sonnet 5.5 731bc8c05e AB-PDF: Zahlungsplan, Termine, AGB-Satz, Privatperson-Seiten, Fuß und Vorschau in den PDF-Daten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:33 +00:00
ClaudeandClaude Sonnet 5.5 6c1eaf697b Rechnungen: Aufwandszeilen je Tag, Kalenderwochen-Zusammenfassung ab 20 Zeilen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:28 +00:00
ClaudeandClaude Sonnet 5.5 32726fa1c2 Projektkonto: Abzeichen abgerechnet je Ticket (Rechnungsnummer oder Entwurf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:25 +00:00
ClaudeandClaude Sonnet 5.5 aeae17b491 build-legal-html: Satz auftraege für die Widerrufsbelehrung der Projektaufträge und die AGB, beide Seiten erzeugt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:23 +00:00
ClaudeandClaude Sonnet 5.5 7cec85f888 Projektkonto: Detail mit ABs, Rechnungen und offenen Arbeitsblättern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:19 +00:00
ClaudeandClaude Sonnet 5.5 7ad9c12bca AB-PDF: auftragPdfDaten mit Kopf, Anschrift, Positionen und Prüfung der Pflichtangaben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:18 +00:00
ClaudeandClaude Sonnet 5.5 7553253746 Rechnungen: Positionen-Grundlagen (Stundentext, Kalenderwoche, feste Positionen, Prüfung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:07 +00:00
ClaudeandClaude Sonnet 5.5 8032aa67c6 Projektkonto: Konten je Kunde (Projektwahl, Zeitquelle, Zeitwert je Tag, Ohne Projekt, Schalter, Stripe-Ausfall)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:12:01 +00:00
ClaudeandClaude Sonnet 5.5 edccd4d8af AGB-Kopie: Quelle docs/rechtliches/agb.md wortgleich mit der Website (Fassung 2026-09-30), Tests gegen AGB.tsx
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:57 +00:00
ClaudeandClaude Sonnet 5.5 b097fedf5a AB-PDF: Zeichen ohne Glyph werden ersetzt statt als Kästchen gedruckt
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:54 +00:00
ClaudeandClaude Sonnet 5.5 e3df5383d1 AB-Kern: Nummernvergabe AB-JJJJ-NNNN nach Berliner Jahr
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:52 +00:00
knsoandClaude Opus 5.5 db9fc61894 Buchungskalender: Kalender und Unterkünfte im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:11:51 +00:00
ClaudeandClaude Sonnet 5.5 ae6d84b2a4 AB-PDF: Figtree Regular und SemiBold (OFL) mit Schriftlader und Glyphenprüfung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:48 +00:00
ClaudeandClaude Sonnet 5.5 445d234695 Rechtstexte der Aufträge: Endfassung der Textbausteine, Bestätigung, Frühbeginn-Erklärung und Widerrufsbelehrung (Fassung 2026-10-06)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:47 +00:00
ClaudeandClaude Sonnet 5.5 cf3e0fd52a Aufträge L5: Bestätigungs-Token (32 Zufallsbytes, nur als Hash gespeichert, 60 Tage gültig)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:43 +00:00
ClaudeandClaude Sonnet 5.5 10679ae726 Projektkonto: Stripe-Rechnungen lesen (Liste, Einzellesen fehlender, Ausfall)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:41 +00:00
ClaudeandClaude Sonnet 5.5 68ffac24c4 AB-Kern: Testhilfe und Vertragsprüfung der L0-Module
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:34 +00:00
ClaudeandClaude Sonnet 5.5 3305371111 Projektkonto: reine Rechenregeln (vereinbart, berechnet, offen, Zeitwert je Tag gerundet, Ohne Projekt)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:32 +00:00
ClaudeandClaude Sonnet 5.5 987268e2f0 Kundenportal: Rechnungsliste kennt das Projekt (Feld projekt aus den Stripe-Metadaten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:11:30 +00:00
knsoandClaude Opus 5.5 0b0d8da335 Buchungskalender: Buchungsseite für Gäste
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:50 +00:00
knsoandClaude Opus 5.5 40a366b8ae Buchungskalender: ID-Prüfung erlaubt Bindestrich (UUIDs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:46 +00:00
knsoandClaude Opus 5.5 bc9b4d9576 Buchungskalender: Widget-Monatsnavigation und Tastaturfokus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:34 +00:00
knsoandClaude Opus 5.5 0c5ca76a4d Buchungskalender: öffentliche API gegen manipulierte IDs und Lookups absichern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:10:30 +00:00
ClaudeandClaude Opus 5.5 8627ed1124 Aufträge: Router-Gerüste und Einbindung in server/index.js und Team-Router
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:08:37 +00:00
ClaudeandClaude Opus 5.5 4f467fa9c5 Finanzen: Einstellung Steuernummer für die Auftragsbestätigung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:08:06 +00:00
ClaudeandClaude Opus 5.5 e9e198e53c Aufträge: PDF-Ablage mit Pfadprüfung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:07:32 +00:00
ClaudeandClaude Opus 5.5 98eceaf14c Aufträge: Sichten auf die Auftragsbestätigung (Team, öffentlich, Frühbeginn)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:07:15 +00:00
knsoandClaude Opus 5.5 9b509fefbc Buchungskalender: Website-Widget mit Kalender und Anfrageformular
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:53 +00:00
ClaudeandClaude Opus 5.5 47a92811a3 Aufträge: Arbeitsblätter laden, normieren und als abgerechnet erkennen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:50 +00:00
knsoandClaude Opus 5.5 3ecea05b52 Buchungskalender: Portal-Bereich und Anfragen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:48 +00:00
knsoandClaude Opus 5.5 035967f8eb Buchungskalender: öffentliche API für Kalender und Anfrage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:30 +00:00
ClaudeandClaude Opus 5.5 9cd5ea3038 Aufträge: Speicher der Projektrechnungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:19 +00:00
knsoandClaude Opus 5.5 118be44847 Buchungskalender: PDF-Writer und Rechnungs-PDF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:06:11 +00:00
ClaudeandClaude Opus 5.5 a3db7f310a Aufträge: Speicher der Auftragsbestätigungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:05:49 +00:00
ClaudeandClaude Opus 5.5 4e4feba4c9 Aufträge: vorläufige Textbausteine der Auftragsbestätigung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:05:02 +00:00
ClaudeandClaude Opus 5.5 004e564479 Aufträge: Setup-Skript für Collections und Zusatzfelder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:51 +00:00
ClaudeandClaude Opus 5.5 ddffc7a92d Aufträge: Konfiguration und Pilot-Schalter AUFTRAEGE_ENABLED
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:48 +00:00
ClaudeandClaude Opus 5.5 0ab3cbbaa3 Aufträge: Appwrite-Schema der Collections auftraege und projektRechnungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:04:01 +00:00
ClaudeandClaude Opus 5.5 16f08b5ac3 Aufträge: Konstanten und Fehlerklasse (L0)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:03:25 +00:00
ClaudeandClaude Opus 5.5 1b5b5b7094 Aufträge: pdfkit und fontkit als Abhängigkeiten, node_modules auch als Symlink ignorieren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:02:50 +00:00
knsoandClaude Opus 5.5 d7b5b98187 Buchungskalender: Verdrahtung absichern (Outbox, Shop-Webhook, Session)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:02:41 +00:00
knsoandClaude Opus 5.5 a68baf10d9 Buchungskalender: Verdrahtung, Router-Gerüste und API-Vertrag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:59:38 +00:00
knsoandClaude Opus 5.5 1d2ca2791d Buchungskalender: Storno und Erstattung getrennt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:58:17 +00:00
knsoandClaude Opus 5.5 f7650fe883 Buchungskalender: Storno und Buchung idempotent bei Teilfehlern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:55:56 +00:00
knsoandClaude Opus 5.5 947e9da2e3 Buchungskalender: Statusmaschine, Token und Rechnungsnummern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:52:52 +00:00
knsoandClaude Opus 5.5 802b5d3ad4 Buchungskalender: Mengen-Obergrenze für Posten auf 20000
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:50:22 +00:00
knsoandClaude Opus 5.5 bdf191d945 Buchungskalender: Speicher und Belegung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:49:29 +00:00
knsoandClaude Opus 5.5 c38c8e7c13 Buchungskalender: Preisrechnung nachgeschärft (Summe, Kleinunternehmer, Vollständigkeit)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:49:20 +00:00
knsoandClaude Opus 5.5 84e7a99f41 Buchungskalender: Preisrechnung und Datumshilfen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:47:41 +00:00
knsoandClaude Opus 5.5 5717a5cdd0 Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:45:57 +00:00
knsoandClaude Opus 5.5 18369a348c Plan: Buchungskalender (23 Tasks, 4 parallele Stränge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:41:26 +00:00
knsoandClaude Opus 5.5 668bc62c55 Spec Buchungskalender: Nachträge aus der Code-Prüfung (N1–N6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:36:12 +00:00
knsoandClaude Opus 5.5 5a05bd27e9 Spec: Buchungskalender für Unterkünfte (Funktion unterkunft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:22:02 +00:00
ClaudeandClaude Opus 5.5 9d82fc579f Umsetzungspläne Aufträge und Rechnungen: Übersicht und 12 Strang-Pläne
Parallele Stränge L0–L7 (Portal), W0–W2 (WOMS), Z (Integration und
Kontrolle); Vertragsfragen der Planrunde entschieden (Abschnitt
„Nachträge nach der Planrunde“ in der Übersicht).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:49:15 +00:00
ClaudeandClaude Opus 5.5 0bb463eb7d Spezifikation nachgeschärft, Plan-Übersicht mit Verträgen der Stränge
Nachträge aus der Planrunde (Fragment-Link, Teamkopie ohne Link, Schalter
AUFTRAEGE_ENABLED, Projekt später zuordnen, Schlussrechnung bis 40 Zeilen,
zusammengesetzte Sperrschlüssel, Zuständigkeiten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 12:06:09 +00:00
ClaudeandClaude Opus 5.5 e00b4ae572 Spezifikation: Auftragsbestätigung und projektbezogene Rechnungen
Entwurf aus dem Brainstorming vom 06.10.2026 (Abschnitte im Chat freigegeben):
Projektkonto, Rechnungsarten Anzahlung/Schluss/Aufwand/Festbetrag über Stripe,
Auftragsbestätigung als eigenes PDF mit Bestätigungsklick und Verbraucher-Variante,
zwei Pläne.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:03:25 +00:00
ClaudeandClaude Opus 5.5 8814c4aef1 Webshop: fehlendes Stripe-Plattformprofil klar melden statt 'Stripe nicht erreichbar'
Lehnt Stripe die Kontoanlage ab, weil das Connect-Plattformprofil von WEBklar fehlt (oder Connect nicht aktiv ist),
bekommt der Kunde 503 'plattform_nicht_bereit' mit eigenem Du-Text, und das Log nennt die Ursache (ohne Stripe-Text).
Gefunden im Pilot 2026-10-06.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:58:20 +00:00
ClaudeandClaude Opus 5.5 50f854ef1c Öffnungszeiten: Datenschutz-Abschnitt heißt 6e (6d ist seit dem Webshop vergeben); Härtungspunkt N13
Entwurf umbenannt (zeiten-datenschutz-6e-entwurf.md), Leistungsbeschreibung, Kanzlei-Fragen,
Livegang-Notiz und Test-Pins ziehen mit. N13: Rückgabe-Marker nach dem Löschen fehlt (vor on).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:30:03 +00:00
ClaudeandClaude Opus 5.5 d0691236d5 Öffnungszeiten: Test hält fest, dass ein gescheitertes Löschen der Zeiten den Kunden bis zum nächsten Lauf hält
Seit TP2 bleiben die Abo-Dokumente stehen (held), wenn das Löschen abhängiger Daten scheitert. Der Test für die
Aufbewahrungs-Löschung prüft jetzt auch für die Zeiten: held nennt den Kunden, das Abo-Dokument bleibt, der
nächste Lauf findet den Kunden wieder und erst nach dem Löschen der Zeiten gehen die Abo-Dokumente.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 3984d468bf Öffnungszeiten: Datenschutz-Entwurf an den Stand von main angepasst (§ 7-Satz, Nummer 6d)
Der Wächter in zeitenRecht.test.js hat den Drift gefunden: Abschnitt 7 in public/datenschutz.html enthält seit TP2
den Satz zu Bestellungen ohne Abo. Das wörtliche Zitat im Entwurf ist nachgeführt, der Vorschlag ändert wie
bisher nur den Anfang des Satzes. Dazu ein Nachtrag im Entwurf: Abschnitt 6d ist seit dem Webshop-Livegang
vergeben, der Abschnitt der Öffnungszeiten muss beim Übernehmen als 6e eingebaut werden (Anker id="zeiten"
bleibt); die Umnummerierung entscheidet der Inhaber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 3e66dfa91c Tests nach dem Zusammenführen mit main: Reihenfolge der Funktionsschlüssel und Importe von funktionen.js
Die Öffnungszeiten stehen jetzt hinter dem Webshop (news, termine, shop, zeiten): Register, FEATURE_KEYS und
ticketFreischaltungKeys in zeitenRegister, die drei Listen in app.js (VIEW_HASHES, APP_PARENT, FEATURE_LABELS)
in zeitenBestellen und shopWiring (APP_PARENT endet mit webshop, zeiten). funktionenZeitenTile prüft die Importe
von funktionen.js gegen den Stand von main (fn-util.js und analyse-view.js); die Kachel der Öffnungszeiten
bringt weiterhin keinen neuen Import mit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00
ClaudeandClaude Opus 5.5 c7cd436ec2 Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den
Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der
Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name.
Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:00:17 +00:00
ClaudeandClaude Opus 5.5 ea4457e6a4 Öffnungszeiten: Fixrunde Doku (Datenschutz-Entwurf, Einbau-Anleitung, Kanzlei-Fragen, Livegang-Notiz)
- 6d-Entwurf § 7: Stripe-Sitzung nur bei Bestellung, IP-Adresse und Browserkennung bei jeder Zustimmung
  (auch Gratisweg); Offener Punkt 4 beantwortet
- Einbau-Anleitung: CSP braucht auch style-src, Portal mit docker compose up -d neu erstellen, Satz zu
  Einzelseiten-Apps, data-api nur am Script-Tag
- Kanzlei-Fragen: ZQ11 (Team-Aktivierung ohne Zustimmung), ZQ12 (Pflichtinformationen als Haken und in K15);
  ZQ3 und Schlussnotiz an den Stand von Wertersatz-Satz und Widerruf-Seite angepasst
- Livegang-Notiz: Zusammenführung über origin/main mit erwarteten Konflikten, genaue Push-Refs und
  Webhook-Warnung, Sicherungen und Prüfungen vor der Compose-Änderung, Schema-Check nach dem Anlegen,
  Runner-Neustart prüfen, setup-legal als ungetestet, Live-Checks und Rückweg, Handrezept für Testdaten,
  Test-Anpassung bei bald:false, Checkliste nachgeführt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:16:42 +00:00
ClaudeandClaude Opus 5.5 aa9ee35ab6 Öffnungszeiten: Fixrunde Website-Skript und Editor (JSON-LD, Kopf beim Scrollen, späte Platzhalter, Links, Bestätigung)
- JSON-LD wird ohne "<" geschrieben (<), ein </script> im Firmennamen bricht keinen Block mehr
- verschiebe(): ist die Hinweis-Leiste weggescrollt, bekommen feste Elemente ihren eigenen Inline-top zurück
  (Köpfe, die per Klasse beim Scrollen wegfahren, funktionieren wieder)
- Späte Platzhalter (Einzelseiten-Apps): entprellter MutationObserver auf body zeichnet neue oder wieder
  eingehängte Platzhalter nach
- sichererLink lehnt Zugangsdaten in der Adresse ab; die Adresse der Schnittstelle bestimmt nur das eigene
  Script-Tag, kein Platzhalter mehr (der siteKey am Platzhalter bleibt laut Spec 4.2)
- Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:10:02 +00:00
ClaudeandClaude Opus 5.5 ecf67d6978 Öffnungszeiten: Fixrunde Server (Schema-Kopplung, K27, Wertersatz, Widerruf, Ticket nachlegen)
- activate() schreibt die Zeiten-Felder (aboEndeAt, behaltenSessionId) nur noch über extra
  (nur Abo-Rückkehr und Team-Aktivierung der Zeiten); News und Terminplaner laufen auch vor zeiten-setup.mjs
- Abo-Abgleich: K27 gilt als offen, solange der Marker vor disabledAt liegt (kein Zurücksetzen nach disable() nötig);
  K27 geht erst nach dem Ersatz-Ticket raus (scheitert es, wiederholt der nächste Lauf beides);
  Löschung setzt auch activatedAt zurück
- Zustimmung "beginn" (bezahlt) mit Wertersatz-Satz (§ 357a Abs. 2 BGB) wie bei News und Terminplaner, Fassung 2026-10-05
- Widerruf: Bestellung "zeiten" in ORDERS, FEATURE_OF_ORDER, orderLabel und widerrufen.html
- Team "Ticket nachlegen" der Zeiten ohne Kauf geht über holeZeitenNach statt über den Zahlungsweg
- Einschalten-Route nimmt nur { zustimmung }, nicht die Altform { consent: true }
- Team-Aktivierung behält kauf/unlockedByPurchase bei einem bisher gekauften Dokument

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:04:05 +00:00
ClaudeandClaude Opus 5.5 c10e50d0b3 Öffnungszeiten: Livegang-Notiz
Reihenfolge nach Spec 13.6 mit allen LIVEGANG-Hinweisen, Probelauf, Rechtsrunde
und Checkliste der offenen Code-Punkte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:32:59 +00:00
ClaudeandClaude Sonnet 5.5 90e8607e26 Öffnungszeiten: Rechtstext-Entwürfe und Einbau-Anleitung
Leistungsbeschreibung „Öffnungszeiten live“ als statische Seite unter
/vertragsunterlagen/zeiten-leistungsbeschreibung.html (Register-Adresse, nur HTML-Kommentar
ENTWURF), Entwürfe für Datenschutz § 6d mit Ergänzung § 6, den Block funktionen in K02/K03
und die Kanzlei-Fragen (⚖ AVV Funktionen, Behalten-Zustimmung), Absatz „Öffnungszeiten auf
dieser Website“ für Variante A und B, Einbau-Anleitung für das Team. Keine Codeänderung,
datenschutz.html und die Mailvorlagen bleiben unverändert. test/zeitenRecht.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 07:34:56 +00:00
ClaudeandClaude Opus 5.5 6a9daa0583 Öffnungszeiten: Urlaub-Frage lässt offene Hinweis-Formulare stehen
„Ja, anlegen“ zeichnet nur noch die Hinweis-Liste neu (offene, ungespeicherte
Formulare und Zeilen bleiben), statt den ganzen Abschnitt zu ersetzen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:54:45 +00:00
ClaudeandClaude Opus 5.5 b3e96d4a56 Öffnungszeiten: Editor für Hinweis- und Kontakt-Leiste, Google und Vorschau
Hinweis-Leiste (Liste, „Hinweis-Leiste anlegen“, Art, Knopf, Zeitraum, Preis-Hinweis),
Kontakt-Leiste (Telefon, Adresse, WhatsApp mit Datenschutz-Haken, R12 in der Kundenansicht),
Google (Für Google kopieren, Profil-Link), Live-Vorschau mit Regler „Uhrzeit testen“
und UI-Prüfung test/ui/zeiten-portal.ui.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:43:22 +00:00
ClaudeandClaude Opus 5.5 242fbb01e5 Öffnungszeiten: Plan-löschen-Rückfrage zählt die Ausnahmen beim Öffnen
Der Hinweis „Die N Ausnahmen, die nur für diesen Plan gelten, werden mit gelöscht“
wurde beim Bauen der Karte berechnet und war nach dem Speichern einer Ausnahme
veraltet. Er wird jetzt beim Öffnen der Rückfrage aus dem aktuellen Stand gelesen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:23:56 +00:00
ClaudeandClaude Opus 5.5 f01ed15247 Öffnungszeiten: Speichern zeichnet nur neu, was sich geändert hat
Ungespeicherte Eingaben in anderen Formularen bleiben stehen: nach dem Speichern
wird nur die Plankarte, die Planliste (mit den Eingaben der übrigen Karten),
Feiertage oder Ausnahmen neu gezeichnet, „Nein, danke“ entfernt nur die Frage.
Während des Speicherns sind auch die Eingabefelder gesperrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:18:21 +00:00
ClaudeandClaude Opus 5.5 04a3525f79 Öffnungszeiten: Editor für Pläne, Feiertage und Ausnahmen
Speichern über PUT /api/zeiten (Version, 409, Feldfehler per ID-Pfad), Zeitpläne mit
Wochentagen, Hauptplan wechseln und Löschen (R10, R33), Feiertage je Bundesland,
Betriebsurlaub und Sondertage mit Hinweis-Leisten-Frage. Die Hülle behält den Editor
bei einem neuen Stand und zeichnet nur den Kopf neu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:02:31 +00:00
ClaudeandClaude Opus 5.5 cee44d6852 Öffnungszeiten: Behalten nur ohne Abo anbieten (kein_abo)
Behalten (30 €) gab es bei jedem inklusive false. nicht_aktiv und gekuendigt heißen aber,
dass das Wartungs-Abo läuft (Server: 409 Abo läuft wieder). Jetzt gilt an allen fünf Stellen
(Kachel, Karte, Behalten-Seite, Weg aus der Kachel, Abgeschaltet-Karte) dasselbe Prädikat:
inklusive false UND grund kein_abo; sonst „Abo läuft wieder“ ohne Bezahlknopf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:41:24 +00:00
ClaudeandClaude Opus 5.5 7bb918390a Öffnungszeiten: Kachel und Bestell-/Einschaltweg im Portal
Kachel „Öffnungszeiten live“ inline in funktionen.js (keine neuen Importe, keine Zusatzabfrage),
Ansicht #zeiten (zeiten.js, zeiten-bestellen.js, zeiten.css) mit Einschalten im Wartungs-Abo,
Bestellen für 30 € (Widerrufsbelehrung vor dem Knopf, Ruling R32), Behalten nach dem Abo-Ende,
Statuskarte je Zustand, Rückkehr von Stripe und Platz für den Editor (Aufgabe 29).
GET /api/funktionen liefert dafür zeiten.belehrung (Haken belehrung der Funktion).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:30:23 +00:00
ClaudeandClaude Opus 5.5 f20f056c60 Öffnungszeiten: Helfer für den Editor
Reine Client-Helfer (public/zeiten-view.js): Grenzen, neue IDs, Kurznamen, Fensterregeln und
Wochenfehler wie auf dem Server, Hauptplan wechseln, Löschgründe, Hinweis-Vorschlag zum
Betriebsurlaub, Preis-Hinweis, Link-Prüfung. Die Tests halten jede Regel gegen
server/services/zeiten/pruefen.js (zufällige Eingaben, gleiche Ergebnisse).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:05:01 +00:00
ClaudeandClaude Opus 5.5 811e054ca4 Öffnungszeiten: Ticket nachlegen lehnt eine Behalten-Zahlung ab (Stillstand an feature-admin)
bezahlteSitzung weist Sitzungen mit metadata.zweck behalten mit 409 ab. Sonst ginge die Sitzung über onFeaturePaid in bucheBehalten, das
feature-admin:<projekt> nimmt, während die Team-Route sie schon hält: Stillstand und dauerhaft belegter Schlüssel. Test in
test/zeitenTeamBehalten.test.js: 409 binnen Sekunden, Sperre frei, folgende Team-Aktion nicht blockiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:58:25 +00:00
ClaudeandClaude Sonnet 5.5 019833d385 Öffnungszeiten: Tests und Belehrung nach dem Merge mit TP2
Zeiten nutzen die echte Widerrufsbelehrung wie der Terminplaner (der Platzhalter BELEHRUNG_PLATZHALTER ist
mit TP2 F7 entfallen). Gepinnte Listen als Vereinigung beider Seiten: K15-Blöcke, T16-Blöcke, legal-Schlüssel der
Projektliste, hindernis in der Team-Ansicht für alle Funktionen. Neue Tests: Abnahme (Freischalten) der Zeiten über den
Haken freischaltbar und mit Ersatz-Hash, abnahme gibt für Zeiten nie ab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 04:43:20 +00:00
ClaudeandClaude Opus 5.5 1d5a565e9f Merge feature/terminplaner-tp2 (a629b25) in feature/oeffnungszeiten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:42:31 +00:00
ClaudeandClaude Opus 5.5 ed99234ae4 Öffnungszeiten: Hinweis-Leiste, Kontakt-Leiste und JSON-LD im Website-Skript
zeiten.js zeichnet mit denselben Daten wie die Platzhalter eine Hinweis-Leiste
(Shadow DOM, erstes Kind von body, rückt feste und klebende Köpfe, scroll-padding-top),
eine Kontakt-Leiste nur auf schmalen Schirmen (Abstandhalter, scroll-padding-bottom,
entfällt bei eigener Anruf-Leiste) und ergänzt das JSON-LD der Seite (nur die beiden
Zeiten-Felder, sonst eigener Block). Alle Adressen aus der Schnittstelle werden im
Browser erneut geprüft. Ruling R29: tick mit try/finally, sofort werfender fetch,
erst zeichnen dann laden beim Zurückkehren in den Tab.

Tests: statische Funktionstests der reinen Funktionen und eine Playwright-Prüfung
gegen den UI-Testserver (echte öffentliche Schnittstelle, Nachbau von Appwrite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:27:04 +00:00
ClaudeandClaude Opus 5.5 a6a4eae294 Öffnungszeiten: Website-Skript mit Status und Tabelle
public/embed/zeiten.js ruft die öffentliche Zeiten-API einmal je Seite ab und füllt
die Platzhalter [data-webklar-zeiten] (tabelle, status, badge; data-plan, Hauptplan
ohne Angabe). Reine Funktionen (waehleWechsel, naechsterWechsel, waehleHinweis,
jetztMitServer, tabelleDaten) stehen zwischen den Marken @rein-anfang/@rein-ende.
Timer auf den nächsten Wechsel (jede Wartezeit auf 10 Minuten begrenzt), Neuladen
höchstens alle 10 Minuten, Daten älter als 24 Stunden, Fehler oder aktiv:false
lassen das Ersatz-HTML stehen bzw. stellen es wieder her. Testseite zeiten-demo.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:01:31 +00:00
ClaudeandClaude Opus 5.5 a2d11ad6c4 Runner: Zeiten-Skript beim Veröffentlichen einsetzen
injectIntoHtml nimmt eine Marke je Tag (geprüft gegen data-wk-[a-z]+),
injectZeiten setzt den data-wk-zeiten-Tag ohne Datenschutzseite ein,
validateJob lässt nur den siteKey durch, ZEITEN_PUBLIC_BASE in der Konfiguration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:46:06 +00:00
ClaudeandClaude Opus 5.5 a198c472bd Öffnungszeiten: Feiertags-Erinnerung und Jobs
erinnereFeiertage(now): stündlich, nur bei Schalter pilot/on (pilot: nur
freigegebene Kunden) und nur 08:00 bis 20:00 Berliner Zeit; je Funktion in
setup/active zuerst der Merker sha1(featureId|datum) je Tag (409 = schon
erinnert), danach eine Mail K25 mit allen neuen Tagen ohne BCC ans Team;
Merker älter als 30 Tage werden gelöscht. zeitenAboJob (täglich) und
zeitenErinnerungsJob (stündlich) mit eigener Überlappsperre, ohne Wurf;
in server/index.js nach 240 s bzw. 300 s gestartet, mit unref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:40:07 +00:00
ClaudeandClaude Opus 5.5 9a14e874c0 Öffnungszeiten: Daten bei Kundenlöschung entfernen
Kunde löschen schaltet die Funktion zeiten wie den Terminplaner ab (Grund
kunde_geloescht) und löscht Zeiten, Erinnerungsmerker und Antwortspeicher
sofort; Fehler halten die Kundenlöschung nicht auf. Die Löschfristen löschen
die Zeiten vor dem Abschalten und zählen sie im Bericht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:30:00 +00:00
ClaudeandClaude Opus 5.5 efc6dcf332 Öffnungszeiten: Abo-Abgleich mit 30 Tagen Frist
gleicheZeitenAboAb(now) koppelt die im Wartungs-Abo enthaltenen Zeiten
(unlockedBy abo) an das Abo (Spec 5, Z11): Abo-Ende erkennen (aboEndeAt, K26),
nach 30 Berliner Kalendertagen abschalten (abo_ende, K27, Ersatz-Ticket),
bei Rückkehr des Abos wieder einschalten (Neubau nach dem Abschalten, K24
wieder), 30 Tage nach dem Abschalten Daten löschen und state off.
Lesefehler beim Abo: Kunde überspringen, nie abschalten oder löschen.

R25: activate() leert aboEndeAt und behaltenSessionId; starteBehalten lehnt
bei laufendem Wartungs-Abo mit 409 ab (Lesefehler 503).
R17: k24Mail mit einbau null und wieder schaltet beide Einbau-Blöcke ab,
K24 bekommt dafür den Block weiter um den Kasten "Wie geht es weiter?".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:16:56 +00:00
ClaudeandClaude Opus 5.5 53253f0451 Öffnungszeiten: nach Abo-Ende für 30 € behalten
Neuer Modus neben Bestellen und Einschalten (Spec 3.3): POST /api/funktionen/zeiten/behalten
startet einen Checkout mit zweck 'behalten', ohne den Zustand zu ändern; der Webhook-Zweig
bucht vor markPaid/adoptPaidSession unter feature-admin zum Kauf (Frist weg, stripeSessionId =
Sitzung), schaltet eine abgeschaltete Funktion wieder ein (Neubau nach der Sperre), ohne neues
Ticket, mit K15 im Behalten-Wortlaut. Passt die Sitzung nicht, geht T19 nach der Sperre raus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:54:39 +00:00
ClaudeandClaude Opus 5.5 97e6971276 Öffnungszeiten: Team-Aktionen und Ersatz-Ticket beim Abschalten
Team-Route für zeiten: Aktivieren läuft über den Gratis-Pfad (einschaltenZeiten, setup,
unlockedBy team, Neubau, Einbau-Ticket, keine K24), Abschalten setzt disabled mit Grund team
ohne Löschfrist und legt über nachAbschaltung bei geändertem Wochenplan das Ticket
"Ersatz-Tabelle aktualisieren" an. PATCH /:customerId/zeiten/team speichert die
Team-Einstellungen mit teamVersion. Die Team-Ansicht liefert für zeiten zusätzlich plaene,
team, teamVersion und hindernis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:39:43 +00:00
ClaudeandClaude Opus 5.5 11dc4b2c9f Öffnungszeiten: Freischalten erst mit eingetragenen Zeiten
Der 5-Minuten-Job setzt Öffnungszeiten live nach dem Schließen des Tickets
nur noch auf active, wenn der Kunde Zeiten eingetragen hat (Haken
freischaltbar: Hauptplan gespeichert, mindestens ein Tag nicht "zu";
Lesefehler ergeben false, das Dokument bleibt in setup). Neuer Register-Haken
nachFreischaltung: runActivation ruft ihn je freigeschaltetem Dokument vor der
K16 auf, Fehler werden nur geloggt; für die Zeiten merkt er den Ersatz-Hash
(wochenplanHash). Selbstheilung (Spec 3.2): im Lauf des Jobs (nicht in der
Projektliste mit customerId) holt holeZeitenNach für Zeiten in setup fehlendes
Ticket und K24 nach, je Dokument abgesichert und ohne Kundendaten im Log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:28:52 +00:00
ClaudeandClaude Opus 5.5 47ae572fff Öffnungszeiten: Bezahlen gesperrt, wenn im Abo enthalten
Checkout: neue Stufe 7b pruefeInklusive (Funktionen mit aboInklusive):
laeuft das Wartungs-Abo (auch nicht aktiv), 409 inklusive mit Hinweis aufs
Einschalten; kein Abo, Hosting und gekuendigt zahlen weiter 30 Euro.

Zahlung (markPaid): setup mit unlockedBy kauf, dann Neubau (stosseNeubauAn,
einbau am Dokument), erst danach das Einbau-Ticket mit dem Ergebnis; die
Webhook-Wiederholung baut nicht noch einmal. Spaete Zahlungen aendern ein
vorhandenes unlockedBy abo/team nicht. News und Terminplaner unveraendert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:18:30 +00:00
ClaudeandClaude Opus 5.5 66793d06d5 Öffnungszeiten: Einschalten im Wartungs-Abo
POST /api/funktionen/zeiten/einschalten (Gratisweg, Spec 3.2): Abo frisch
prüfen, Zustimmung (consentFrei) und Nachweis ohne Stripe-Sitzung, Zustand
setup mit unlockedBy abo, dann Neubau, Einbau-Ticket und K24; ein zweiter
Klick holt Fehlendes nach (holeZeitenNach, eigene Sperre). GET /api/funktionen
liefert das Angebot (zeiten), features.zeiten mit unlockedBy, aboEndeAt, einbau,
loeschungAm sowie zeitenStatus und zeitenLeer je Projekt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:03:49 +00:00
ClaudeandClaude Opus 5.5 8d670ca5dc Öffnungszeiten: Einbau-Ticket und Ersatz-HTML für das Team
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:48:20 +00:00
ClaudeandClaude Opus 5.5 302b58cf72 Öffnungszeiten: Neubau anstoßen und Feld im Runner-Auftrag
buildZeiten (einbau.js) liefert dem Runner-Auftrag { siteKey } bei Funktion in setup/active,
null bei Sperrliste und Fehlern. stosseNeubauAn (neubau.js) stößt den Neubau unter der
Sperre zeiten-neubau an und meldet angestossen oder manuell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:41:10 +00:00
ClaudeandClaude Opus 5.5 0042d8dd8f Öffnungszeiten: Mails K24 bis K27
Neue Kundenmails (Du-Form, Skelett von K23):
- K24 eingeschaltet (Blöcke angestossen/manuell nach einbau, wieder nach der Abo-Rückkehr)
- K25 Feiertage prüfen (Blöcke pruefen/mehrere, ohne BCC)
- K26 Abo beendet (abo_ende/hosting/wechsel_widerrufen, Frist, einmalig 30 € behalten)
- K27 abgeschaltet (feste Zeiten, Eingaben bis zum Löschdatum, Wiedereinschalten)

server/services/zeiten/mails.js: Builder k24Mail bis k27Mail und sendeZeitenMail
(wirft nie, ohne Adresse { sent:false, reason:'keine_adresse' }, Log ohne Adresse und Namen).

K15 bekommt den Block behalten (Gegenstück neu für den Einrichtungstext; bestellMail
setzt neu = nicht behalten, Standard unverändert), T16 die Blöcke abo und team
(sendT16 führt sie als Vorgabe aus). Pins in mailTemplates- und featureMails-Tests
angepasst; K26 darf „Wartungs-Abo“ im Betreff führen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:33:04 +00:00
ClaudeandClaude Opus 5.5 d1f9176c99 Öffnungszeiten: öffentliche Route
GET /api/public/zeiten/v1/:siteKey mit CORS *, OPTIONS 204, 405, weichem ETag
ohne stand, Cache-Control max-age=60, Expose-Headers Date, Grenze 120 je Minute
und IP (zeiten/limits.js), 503 ohne Details und 404 für unbekannte Pfade.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:19:43 +00:00
ClaudeandClaude Opus 5.5 d4c1be91c7 Öffnungszeiten: wechsel der öffentlichen Antwort trägt wieder den Berliner Tag
Das Website-Skript rechnet keine Zeitzonen und braucht tag (Berliner Kalendertag
bei ab) für „heute“ und die nächsten 14 Tage; wechsel-Einträge sind wieder
{ ab, tag, offen, text, kurz }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:15:42 +00:00
ClaudeandClaude Opus 5.5 1c9f773e97 Öffnungszeiten: öffentliche Antwort mit Cache
oeffentlich.js baut die Antwort für das Website-Skript (Pläne mit Woche, 60 Tagen
und Statuswechseln, Hinweise, Kontakt-Leiste, JSON-LD, Firma, Team) und merkt sie
30 Sekunden je siteKey. Ungültige Schlüssel und ausgeschalteter Schalter lösen
weder eine Abfrage noch einen Cache-Eintrag aus; die Funktion antwortet schon in
setup. findActiveBySiteKey nimmt dafür { states } entgegen (Vorgabe wie bisher
nur active).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:12:16 +00:00
ClaudeandClaude Opus 5.5 7bf2ad2665 Öffnungszeiten: Kundenschnittstelle zum Bearbeiten
GET/PUT /api/zeiten/:projectId und GET .../vorschau für den Editor im Portal.
Zugang nach Funktionszustand (ordered/setup/active lesen und speichern,
disabled mit Daten nur lesen), Version 0 vor dem ersten Speichern, 409
ZEITEN_VERALTET, 60 Speichervorgänge je Kunde und Stunde. Die Kundenansicht darf
speichern (Protokoll zeiten_gespeichert), den WhatsApp-Haken setzt nur der
Kunde selbst: bestaetigtAt kommt vom Server (R12), in der Kundenansicht 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:01:24 +00:00
ClaudeandClaude Opus 5.5 097036c9a5 Öffnungszeiten: Speicher mit Versionen und Cache
LRU-Speicher mit Ablaufzeit (lru.js), Cache-Instanz der öffentlichen Antwort
(cache.js: 500 Einträge, 30 s) und Appwrite-Speicher mit getrennten Versionen
für Kundendaten und Team-Einstellungen (speicher.js), alle Schreibzugriffe
unter der Sperre zeiten:<projectId>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:50:41 +00:00
ClaudeandClaude Opus 5.5 88fb9263f9 Öffnungszeiten: Prüfregeln für Zeiten und Team-Einstellungen
normalisiereZeiten/normalisiereTeam prüfen und normalisieren alles, was Kunde und
Team speichern: Fensterregeln, Ausnahmen, Hinweise, Kontakt, Telefon, Links, Größe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:35:10 +00:00
ClaudeandClaude Opus 5.5 5f52ef54e8 Öffnungszeiten: Ersatz-Tabelle und Wochenplan-Hash
ersatzHtml liefert die schlichte Wochentabelle eines Plans als reines ASCII
(Entities, maskierter Planname), wochenplanHash den 16-stelligen sha1 der
Tage des Hauptplans. Dafuer wird modusUndFenster aus der Zeitlogik exportiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:19:22 +00:00
ClaudeandClaude Opus 5.5 61c2f72631 Öffnungszeiten: Hinweise, JSON-LD, Google-Text und Erinnerungstage
Reine Funktionen in zeitlogik.js: hinweisZeitraum/gueltigeHinweise (Berliner Kalendertage,
Rang, Beginn in 8 Tagen), jsonLdFuer (Mitternachtsfenster, rund 00:00–23:59, Sonderzeiten
der nächsten 60 Tage, neuerBlock mit @context), googleText, erinnerungsTage und
imErinnerungsfenster (08:00–20:00 Berlin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:12:20 +00:00
ClaudeandClaude Opus 5.5 59b0ea799d Öffnungszeiten: Live-Status, Texte und Wechselzeitpunkte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:59:20 +00:00
ClaudeandClaude Opus 5.5 0f25f7cb52 Öffnungszeiten: Tagesbild und Öffnungsintervalle
Reine Zeitlogik in Europe/Berlin: tagesbild (Sondertag > Urlaub > Feiertag > Wochenplan,
planbezogen vor global, später angelegt gewinnt), oeffnungsIntervalle (zusammengefügt,
Fenster über Mitternacht gehören zum Starttag), zuUtcOderDanach (März-Lücke auf 03:00).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:44:28 +00:00
ClaudeandClaude Opus 5.5 2636ce60be Feiertage: Namen und Prüftage im Zeitraum
Neue Exporte feiertagName und feiertageImZeitraum (feiertagejs getHolidays je Jahr,
Namen per translate('de')). Heiligabend, Silvester, Mariä Himmelfahrt in BY und
Fronleichnam in SN/TH (Datum aus RP) sind 'pruefen'; istFeiertag bleibt unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:36:44 +00:00
ClaudeandClaude Opus 5.5 db9f7df747 Öffnungszeiten: Register-Eintrag und Stripe-Katalog
Funktion zeiten in config.features (30 €, aboInklusive, consentFrei, teilweiseLive),
freiweg() und Standard-Haken in featureRegistry.js, Stripe-Katalog zeitenSetup
(Metadaten zeiten-einrichtung) in stripeBilling.js und scripts/stripe-products.mjs,
vorläufige zeitenTicketDetails; Schlüssellisten-Pins um zeiten ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:31:36 +00:00
ClaudeandClaude Opus 5.5 2314f7d940 Öffnungszeiten: Index idx_feature für die Erinnerungsmerker
Task 9 löscht die Merker einer Funktion per featureId; Appwrite verlangt dafür einen Index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:26:30 +00:00
ClaudeandClaude Opus 5.5 4b4cd1058f Öffnungszeiten: Schalter, Collections und Felder
ZEITEN_ENABLED/ZEITEN_PILOT_EMAILS/ZEITEN_KEIN_NEUBAU als config.zeiten (neben config.termine),
zeitenSetup (30 € einmalig), Collection-IDs oeffnungszeiten und zeitenErinnerungen, Schema der beiden
Collections plus fünf neue portalFeatures-Felder, scripts/zeiten-setup.mjs (idempotent, --check),
schemaKeys kennt die neuen Collections und Felder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:25:37 +00:00
ClaudeandClaude Opus 5.5 385f768d76 Öffnungszeiten live: Umsetzungsplan (37 Tasks in Blöcken A bis M)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:19:34 +00:00
ClaudeandClaude Opus 5.5 360d218d4e Öffnungszeiten live: Design (Entscheidungen Z1–Z23 des Inhabers, Prüfung gegen den Code eingearbeitet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 22:52:38 +00:00
672 changed files with 136048 additions and 728 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
node_modules/
node_modules
.env
.DS_Store
preview-data/
+13 -2
View File
@@ -5,6 +5,10 @@ export const CONSENT_KEY = 'wk-consent';
export const CONSENT_MAX_AGE = 183 * 86400000;
export const BANNER_VERSION = '2026-10-02';
export const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
export const BANNER_VERSION_EIGEN = '2026-10-17';
export const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
export const BANNER_LINK_LABEL = 'Datenschutz';
export const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
export const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
@@ -88,13 +92,20 @@ export function contactKind(a, ownHost) {
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
export function readConsent(storage, now) {
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
export function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
export function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
+13 -2
View File
@@ -8,6 +8,10 @@ const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
const BANNER_VERSION = '2026-10-02';
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
const BANNER_VERSION_EIGEN = '2026-10-17';
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
const BANNER_LINK_LABEL = 'Datenschutz';
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
@@ -91,13 +95,20 @@ function contactKind(a, ownHost) {
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
function readConsent(storage, now) {
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
+24 -8
View File
@@ -8,6 +8,10 @@ const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
const BANNER_VERSION = '2026-10-02';
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
const BANNER_VERSION_EIGEN = '2026-10-17';
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
const BANNER_LINK_LABEL = 'Datenschutz';
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
@@ -91,13 +95,20 @@ function contactKind(a, ownHost) {
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
function readConsent(storage, now) {
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
function bannerFor(variant) {
return variant === 'eigen'
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
: { version: BANNER_VERSION, text: BANNER_TEXT };
}
function readConsent(storage, now, version = BANNER_VERSION) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t, bv: c.bv };
@@ -134,6 +145,8 @@ function main() {
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
@@ -173,12 +186,12 @@ function main() {
const dnt = (v) => v === '1' || v === 'yes';
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
const stored = readConsent(storage, Date.now());
const stored = readConsent(storage, Date.now(), banner.version);
let memChoice = null;
let tracking = false;
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
function choiceNow() {
const c = readConsent(storage, Date.now());
const c = readConsent(storage, Date.now(), banner.version);
return c ? c.a : (memChoice ? memChoice.a : null);
}
const counting = () => choiceNow() !== 'reject';
@@ -207,7 +220,10 @@ function main() {
if (navTimer || !counting()) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
sendEn();
sendPv(false);
}), 300);
}
let hooksOn = false;
@@ -312,7 +328,7 @@ function main() {
function choose(a) {
memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: BANNER_VERSION })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
closeBanner();
@@ -352,7 +368,7 @@ function main() {
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
const p = el('p', { class: 't' }, BANNER_TEXT);
const p = el('p', { class: 't' }, banner.text);
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
@@ -433,7 +449,7 @@ function main() {
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now());
const c = readConsent(storage, Date.now(), banner.version);
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
if (c) closeBanner();
}));
+12 -7
View File
@@ -1,7 +1,7 @@
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, CONSENT_KEY, BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, bannerFor, CONSENT_KEY, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
function main() {
const DOC = document;
@@ -15,6 +15,8 @@ function main() {
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
@@ -54,12 +56,12 @@ function main() {
const dnt = (v) => v === '1' || v === 'yes';
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
const stored = readConsent(storage, Date.now());
const stored = readConsent(storage, Date.now(), banner.version);
let memChoice = null;
let tracking = false;
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
function choiceNow() {
const c = readConsent(storage, Date.now());
const c = readConsent(storage, Date.now(), banner.version);
return c ? c.a : (memChoice ? memChoice.a : null);
}
const counting = () => choiceNow() !== 'reject';
@@ -88,7 +90,10 @@ function main() {
if (navTimer || !counting()) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
sendEn();
sendPv(false);
}), 300);
}
let hooksOn = false;
@@ -193,7 +198,7 @@ function main() {
function choose(a) {
memChoice = null;
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: BANNER_VERSION })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
closeBanner();
@@ -233,7 +238,7 @@ function main() {
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
const p = el('p', { class: 't' }, BANNER_TEXT);
const p = el('p', { class: 't' }, banner.text);
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
@@ -314,7 +319,7 @@ function main() {
// Einwilligung in anderem Tab geaendert
WIN.addEventListener('storage', safe(function (e) {
if (e.key !== null && e.key !== CONSENT_KEY) return;
const c = readConsent(storage, Date.now());
const c = readConsent(storage, Date.now(), banner.version);
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
if (c) closeBanner();
}));
+70 -4
View File
@@ -7,7 +7,7 @@ import { dirname, join } from 'node:path';
import {
vpClass, cleanPath, refHost, cssPath, contactKind, readConsent, contrastOk, escapeHtml,
} from '../client/core.js';
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from '../client/core.js';
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL, BANNER_VERSION_EIGEN, BANNER_TEXT_EIGEN, bannerFor } from '../client/core.js';
import { buildAll } from '../client/build.js';
const BV = '2026-10-02';
@@ -170,12 +170,12 @@ function findAll(root, pred) { const out = []; walk(root, (n) => { if (pred(n))
const btn = (root, label) => findAll(root, (n) => n.tagName === 'BUTTON' && n.textContent === label)[0];
const settingsBtn = (h) => findAll(h.body, (n) => n.tagName === 'BUTTON' && n.attrs['aria-label'] === SETTINGS_LABEL)[0];
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad } = {}) {
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad, variant } = {}) {
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
if (stored) store['wk-consent'] = JSON.stringify(stored);
const body = makeEl('body'); if (bodyPad !== undefined) body.style.paddingBottom = bodyPad; const html = makeEl('html');
const timers = [];
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); script.src = src;
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); if (variant !== undefined) script.setAttribute('data-variant', variant); script.src = src;
const docL = {}; const winL = {};
const link = withConsentLink ? makeEl('a') : null; if (link) link.setAttribute('data-webklar-consent', '');
const injected = [];
@@ -565,7 +565,9 @@ test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () =>
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.map((s) => s.d.path), ['/start', '/zwei']);
// en der vorigen Seite vor dem pv der neuen (Router-Links verhindern den Standard-Klick, onLinkClick greift nicht)
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
assert.equal(h.sent[1].d.scroll, 55);
h.ck(target());
assert.equal(h.sent.at(-1).d.path, '/zwei');
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
@@ -641,3 +643,67 @@ test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
g.ck(lnk('/kontakt'));
assert.equal(enCount(g), 0);
});
// ---------- Variante "eigen" (WEBklars eigene Websites, z. B. webklar.com) ----------
test('core: bannerFor und readConsent je Fassung', () => {
assert.deepEqual(bannerFor('eigen'), { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN });
for (const v of ['', 'kunde', 'EIGEN', 'x', undefined]) assert.deepEqual(bannerFor(v), { version: BANNER_VERSION, text: BANNER_TEXT });
assert.equal(BANNER_VERSION_EIGEN, '2026-10-17');
assert.notEqual(BANNER_VERSION_EIGEN, BANNER_VERSION);
const now = 1_700_000_000_000;
const st = (o) => ({ getItem: () => JSON.stringify(o) });
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BV, t: now }), now, BANNER_VERSION_EIGEN), null, 'Kunden-Fassung gilt nicht fuer eigen');
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now, BANNER_VERSION_EIGEN).a, 'accept');
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now), null, 'ohne Fassung gilt die Kunden-Fassung');
});
test('core: Banner-Text eigen (Sie-Form, ohne Auftragsverarbeiter)', () => {
assert.match(BANNER_TEXT_EIGEN, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
assert.match(BANNER_TEXT_EIGEN, /Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung\./);
assert.match(BANNER_TEXT_EIGEN, /Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben\./);
assert.match(BANNER_TEXT_EIGEN, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
assert.doesNotMatch(BANNER_TEXT_EIGEN, /\b(Du|Dein|Deine|Deiner|Dich|Dir)\b/);
assert.doesNotMatch(BANNER_TEXT_EIGEN, /betreibt für uns/);
});
test('w.js: data-variant="eigen" => Sie-Text, eigene Fassung im Speicher', () => {
const h = harness({ variant: 'eigen' });
const host = hostEl(h);
const p = findAll(host.shadow, (n) => n.tagName === 'P')[0];
assert.ok(p.textContent.startsWith(BANNER_TEXT_EIGEN));
assert.ok(!p.textContent.startsWith(BANNER_TEXT));
btn(host.shadow, 'Annehmen').click();
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 });
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
});
test('w.js: eigen fragt neu bei Wahl aus der Kunden-Fassung, gilt mit eigener Fassung', () => {
const alt = harness({ variant: 'eigen', stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
assert.ok(hostEl(alt), 'Banner erscheint erneut');
const ok = harness({ variant: 'eigen', stored: { v: 1, a: 'reject', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 - 1000 } });
assert.equal(hostEl(ok), undefined);
assert.deepEqual(ok.types(), []);
});
test('w.js: unbekannte Variante => Kunden-Fassung', () => {
const h = harness({ variant: 'sonstwas' });
const p = findAll(hostEl(h).shadow, (n) => n.tagName === 'P')[0];
assert.ok(p.textContent.startsWith(BANNER_TEXT));
});
test('w.js: SPA-Wechsel ohne Einwilligung sendet nur pv, kein en', () => {
const h = harness();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'pv']);
});
test('w.js: en nach SPA-Wechsel nur einmal je Seite (Link-Klick und Router)', () => {
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
h.leave();
h.win.location.pathname = '/zwei';
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
h.leave();
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`).slice(-1), ['en:/zwei']);
});
Binary file not shown.
@@ -1 +1 @@
d73389a0cf78380029948d91db022e20d7d2f7182ea1c4fa78478d78c719c938
21d406e6836a7d0c1afdb99dc259e4174fb3fe499afcbdac30c8a45a1aa8d605
+46
View File
@@ -0,0 +1,46 @@
# Buchungskalender: API-Vertrag
Verbindlich für Strang 2 (Widget und Buchungsseite, bauen gegen die öffentliche Schnittstelle) und Strang 3 (Portal, baut gegen die Kunden-API). Task 6 bis 8 erfüllen ihn. Fehlerform überall `{ fehler: code, felder?: {feld: text} }`.
## Livegang-Reihenfolge
Vor dem Anheben von `UNTERKUNFT_ENABLED` über `off` zuerst `scripts/unterkunft-setup.mjs` (legt die Collections samt Index `idx_account` an) und das termine-Setup (Attribut `anhang` der Mail-Warteschlange) auf Produktion laufen lassen.
## Öffentlich (`/api/public/unterkunft/v1`)
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
- `GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD` → `{ belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' }` (Quellen nicht offengelegt). Liegt `bis` hinter `bisMax` (heute + `horizontMonate`), wird es auf `bisMax` gekappt (kein 400); ein Zeitraum mit `bis < von` oder ungültigem Datum ergibt 400 `ungueltig`.
- `POST /:siteKey/anfrage` Body `{ unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url }` → `201 { nummer }`.
- `GET /:siteKey/buchung/:id?t=` → `{ status, nummer, akzentfarbe, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon,datenschutzUrl}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }`.
- `POST /:siteKey/buchung/:id?t=` nach Fristablauf: 410 `{ fehler:'frist_abgelaufen' }`. Body `{ rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart }` → `{ status:'gebucht', rechnungNummer, zahlungUrl? }` (bei `online` direkt Checkout-URL).
- `POST /:siteKey/buchung/:id/zahlung?t=` → `{ url }`.
- `GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno` → PDF.
## Kunde (`/api/unterkunft`, Session, `?projectId=` steht überall in der Query, auch bei POST/PUT)
Antwortformen, die die Portal-Oberfläche erwartet (Geld in Cent, Daten `YYYY-MM-DD`; Fehler überall `{ fehler, felder? }`, nie `tokenHash`/`ipHash`):
- `GET /status` → `{ state, steuerart?, stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto }, onlineZahlung, unterkuenfte:Zahl, rechnungenVorhanden }`; bei `state:'off'` zusätzlich `anfrage:{ status:'offen'|'abgelehnt'|'angenommen', at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } } | null`.
- `GET /einstellungen` → `{ einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon, steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung:boolean, rechnungAuftrag:boolean }, stripe:{ status, vorhandenesShopKonto }, rechnungenVorhanden }`.
- `PUT /einstellungen` Body = genau diese Felder ohne Wrapper. `rechnungAuftrag` (boolean) leitet der Server aus `rechnungAuftragAt` ab: `true` setzt es, falls leer, `false` widerruft (null). `onlineZahlung` nur `true`, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler als `felder.<name>`.
- `GET /unterkuenfte` → `{ unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }] }` (`icalExportUrl` = volle URL mit Token).
- `POST /unterkuenfte`, `PUT /unterkuenfte/:id` Body `{ name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }` → `{ unterkunft }`; Deaktivieren = `PUT` mit `aktiv:false`. `DELETE /unterkuenfte/:id`.
- `GET|PUT /unterkuenfte/:id/preise` → `{ preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }`; PUT sendet genau diese Felder.
- Preismodell „pauschal“ (ohne `personenCent`): `preisProNachtCent` gilt für `personenInklusive` Gäste, jede weitere Person kostet `aufpreisPersonNachtCent` je Nacht (Posten „Zusatzperson“).
- Preismodell „je Personenzahl“: jede Staffel trägt `personenCent:[c1,c2,…,ck]` = Preis pro Nacht für die ganze Unterkunft bei 1, 2, …, k Gästen (k = 2 bis 10, Werte ganze Cent 100 bis 100000). Entweder haben alle Staffeln `personenCent` mit gleicher Länge k oder keine; sonst 400 `felder.staffeln`. Der Server setzt `preisProNachtCent = personenCent[0]` (eine mitgeschickte Zahl wird überschrieben). Eine Staffel gilt weiterhin ab ihrer Nächtezahl für alle Nächte. Der Posten heißt „Übernachtung, 7 Nächte, 3 Personen“; `personenInklusive` wird ignoriert. Bei mehr als k Gästen gilt `ck` plus der Posten „Zusatzperson“ (`aufpreisPersonNachtCent`, nur wenn größer 0) für die Gäste über k. Der Direktrabatt gilt auf Übernachtung und Zusatzpersonen.
- Kein neues Schema: `personenCent` liegt im JSON-Text `ukPreise.staffeln` (2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
- `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{ von, bis (Abreisetag, exklusiv), quelle:'gebucht'|'reserviert'|'ical'|'manuell' (auch 'ical:<importId>', 'anfrage:<id>' zählt als gebucht), anfrageNummer?, sperreId?, notiz? }] }`.
- `POST /unterkuenfte/:id/sperren` `{ von, bis, notiz }` → 201, 409 `zeitraum_belegt`; `DELETE /sperren/:id`.
- `POST /unterkuenfte/:id/ical-importe` `{ name, url }` (nur https; Feldfehler `felder.url`) → 201; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`.
- `GET /anfragen?filter=offen|gebucht|alle` → `{ anfragen:[{ id, nummer, von, bis, naechte, personen, gast:{name}, status, summen:{bruttoCent}|null, unterkunftName }] }`.
- `GET /anfragen/:id` → `{ anfrage:{ id, nummer, status, von, bis, naechte, personen, unterkunftId, unterkunftName, gast:{name,firma,email,telefon,nachricht}, rechnungsadresse|null, posten|null, summen|null, steuerart, zahlart|null, bezahltVia|null, bezahltAt|null, erstattetCent, rechnungId|null, rechnungNummer|null, stornoRechnungId|null, fristAt|null, nachrichtVermieter }, vorschlag:{ posten, summen } }` (`vorschlag` = `berechnePreis`).
- `POST /anfragen/:id/freigeben` `{ posten, fristTage, nachricht }` → `{ anfrage, buchungsLink }`; `POST /anfragen/:id/ablehnen` `{ text }`; `POST /anfragen/:id/bezahlt`; `POST /anfragen/:id/stornieren` `{ grund, erstattungCent }` (502 `erstattung_fehlgeschlagen`, wenn die Stornierung galt, die Erstattung aber scheiterte).
- `POST /preisvorschau` `{ unterkunftId, von, bis, personen }` → `{ naechte, posten, summen:{ bruttoCent } }` (rechnet mit den gespeicherten Preisen).
- `GET /rechnungen` → `{ rechnungen:[{ id, nummer, datum, art:'rechnung'|'storno', gastName, bruttoCent (Storno negativ), anfrageNummer? }] }`; `GET /rechnungen/:id.pdf` (nur Einzel-PDF, kein ZIP, Spec N7).
- `POST /stripe/verbinden` → `{ url }`; `POST /stripe/vorhandenes-konto` und `POST /stripe/aktualisieren` → `{ stripe:{ status } }`.
- `POST /anfrage-funktion` `{ art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges' ('monteur' wird als Alias angenommen), einheiten:1-999, portale:Liste aus 'booking'|'airbnb', nachricht }` → `{ anfrage }`; Feldfehler `felder.art|einheiten|portale|nachricht`; 409, wenn schon eine Anfrage läuft. `POST /anfrage-funktion/zurueckziehen`.
## Team (`/api/team/unterkunft`)
- `GET /:customerId` → `{ projects:[{ projectId, name, state, anfrage:{ status:''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen', at, art:'ferienwohnung'|'monteurzimmer'|'pension'|'sonstiges', einheiten, portale:boolean, nachricht, entschiedenAt, entschiedenVon, begruendung }, stripe, unterkuenfte:Zahl, offeneAnfragen:Zahl }] }` (Hülle wie `GET /api/team/shop/:customerId`).
- `POST /:customerId/anfrage` `{ projectId, entscheidung:'annehmen'|'ablehnen', begruendung }` → `{ ok, anfrage, mailFehler? }`.
@@ -0,0 +1,57 @@
# Mustertext: Datenschutzerklärung der Kundenwebsite (Buchungskalender)
**Muster – keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U12). Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der der Buchungskalender läuft. Der Vermieter ist Verantwortlicher und verantwortet seine Datenschutzerklärung selbst; WEBklar liefert nur diesen Baustein (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 10 und Nr. 6). Bau das Widget erst ein, wenn der Absatz in der Datenschutzerklärung steht.
Hinweise für das Team:
- Liegt die Datenschutzerklärung in der Website, die WEBklar betreut, fügen wir den Absatz beim Einbau ein (gehört zur Einrichtung). Liegt sie woanders, bekommt der Vermieter den Text im Chat und fügt ihn selbst ein.
- Platzhalter in eckigen Klammern durch die Angaben des Vermieters ersetzen. Nicht Zutreffendes streichen: Den Absatz „Zahlung über Stripe“ nur, wenn der Vermieter die Online-Zahlung eingeschaltet hat; den Satz zu anderen Plattformen nur, wenn er Kalender einbindet.
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). Die Nummerierung der Überschrift der Website beachten.
- Wenn der Vermieter im Impressum oder in der Erklärung einen Datenschutzbeauftragten nennt, dort dazuschreiben.
- Die Löschfristen entsprechen dem AVV Buchungskalender Nr. 9 und Abschnitt 6f der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html, nach dem Livegang). Ändert sich dort etwas, hier mitändern. Die Frist für Server-Protokolle (14 Tage) steht in Abschnitt 3 dieser Erklärung.
- Das Widget setzt keine Cookies.
- Im sichtbaren Text der Kundenwebsite die Wünsche des Vermieters beachten (etwa keine Gedankenstriche).
---
## [Nummer]. Anfragen und Buchungen über unseren Buchungskalender
### Verantwortlicher und technischer Betrieb
Verantwortlich für die Verarbeitung Ihrer Daten bei einer Anfrage oder Buchung ist [Name des Vermieters, Anschrift, E-Mail-Adresse], also der Vermieter. Den Buchungskalender betreibt technisch die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag. Sie verarbeitet Ihre Daten nur nach unseren Weisungen und nicht für eigene Zwecke (Auftragsverarbeitung nach Art. 28 DSGVO). Der Server steht bei der Hetzner Online GmbH in Falkenstein (Deutschland); der Versand der E-Mails zu Ihrer Anfrage und Buchung läuft über den E-Mail-Dienst der Hetzner Online GmbH in Nürnberg (Deutschland).
### Welche Daten wir bei einer Anfrage und Buchung verarbeiten
Wenn Sie über den Buchungskalender anfragen, verarbeiten wir die gewünschte Unterkunft, den Zeitraum und die Zahl der Personen, Ihren Namen, Ihre E-Mail-Adresse und, wenn Sie es angeben, Ihre Firma, Ihre Telefonnummer und Ihre Nachricht. Bitte schreiben Sie keine Angaben zu Ihrer Gesundheit oder anderen besonders schutzbedürftigen Umständen in die Nachricht. Die Verarbeitung ist nötig, um Ihre Anfrage zu beantworten und gegebenenfalls einen Vertrag mit Ihnen zu schließen (Art. 6 Abs. 1 lit. b DSGVO). Ohne diese Angaben können wir Ihre Anfrage nicht bearbeiten. Eine Anfrage ist unverbindlich. Geben wir sie frei, erhalten Sie eine E-Mail mit dem Preis, den Stornobedingungen und einem Link zur Buchungsseite; erst dort buchen Sie verbindlich.
Wenn Sie buchen, verarbeiten wir zusätzlich Ihre Rechnungsadresse (Name oder Firma, Straße, Postleitzahl, Ort, Land), die gewählte Zahlart, die Buchung mit ihrem Stand (zum Beispiel gebucht, bezahlt, storniert) und die Rechnung. Die Rechnung wird in unserem Namen und in unserem Auftrag von der WEBklar GbR erstellt (§ 14 Abs. 2 Satz 4 UStG) und Ihnen als PDF mit der Buchungsbestätigung gesendet.
Außerdem speichern wir in der Anfrage einen Hashwert Ihrer IP-Adresse (nicht die IP-Adresse selbst; er wird mit einem geheimen Schlüssel des Servers berechnet, sodass sich daraus keine IP-Adresse ableiten lässt). Wir begrenzen damit die Zahl der Anfragen je IP-Adresse und schützen den Buchungskalender so vor automatisierten Massenanfragen. Das ist unser berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO); Sie können dieser Verarbeitung widersprechen (Art. 21 DSGVO). Wir verwenden den Hashwert nur dafür und entfernen ihn, sobald wir Ihre Anfrage freigeben oder ablehnen, spätestens nach 30 Tagen.
### Zahlung über Stripe
[Nur aufnehmen, wenn die Online-Zahlung eingeschaltet ist.] Wenn Sie online zahlen, werden Sie auf die Zahlungsseite der Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“) weitergeleitet. Die Zahlung geht direkt auf unser Stripe-Konto. Online können Sie derzeit nur per Karte zahlen (auch mit Apple Pay und Google Pay), nicht per SEPA-Lastschrift. Kartendaten geben Sie nur dort ein; wir und der technische Betreiber des Buchungskalenders erhalten sie nicht. Beim Start der Zahlung übergibt der technische Betreiber Stripe die Bezeichnung des Aufenthalts mit der Rechnungsnummer, den Betrag, die Kennungen der Buchung, Ihre E-Mail-Adresse (zur Vorbelegung der Zahlungsseite) und als Rücksprungadresse den geheimen Link zur Buchungsseite; dieser Link ist damit für Stripe und in unserem Stripe-Dashboard sichtbar, gilt aber nur bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur Ihre eine Buchungsseite. Geht eine Zahlung erst nach einer Stornierung oder nach dem Ablauf der Reservierung ein oder zahlen Sie eine bereits bezahlte Buchung ein zweites Mal, wird der Betrag automatisch in voller Höhe an Sie zurückerstattet. Stripe verarbeitet Ihre Daten teils im Rahmen der Zahlungsabwicklung für uns und teils als eigener Verantwortlicher, zum Beispiel zur Betrugsvorbeugung und zur Erfüllung gesetzlicher Pflichten; dafür gilt die Datenschutzerklärung von Stripe (stripe.com/de/privacy). Dabei können Daten an die Stripe, LLC in den USA übermittelt werden. Die Stripe, LLC ist nach dem EU-US Data Privacy Framework zertifiziert, für das ein Angemessenheitsbeschluss der EU-Kommission besteht. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO. Zahlen Sie vor Ort, entfällt dieser Absatz für Sie.
### E-Mails zu Ihrer Anfrage und Buchung
Zu Ihrer Anfrage und Buchung senden wir Ihnen E-Mails an die angegebene E-Mail-Adresse: eine Bestätigung, dass Ihre Anfrage eingegangen ist, die Freigabe mit dem Link zur Buchungsseite oder die Absage, die Buchungsbestätigung mit den Vertragsangaben, den Stornobedingungen und der Rechnung, die Bestätigung eines Zahlungseingangs, einen Hinweis, wenn eine Reservierung abgelaufen ist, und bei einer Stornierung die Stornorechnung. Das sind Mitteilungen zur Abwicklung Ihres Vertrags (Art. 6 Abs. 1 lit. b DSGVO), keine Werbung. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand wird Empfängeradresse und Inhalt dort gelöscht, der Eintrag selbst nach sieben Tagen. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern und das bei einer Online-Zahlung an Stripe übergeben wird (siehe oben); behandeln Sie ihn vertraulich.
### Besuch der Seite
Beim Aufruf der Seite lädt Ihr Browser das Programm des Buchungskalenders und die Belegung von der Adresse project.webklar.com. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und für 14 Tage in Server-Protokollen gespeichert, um den sicheren und störungsfreien Betrieb zu gewährleisten (Art. 6 Abs. 1 lit. f DSGVO). Der Buchungskalender setzt keine Cookies. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe werden nur im Arbeitsspeicher des Servers gehalten. Mit Ihrer Anfrage wird Ihre IP-Adresse nicht gespeichert, sondern nur der oben beschriebene Hashwert.
### Abgleich mit anderen Plattformen
[Nur aufnehmen, wenn der Vermieter Kalender einbindet.] Wir gleichen die Belegung unserer Unterkünfte mit [Booking.com, Airbnb, …] über Kalenderdateien (iCal) ab, damit eine Unterkunft nicht doppelt vergeben wird. Dabei werden nur belegte Zeiträume ausgetauscht, keine Namen oder anderen Angaben zu Gästen.
### Weitergabe
Wir geben Ihre Daten nur weiter, soweit das für die Vertragsabwicklung nötig ist (technischer Betreiber des Buchungskalenders als Auftragsverarbeiter, Stripe bei Online-Zahlung) oder wir gesetzlich dazu verpflichtet sind (zum Beispiel an das Finanzamt oder, soweit vorgeschrieben, an die Meldebehörde). Eine Weitergabe zu Werbezwecken findet nicht statt.
### Speicherdauer
Anfragen, aus denen keine Buchung wird, anonymisiert das System nach sechs Monaten. Für gebuchte Aufenthalte sind wir als Vermieter verpflichtet, Rechnungen und Buchungsbelege nach Handels- und Steuerrecht aufzubewahren (in der Regel acht bis zehn Jahre, § 257 HGB, § 147 AO; Art. 6 Abs. 1 lit. c DSGVO); dafür bewahrt das System die Rechnungen und die Rechnungsadresse acht Jahre auf, solange der Buchungskalender läuft. Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht eines gebuchten Aufenthalts werden drei Jahre nach dem Ende des Jahres Ihrer Abreise anonymisiert.
### Ihre Rechte
Sie haben das Recht auf Auskunft über Ihre Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20) und auf Widerspruch gegen Verarbeitungen, die auf Art. 6 Abs. 1 lit. f DSGVO beruhen (Art. 21). Wenden Sie sich dazu an uns unter [E-Mail-Adresse des Vermieters]; wir leiten Ihre Anfrage, soweit nötig, an den technischen Betreiber des Buchungskalenders weiter. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, etwa bei der für uns zuständigen Behörde [zuständige Aufsichtsbehörde des Vermieters].
+91
View File
@@ -0,0 +1,91 @@
# Buchungskalender auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. Das Widget lässt Besucher auf der Website des Kunden („Vermieter“) einen Zeitraum im Kalender wählen und eine unverbindliche Anfrage senden. Unterkünfte, Preise, Anfragen, Rechnungen und die Verbindung zu Stripe pflegt der Vermieter im Kundenportal (Tab „Funktionen“, Kachel „Buchungskalender“, Reiter „Buchungen“). Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im Einrichtungs-Ticket und im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“, Zeile Buchungskalender. Im Ticket stehen Snippet und Schritte schon fertig; dieses Dokument ist die Referenz dahinter. Ohne aktive Funktion oder ohne freigeschaltete Unterkunft ist nichts buchbar: Das Widget zeigt dann den Ausweichtext (Abschnitt 4). API-Vertrag: `docs/funktionen/unterkunft-api.md`.
Der Buchungskalender ist an den Vertrag über den Betrieb der Website gekoppelt (Leistungsbeschreibung Buchungskalender Nr. 10), aber **nicht in der Software**: Das Portal schaltet ihn nicht von selbst ab. Das Abschalten zum Vertragsende machen wir von Hand (Abschnitt 8).
## 1. Das Snippet
```html
<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com">
<p>Der Buchungskalender ist gerade nicht erreichbar. Bitte schreiben Sie uns: info@beispiel.de</p> <!-- Ausweichtext, Abschnitt 4 -->
</div>
<script src="https://project.webklar.com/embed/unterkunft.js" defer></script>
```
Das Skript steht einmal pro Seite, vor `</body>`. Es hat keine Abhängigkeiten, setzt keine Cookies, lädt nichts von Dritten und fragt nur `project.webklar.com` an. Texte des Vermieters setzt das Skript nur als Text ein, nie als HTML. Das Widget rechnet nie selbst: Preise, Summen und belegte Nächte kommen fertig vom Server (die Anfrage ist unverbindlich; der Preis wird erst bei der Freigabe festgelegt).
| Attribut | Pflicht | Bedeutung |
| --- | --- | --- |
| `data-webklar-unterkunft` | ja | der `siteKey` des Projekts |
| `data-api` | nein | Herkunft des Portals, Standard `https://project.webklar.com`. Bei Modul-Import, Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht. |
## 2. Wohin damit
1. Auf der Seite einen **Bereich „Anfrage“** (Anker `#anfrage` oder eine eigene Seite) anlegen und das Snippet an die Stelle des Formulars setzen. Ein bisheriges Anfrageformular wird ersetzt; Telefon und E-Mail des Vermieters stehen unter dem Kalender. Das `div` braucht Breite (normaler Blockcontainer; in einem Flex- oder Grid-Element ohne Breite schrumpft es auf null).
2. Layout: Das Widget zeigt zwei Spalten (Kalender links, Formular rechts) und unter 760 px Breite beide untereinander; der Kalender zeigt einen Monat, ab 1100 px Breite zwei.
3. Menüpunkt oder Knopf „Anfragen“ auf den Bereich verlinken.
4. **Mehrere Widgets** auf einer Seite sind nicht vorgesehen. Der Buchungskalender ist ein Widget je Website; mehrere Unterkünfte wählt der Gast im Widget (bei nur einer Unterkunft entfällt die Auswahl).
Aussehen: Das Widget nimmt die Akzentfarbe aus den Einstellungen des Vermieters. Die Stile stehen in `@layer wku`, jede Regel der Kundenseite gewinnt also. Anpassen per CSS-Variablen auf dem `div`, in der Seiten-CSS oder auf `:root`, zum Beispiel `[data-webklar-unterkunft]{ --wku-akzent:#8a5a2b; --wku-akzent-text:#fff; }`:
| Variable | Standard |
| --- | --- |
| `--wku-akzent` und `--wku-akzent-text` (Auswahl, Knöpfe; setze beide zusammen) | Akzentfarbe des Vermieters, sonst Standardblau und `#fff` |
| `--wku-hintergrund` | `#fff` |
| `--wku-text` | `#1f2937` |
| `--wku-rand` | `#858f9e` |
| `--wku-radius` | `10px` |
| `--wku-schrift`, `--wku-schrift-titel` | Schrift der Seite |
Kontrast (WCAG 2.1 AA): Text auf dem Hintergrund und Schrift auf dem Akzent mindestens 4,5 : 1, Rand gegen den Hintergrund mindestens 3 : 1. Prüfe beim Abnehmen, ob globale Regeln des Themes (`button{…}`, `input{…}`) das Widget stören, und überschreibe sie gezielt (`[data-webklar-unterkunft] .wku-btn{ … }`).
## 3. Datenschutz-Absatz (Pflicht vor dem Einbau)
Bau das Widget erst ein, wenn der Absatz aus `docs/funktionen/unterkunft-datenschutz-absatz.md` in der Datenschutzerklärung der Website steht. Liegt sie in der Website, die WEBklar betreut, fügst Du ihn ein (gehört zur Einrichtung), Platzhalter und nicht Zutreffendes angepasst. Liegt sie woanders, schickst Du ihn dem Vermieter im Chat (Leistungsbeschreibung Nr. 5 Abs. 10). Dasselbe gilt für Impressum und Verlinkung der Datenschutzerklärung: Die Adresse der Datenschutzerklärung trägt der Vermieter in den Einstellungen ein; das Widget und die Buchungsseite verlinken sie.
## 4. Ausweichtext
Der Inhalt des `div` bleibt stehen, solange das Widget nicht lädt oder die Funktion nicht aktiv ist (zum Beispiel vor der Freischaltung, bei `UNTERKUNFT_ENABLED=off`, bei Wartung). Schreib dort Kontaktdaten des Vermieters hinein, damit niemand ohne Möglichkeit zur Anfrage bleibt. Ein aktives Widget ersetzt den Inhalt.
## 5. Vor der Abnahme prüfen
- Im Portal: mindestens eine aktive Unterkunft mit Preisen und Stornobedingungen, Firmendaten vollständig, Steuernummer oder USt-ID, Steuerart gewählt. Sonst ist die Funktion nicht freischaltbar.
- Der Datenschutz-Absatz steht in der Datenschutzerklärung.
- Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer.
- Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status.
- Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2).
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K36 beim Vermieter.
## 6. Vorschau und Testanfrage
1. Vorschau-Subdomain des Projekts (`<subdomain>.project.webklar.com`) ansehen.
2. Anfrage senden, im Portal unter „Buchungen → Anfragen“ öffnen, Preis prüfen, **Freigeben** (E-Mail G17 mit Buchungslink).
3. Buchungslink öffnen: Rechnungsadresse eintragen, Zahlart „vor Ort“ wählen, **„Zahlungspflichtig buchen“**. E-Mail G19 mit Rechnungs-PDF prüfen (Rechnung trägt Name und Daten des Vermieters, keinen WEBklar-Hinweis).
4. Online-Zahlung nur testen, wenn das Stripe-Konto verbunden ist (kleiner Betrag, danach im Portal stornieren und erstatten).
5. Im Portal „bezahlt (vor Ort)“ setzen, dann stornieren (Stornorechnung, E-Mail G22).
6. Testanfragen danach im Portal stornieren oder ablehnen; Sperren des Tests im Kalender entfernen.
## 7. Was das Widget zeigt (Kurzüberblick)
- Kalender mit belegten Nächten (durchgestrichen); vergangene Tage und Zeiträume über eine belegte Nacht hinweg sind nicht wählbar. Anreise am Abreisetag eines anderen Gastes ist möglich.
- Zusammenfassung des Zeitraums („12.–30. Nov. · 18 Nächte“), Personenzahl, Name, E-Mail, Telefon und Nachricht (optional), Firma (optional), Datenschutzhinweis.
- Schaltfläche „Anfrage senden“ (unverbindlich). Danach die Anfragenummer und der Hinweis, dass der Vermieter sich meldet.
- Die Buchung selbst läuft nicht im Widget, sondern auf der Buchungsseite bei WEBklar (Link in der E-Mail G17).
## 8. Wenn der Vertrag über die Website endet (Abschalten von Hand)
Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt (Leistungsbeschreibung Nr. 10). Die Software prüft das **nicht**. Du musst es selbst im Blick haben, wenn ein solcher Vertrag endet, gekündigt oder widerrufen wird:
1. **Vorher** schreibst Du dem Vermieter eine E-Mail (oder Chat): Der Vertrag endet am TT.MM.JJJJ, damit endet auch der Buchungskalender. Er soll **seine Rechnungen als PDF herunterladen** (WEBklar löscht sie 90 Tage nach dem Abschalten), offene Anfragen bearbeiten und seine Gäste informieren.
2. **Zum Vertragsende** im Ticketsystem (WOMS): Kunde → Reiter „Funktionen“ → Buchungskalender → **„Abschalten“**. Danach nimmt das Widget keine Anfragen mehr an und zeigt den Ausweichtext; Reservierungen laufen aus.
3. Der Vermieter kann Anfragen, Buchungen und Rechnungen danach **90 Tage** lang im Portal lesen und die Rechnungen herunterladen. Danach löscht der tägliche Job automatisch alle Daten des Buchungskalenders; das Einrichtungs-Ticket bleibt als Arbeitsnachweis ohne Gästedaten.
4. Nimm nach den 90 Tagen das Snippet und den Menüpunkt aus der Website.
5. Beginnt **innerhalb der 90 Tage** wieder ein Vertrag über den Betrieb der Website, schaltest Du den Buchungskalender auf Wunsch des Vermieters in Textform ohne weitere Kosten wieder ein (WOMS → Funktionen → Buchungskalender → Aktivieren).
6. **Erstattung (Leistungsbeschreibung Nr. 10 Abs. 6):** Endet der Vertrag, bevor wir den Buchungskalender freigeschaltet haben, erstatten wir den gezahlten Einrichtungspreis (199 €) vollständig, in Stripe von Hand. Sonst gibt es keine Erstattung.
Das Stripe-Konto des Vermieters bleibt in jedem Fall beim Vermieter; wir rühren es nicht an.
## 9. Kalenderabgleich (iCal) einrichten
Im Portal unter Buchungen → Unterkünfte & Preise → Kalenderabgleich: Der Vermieter trägt die iCal-Adresse von Booking.com oder Airbnb als Import ein (Name und `https`-Adresse) und kopiert die **Export-Adresse** des Buchungskalenders zu Booking.com („Kalender synchronisieren“) oder Airbnb. Der Abgleich ist zeitversetzt (Minuten bis Stunden, Leistungsbeschreibung Nr. 4); sag das dem Vermieter, damit er vor jeder Freigabe seine anderen Plattformen prüft.
+125
View File
@@ -0,0 +1,125 @@
# Buchungskalender – Livegang
Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon hat Claude gegen Produktion, das Live-Stripe-Konto oder die Website Donau Home ausgeführt. Code: Portal `feature/unterkunft`, WOMS `feature/unterkunft`. Erste Kundin ist Mimi (Donau Home, MIMI FEWO, Website `WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
**Wichtig beim Pushen:** Den Feature-Zweig nie unter seinem eigenen Namen pushen. Der Portal-Webhook deployt jeden Zweig. Immer mit Zielzweig:
- Portal: `git push origin feature/unterkunft:main`
- WOMS: `git push origin feature/unterkunft:test`
## 0. Voraussetzung
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K36 bis K35, G16 bis G22, T23 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
- Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6).
## 1. Inhaber prüft die Texte
Vom Inhaber freizugeben, bevor irgendetwas live geht:
- `docs/rechtliches/leistungsbeschreibung-unterkunft.md` und `avv-unterkunft.md` (beide „Entwurf – Prüfung durch Kanzlei offen“),
- `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md`,
- `docs/funktionen/unterkunft-datenschutz-absatz.md` und `unterkunft-storno-muster.md`,
- `docs/rechtliches/unterkunft-kanzlei-fragen.md` (Fragen U1 bis U12 für die Kanzlei).
**Achtung Widerrufsbelehrung:** Die Datei `widerrufsbelehrung-funktionen-2026-10-07.*` ist eine Kopie des gemeinsamen Textes der Funktionen (Quelle `docs/rechtliches/widerrufsbelehrung-funktionen.md`). Ändert sich dieser Text (neue Fassung beim Terminplaner oder Webshop), braucht der Buchungskalender eine neue Fassung mit neuem Datum; eine ausgelieferte Datei wird nie an Ort und Stelle neu gebaut.
Ändert sich ein Text, gibt es eine neue Fassung (neues Datum in `config.unterkunft.fassung` und im Zustimmungs-Pin der Bestelltexte); ausgelieferte Fassungen werden nie überschrieben.
## 2. Datenbank (nach dem Portal-Deploy, vor dem Pilot)
**Zeitpunkt:** Die Skripte laufen mit dem NEUEN Code, also erst nach dem Portal-Deploy (Schritt 5) und vor dem Einschalten des Piloten (`UNTERKUNFT_ENABLED=pilot`, Schritt 4 Teil 2), oder vorher aus dem Worktree `dev-unterkunft` (`node scripts/…` mit denselben Umgebungswerten gegen die Produktionsdatenbank). Der Schalter bleibt bis zum grünen `--check` auf `off`. Reihenfolge der Skripte: erst `termine-setup.mjs --check` und anwenden (Attribut `anhang`), dann `unterkunft-setup.mjs`:
```bash
cd /home/webklar/apps/kundenbereich.webklar.com/build
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs --check
docker exec -w /app webklar-kundenbereich-server node scripts/termine-setup.mjs # zieht das Attribut anhang der Mail-Warteschlange nach (Rechnung als Anhang)
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs --check
docker exec -w /app webklar-kundenbereich-server node scripts/unterkunft-setup.mjs # legt ukUnterkuenfte, ukPreise, ukSperren, ukAnfragen, ukRechnungen, ukEinstellungen samt Indizes an
```
Wichtige Indizes (ohne sie sind Suche, Eindeutigkeit und Abfragen unsicher oder langsam; das Skript legt sie an, `--check` meldet fehlende): `idx_account` (ukEinstellungen, stripeAccountId), `idx_export_token` (ukUnterkuenfte, icalExportToken), `uniq_feature_nummer` (ukRechnungen, featureId+nummer, **unique**: verhindert doppelte Rechnungsnummern), `idx_bis` (ukSperren und ukAnfragen, Belegungsabfrage). Fehlt `termineOutbox.anhang`, meldet `unterkunft-setup.mjs --check` es ebenfalls (Exit 1) und der normale Lauf legt es an.
Die Skripte müssen aus dem neuen Code laufen (siehe Zeitpunkt oben). `termine-setup.mjs` ändert nichts an vorhandenen Daten, es fügt nur fehlende Attribute und Indizes hinzu.
## 3. Stripe-Preis (ebenfalls nach dem Portal-Deploy oder aus dem Worktree, vor dem Pilot)
```bash
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --anlegen # legt unterkunftSetup (199 €, Lookup-Key webklar_unterkunft_einrichtung_19900) an
docker exec -w /app webklar-kundenbereich-server node scripts/stripe-products.mjs --pruefen # Preis-Lookup nach --anlegen prüfen
```
Der vorhandene Connect-Webhook (`https://project.webklar.com/api/shop/stripe-webhook`) bleibt unverändert; er leitet nach dem Metadatum `wkUk` an den Buchungskalender weiter. Kontrolle im Stripe-Dashboard: Die Ereignisse `account.updated`, `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `checkout.session.expired` und `charge.refunded` sind schon eingetragen (Webshop-Livegang).
## 4. Server-Konfiguration (Pilot)
Sicherungen zuerst: `cp /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-2026-10-07-unterkunft` und `(umask 077; cp /home/webklar/apps/.env /home/webklar/apps/.env.bak-2026-10-07-unterkunft)` (`.env` enthält Zugangsdaten).
**Teil 1 (vor dem Push):** In `/home/webklar/apps/.env` zunächst nur
```
UNTERKUNFT_ENABLED=off
UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimis Portal-Adresse>
```
In `/home/webklar/apps/docker-compose.yml` beim Dienst `kundenbereich-server` neben `ZEITEN_ENABLED` und `SHOP_ENABLED` zwei Zeilen:
```
- UNTERKUNFT_ENABLED=${UNTERKUNFT_ENABLED:-off}
- UNTERKUNFT_PILOT_EMAILS=${UNTERKUNFT_PILOT_EMAILS:-}
```
**Teil 2 (nach Push, grünem Portal-Deploy und Schritt 2/3):** `UNTERKUNFT_ENABLED=pilot` setzen und Compose neu anwenden (derselbe Befehl).
Prüfen mit `docker compose -f /home/webklar/apps/docker-compose.yml config -q` (keine Ausgabe = gültig), dann übernehmen mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Ein bloßes `docker restart` liest neue Umgebungswerte nicht. Das alles vor dem Push (Schritt 5): Der Container läuft dann mit dem alten Code und den neuen Werten (die kennt er nicht, harmlos), und der Neustart durch den Deploy nach dem Push ist der einzige mit dem neuen Code. Rückweg: `UNTERKUNFT_ENABLED=off`, Compose neu anwenden; das Schema ist rein additiv, es gibt nichts zurückzubauen.
## 5. Push (durch den Inhaber)
- Portal: `git push origin feature/unterkunft:main` (der Webhook deployt),
- WOMS: `git push origin feature/unterkunft:test` **erst nach grünem Portal-Deploy** (das WOMS ruft die neue Portal-Schnittstelle, z. B. das Feld `unterkunft` der Funktionen-Antwort, auf).
Nie `git push origin feature/unterkunft` (eigener Name). Danach (Portal) prüfen: `https://project.webklar.com/embed/unterkunft.js` antwortet 200; `https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html` und `…/avv-unterkunft.html` antworten 200.
## 6. Rechtstexte in WOMS „Rechtliches“ hochladen (macht Claude)
Das macht Claude selbst (Wunsch des Inhabers). Dafür legt der Inhaber vorher den **kurzlebigen** Appwrite-Schlüssel mit dem Recht für Rechtliches unter `/root/.appwrite-rechtliches.key` ab und löscht ihn danach. Claude legt (falls nötig, `scripts/setup-legal.mjs` im WOMS) die Karten „Leistungsbeschreibung Buchungskalender“ und „AVV Buchungskalender“ an und lädt die Fassung 2026-10-07 hoch:
- `public/rechtliches/leistungsbeschreibung-unterkunft-2026-10-07.pdf`
- `public/rechtliches/avv-unterkunft-2026-10-07.pdf`
(Die Widerrufsbelehrung `widerrufsbelehrung-funktionen-2026-10-07.pdf` ist ein Anhang der Bestellmail und der Bestellseite; sie ist inhaltlich die der Funktionen.)
## 7. Mimi freischalten und Donau Home einbauen
**Für Mimi ist der Buchungskalender kostenlos** (Entscheidung des Inhabers, 2026-10-07): keine Funktions-Anfrage, keine Bestellung und keine Zahlung der 199 €, kein Stripe-Checkout für die Einrichtung; freigeschaltet wird nur über „Aktivieren (ohne Zahlung)“. In der Textform-Nachricht an Mimi ausdrücklich schreiben, dass die Einrichtung für sie kostenlos ist (abweichend vom Preis in der Leistungsbeschreibung, Abschnitt „9. Preis, Abrechnung und Widerruf“ Abs. 1), damit keine Zahlungspflicht entsteht.
0. **Vorher:** die Zustimmung von Mimi in Textform einholen (Chat im Kundenportal oder E-Mail: Leistungsbeschreibung und AVV Buchungskalender in der Fassung 2026-10-07). Datum und Weg trägt das Team beim Aktivieren im WOMS ein; ohne beides schaltet das Portal nicht ein. Das Portal speichert den Nachweis und schickt Mimi die Unterlagen als PDF (K16).
1. WOMS → Kunde (Mimi) → Funktionen → Buchungskalender → **„Aktivieren (ohne Zahlung)“** (`action: 'activate'`; der Weg für Kunden, die die Funktion als Teil ihres Projekts bekommen; verlangt Datum und Weg der Zustimmung in Textform, auch im Pilot).
2. Mimi richtet im Portal ein: Unterkünfte, Preise, Stornobedingungen (Muster `docs/funktionen/unterkunft-storno-muster.md`), Firmendaten, Steuerart, optional Stripe; mit dem Team zusammen.
3. **siteKey in Donau Home:** den `siteKey` aus dem Ticket oder dem WOMS in `/root/reports/donau-home-website/index.html` im Abschnitt `#anfrage` einsetzen (`data-webklar-unterkunft="SITEKEY"`), das Skript `https://project.webklar.com/embed/unterkunft.js` einbinden, CSS-Variablen (`--wku-*`) auf Palette A (Eiche & Leinen) und Schrift Figtree stellen, den Datenschutz-Absatz in die Datenschutzerklärung der Website einfügen, den Einleitungstext anpassen. Einbau nach `docs/funktionen/unterkunft-einbau.md`.
4. Donau Home pushen (Inhaber, nicht Claude); die Vorschau zeigt das Widget, solange `UNTERKUNFT_ENABLED=pilot` mit Mimis siteKey läuft.
## 8. Pilot mit Mimi
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K36 prüfen.
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K37).
3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis.
4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung).
5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen.
## 9. Datenschutz § 6f übernehmen (VOR dem Pilotstart, also vor Schritt 8)
Den Abschnitt 6f aus `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md` in `public/datenschutz.html` übernehmen (hinter 6d, vor Abschnitt 7; Abschnitt 12 anpassen), danach `FORCE=1 scripts/build-legal-pdfs.sh datenschutz`. Erst mit der Freigabe des Inhabers, aber vor dem ersten Gast-Test mit Mimi: Die Anfrageformulare verweisen auf die Datenschutzerklärung.
## 10. Freigabe für alle
- Kanzlei-Fragen (`unterkunft-kanzlei-fragen.md`) beantworten lassen, Befund einarbeiten (neue Fassung, falls sich Texte ändern; neue Texte hochladen wie in Schritt 6).
- Dann `UNTERKUNFT_ENABLED=on` in `.env`, Compose neu anwenden (wie Schritt 4).
## PDFs der Rechtstexte erzeugen
Die PDFs entstehen aus den HTML-Seiten auf dem Host mit Chrome (Memory prüfen, `free -m` > 1500 MB verfügbar):
```bash
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft # schreibt die HTML-Seiten
node --max-old-space-size=512 scripts/build-legal-html.mjs unterkunft --check # Exit 0 = ok
nice scripts/build-legal-pdfs.sh unterkunft # druckt die PDFs samt .sha256
```
Änderst Du einen Text, HTML neu erzeugen und die PDFs neu drucken (Prüfsumme entscheidet, was neu gedruckt wird; `FORCE=1` druckt alle neu).
@@ -0,0 +1,36 @@
# Muster: Stornobedingungen für den Buchungskalender
**Vorlage, keine Rechtsberatung.** Entwurf, Prüfung durch Kanzlei offen (`docs/rechtliches/unterkunft-kanzlei-fragen.md`, Frage U11). Der Vermieter ist für seine Stornobedingungen allein verantwortlich (Leistungsbeschreibung Buchungskalender Nr. 5 Abs. 3 und Nr. 6). Das Portal bietet diesen Text in den Einstellungen („Mustertext Stornobedingungen zum Übernehmen“) an; der Vermieter prüft ihn, passt ihn an seinen Betrieb an und kann ihn von einer Rechtsanwältin oder einem Rechtsanwalt prüfen lassen. Ohne Stornobedingungen kann der Buchungskalender nicht freigeschaltet werden.
Hinweise für das Team:
- Die Bedingungen werden bei der Freigabe einer Anfrage als Schnappschuss in der Anfrage gespeichert; die Buchungsseite und die Buchungsbestätigung zeigen genau diese Fassung. Eine spätere Änderung gilt nur für neue Freigaben.
- Der Buchungskalender erstattet eine Online-Zahlung nur auf Anweisung des Vermieters (Stornieren mit Erstattungsbetrag 0 bis Gesamtbetrag); die Staffel unten ist Text, die Software wendet sie nicht von selbst an. Der Vermieter trägt den Betrag beim Stornieren selbst ein.
- Storniert der Vermieter, schließt der Buchungskalender eine noch offene Zahlungsseite bei Stripe. Geht trotzdem eine Zahlung nach der Stornierung oder dem Ablauf der Reservierung ein, und bei einer zweiten Zahlung auf eine bezahlte Buchung, erstattet WEBklar sie automatisch in voller Höhe über das Stripe-Konto des Vermieters (dauerhafte Weisung, der Stand der Buchung bleibt). Das ist keine Erstattung nach Nr. 3 des Musters, sondern die Rückgabe einer irrtümlichen Zahlung.
- Zahlt der Gast vor Ort, entfällt eine Erstattung; die Stornierung stellt die Stornorechnung aus.
- Gäste können nicht selbst stornieren: Sie schreiben dem Vermieter. Der Text sagt das.
- Prozentsätze und Fristen sind Platzhalter und Beispiele; der Vermieter wählt eigene Werte. Pauschalen müssen den üblichen Schaden angemessen berücksichtigen (§ 309 Nr. 5 BGB); bei Verbrauchern sind überhöhte Pauschalen unwirksam.
---
## Stornobedingungen (Muster)
**1. Stornierung durch den Gast.** Eine Stornierung der Buchung ist nur in Textform möglich (E-Mail an [E-Mail-Adresse des Vermieters]) und wird erst mit unserer Bestätigung wirksam; wir stornieren die Buchung in unserem System und senden Ihnen eine Stornorechnung. Maßgeblich für die Frist ist der Eingang Ihrer Nachricht bei uns.
**2. Kosten der Stornierung.** Bei einer Stornierung berechnen wir folgenden Anteil des Gesamtpreises (ohne Endreinigung, soweit der Aufenthalt nicht angetreten wird):
- bis [30] Tage vor der Anreise: kostenfrei,
- [29] bis [14] Tage vor der Anreise: [30] Prozent,
- [13] bis [7] Tage vor der Anreise: [60] Prozent,
- ab [6] Tage vor der Anreise und bei Nichtanreise: [90] Prozent.
Ihnen bleibt der Nachweis gestattet, dass uns kein oder ein geringerer Schaden entstanden ist. Uns bleibt unser Anspruch unberührt, wenn wir die Unterkunft anderweitig vermieten können: In diesem Fall berechnen wir nur den tatsächlichen Ausfall.
**3. Erstattung.** Haben Sie bereits online bezahlt, erstatten wir den Teil des Preises, der nach Nr. 2 nicht anfällt, auf das Zahlungsmittel, mit dem Sie bezahlt haben. Die Erstattung geht über unser Stripe-Konto; die Gutschrift auf Ihrem Konto kann einige Tage dauern. Haben Sie noch nicht bezahlt, zahlen Sie den nach Nr. 2 anfallenden Betrag vor Ort oder nach Rechnung.
**4. Verspätete Anreise und vorzeitige Abreise.** Reisen Sie später an oder früher ab, besteht kein Anspruch auf Erstattung des Preises für die nicht genutzten Nächte, es sei denn, wir können die Unterkunft anderweitig vermieten.
**5. Stornierung durch uns.** Können wir die Unterkunft aus einem Grund, den wir zu vertreten haben, nicht bereitstellen, stornieren wir die Buchung, bieten nach Möglichkeit eine gleichwertige Unterkunft an und erstatten bereits gezahlte Beträge vollständig.
**6. Kein Widerrufsrecht.** Für die Buchung zu einem bestimmten Zeitraum besteht kein gesetzliches Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Diese Stornobedingungen gelten stattdessen.
**7. Reiserücktrittsversicherung.** Wir empfehlen Ihnen eine Reiserücktrittsversicherung.
+28 -3
View File
@@ -1,6 +1,6 @@
# Mustertext: Datenschutzerklärung der Kundenwebsite (News-Pinnwand)
# Mustertext: Datenschutzerklärung der Kundenwebsite (News-Pinnwand, Öffnungszeiten live)
Entwurf, Freigabe Nutzer ausstehend. Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der die News-Pinnwand läuft. Bau die Pinnwand erst ein, wenn er dort steht.
Entwurf, Freigabe Nutzer ausstehend. Der Absatz gehört in die Datenschutzerklärung jeder Kundenwebsite, auf der die News-Pinnwand läuft. Bau die Pinnwand erst ein, wenn er dort steht. Dasselbe gilt für „Öffnungszeiten live“: Der Absatz „Öffnungszeiten auf dieser Website“ (je Variante unten) gehört in die Erklärung jeder Website mit dieser Funktion; bau die Funktion erst ein, wenn er dort steht (Einbau-Anleitung: `docs/funktionen/zeiten-einbau.md`).
Hinweise für das Team:
@@ -8,7 +8,8 @@ Hinweise für das Team:
- Im sichtbaren Text der Kundenwebsite keine Gedankenstriche (Wunsch von Emma's Schatzkiste). Den Text unverändert übernehmen oder ohne Gedankenstriche anpassen.
- Anrede und Ton an die übrige Erklärung der Website anpassen (meist Sie). `[Überschrift]`-Nummerierung der Website beachten.
- Die Löschfrist von 14 Tagen für Server-Protokolle entspricht Abschnitt 3 der Datenschutzerklärung von WEBklar (project.webklar.com/datenschutz.html). Ändert sich dort die Frist, hier mitändern.
- Beide Varianten setzen voraus, dass das Skript keine Cookies setzt und nichts im Browser speichert. Das gilt für `news.js` (nur lesende Abrufe).
- Beide Varianten setzen voraus, dass das Skript keine Cookies setzt und nichts im Browser speichert. Das gilt für `news.js` (nur lesende Abrufe) und für `zeiten.js` (ein Abruf je Seite, keine Anfragen an Dritte).
- „Öffnungszeiten auf dieser Website“: den Absatz und nur die Bausteine übernehmen, die die Seite wirklich zeigt. Der Baustein „Route“ gehört dazu, wenn die Kontakt-Leiste den Knopf „Route“ hat, der Baustein „WhatsApp“, wenn sie den Knopf „WhatsApp“ hat. Der WhatsApp-Baustein ist der „WhatsApp-Hinweis“, den der Kunde im Portal mit einem Haken bestätigt, bevor die WhatsApp-Schaltfläche erscheint. Beide Dienste öffnen sich erst nach dem Klick des Besuchers. Anschriften von Google und WhatsApp vor der Übernahme prüfen. Auch hier gilt der Entwurfsstatus: erst nach der Freigabe durch den Inhaber (Rechtsrunde „Öffnungszeiten live“) verwenden.
## Variante A: Website wird von WEBklar gehostet
@@ -16,8 +17,32 @@ Hinweise für das Team:
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Beim Aufruf der Seite lädt Ihr Browser diese Beiträge und Bilder von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Beiträge auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Informationen und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht.
### Öffnungszeiten auf dieser Website
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Beim Aufruf der Seite lädt Ihr Browser ein kleines Programm und unsere Öffnungszeiten von dem Server, den die WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, in unserem Auftrag betreibt und der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir brauchen diese Daten, um die Öffnungszeiten auszuliefern und den Betrieb sicher zu halten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Darstellung aktueller Öffnungszeiten und im sicheren, funktionsfähigen Betrieb der Website. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode.
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
## Variante B: Website liegt bei einem anderen Anbieter
### Neuigkeiten auf dieser Website
Auf dieser Website zeigen wir unter „Neuigkeiten“ kurze Beiträge mit Text und Fotos. Diese Beiträge und Bilder lädt Ihr Browser beim Aufruf der Seite von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Die Pinnwand setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit wir Ihnen aktuelle Informationen zeigen können, ohne die Website jedes Mal neu zu veröffentlichen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Neuigkeiten. Die Protokolle werden nach 14 Tagen gelöscht. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
### Öffnungszeiten auf dieser Website
Auf dieser Website zeigen wir unsere Öffnungszeiten und ob wir gerade geöffnet haben. Dazu können Hinweise, zum Beispiel zu Betriebsurlaub, am Seitenanfang und auf dem Handy eine Leiste mit Knöpfen zum Anrufen und für die Route kommen. Diese Angaben lädt Ihr Browser beim Aufruf der Seite als kleines Programm und als Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, der bei der Hetzner Online GmbH in Falkenstein (Deutschland) steht. Dabei werden Ihre IP-Adresse, Datum und Uhrzeit des Aufrufs sowie Angaben zu Ihrem Browser und Betriebssystem an project.webklar.com übertragen und kurzzeitig in Server-Protokollen gespeichert. Das Programm setzt keine Cookies und speichert nichts in Ihrem Browser. Wir nutzen den Dienst, damit unsere Öffnungszeiten auf der Website überall gleich und aktuell erscheinen, ohne dass wir die Website bei jeder Änderung neu veröffentlichen müssen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der einfachen und aktuellen Darstellung unserer Öffnungszeiten. Die Protokolle werden nach 14 Tagen gelöscht. Für Suchmaschinen stehen unsere Öffnungszeiten außerdem in maschinenlesbarer Form im Seitencode. WEBklar verarbeitet die Daten in unserem Auftrag. [Prüfen: AVV mit WEBklar erforderlich]
#### Baustein Route (nur aufnehmen, wenn die Leiste den Knopf „Route“ zeigt)
Der Knopf „Route“ ist ein Link zu Google Maps. Erst wenn Sie darauf klicken, öffnet sich Google Maps (Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland) mit unserer Adresse als Ziel. Vorher werden keine Daten an Google übertragen. Nach dem Klick gelten die Datenschutzhinweise von Google.
#### Baustein WhatsApp (nur aufnehmen, wenn die Leiste den Knopf „WhatsApp“ zeigt)
Der Knopf „WhatsApp“ ist ein Link zu WhatsApp. Erst wenn Sie darauf klicken, öffnet sich WhatsApp (WhatsApp Ireland Limited, Merrion Road, Dublin 4, D04 X2K5, Irland, ein Unternehmen von Meta) mit unserer Nummer und gegebenenfalls einem vorgeschlagenen Starttext Ihrer Nachricht. Vorher werden keine Daten an WhatsApp oder Meta übertragen. Die Nachricht senden Sie selbst ab; nach dem Klick gelten die Datenschutzhinweise von WhatsApp.
+123
View File
@@ -0,0 +1,123 @@
# Öffnungszeiten live auf der Kundenwebsite einbauen
Anleitung für das WEBklar-Team. „Öffnungszeiten live“ zeigt die Zeiten, die der Kunde im Portal einträgt (Tab „Funktionen“, Kachel „Öffnungszeiten live“), auf seiner Website: als Tabelle, als Live-Status und als Kurzanzeige, dazu eine Hinweis-Leiste, auf dem Handy eine Kontakt-Leiste und Google-Angaben (JSON-LD) im Seitencode. Den `siteKey` (16 Zeichen, a–z und 2–7) findest Du im WOMS: Kunde → Reiter „Funktionen“, Zeile „Öffnungszeiten live“ (mit Kopierknopf). Im Einrichtungs-Ticket „Öffnungszeiten live einbauen – <Projekt>“ stehen Einbau-Zeile, Ergebnis des automatischen Einbaus (`einbau`), Beispiele der Platzhalter mit den Plan-Kurznamen und die Prüfliste. Dieses Dokument ist die Referenz dahinter. Die Datenschutz-Absätze für die Kundenseite stehen in `docs/funktionen/website-datenschutz-absatz.md`.
Die Spec ist `docs/superpowers/specs/2026-10-04-oeffnungszeiten-live-design.md` (Abschnitte 4.2, 4.3, 4.5, 4.6).
## 1. Was automatisch geschieht und was Du tust
| Teil | Wer | Wie |
| --- | --- | --- |
| Einbau-Zeile (Skript) | Runner, automatisch | beim nächsten Veröffentlichen in jede HTML-Datei vor `</head>`, nur wenn die Funktion in `setup` oder `active` steht |
| Hinweis-Leiste, Kontakt-Leiste, JSON-LD | Skript | kommen mit den Daten, ohne Platzhalter |
| Platzhalter (Tabelle, Status, Badge) mit Ersatz-HTML | Du | an die Stellen setzen, an denen feste Zeiten stehen, und die festen Zeiten ersetzen |
| Datenschutz-Absatz auf der Website | Du | Abschnitt 8, Punkt 5 |
| Einbau-Zeile bei `einbau: manuell` oder wenn die Seite woanders liegt | Du | Abschnitt 9 |
Der Einbau ist **ein Einbau je Website** und ist im Preis (30 € oder im Wartungs-Abo ohne Aufpreis) enthalten. Er wird nicht auf das Stundenkontingent angerechnet. Sonderwünsche (zusätzliche Stellen, Sondergestaltung) gehen über ein Angebot oder, mit Abo, über Stunden.
## 2. Die Einbau-Zeile
```html
<script data-wk-zeiten src="https://project.webklar.com/embed/zeiten.js" data-site="SITEKEY" defer></script>
```
- Ins `<head>` jeder HTML-Seite, einmal pro Seite. Der Runner setzt sie vor `</head>` ein (liegt kein `</head>` vor, nach `<body>`); eine schon vorhandene Zeile mit `data-wk-zeiten` ersetzt er, der Einbau ist also wiederholbar und doppelt nichts.
- `data-site` ist der `siteKey`. Optional `data-api="https://project.webklar.com"` (nur am Script-Tag, nie an einem Platzhalter); ohne Angabe nimmt das Skript die Herkunft seiner eigenen Adresse (das klappt nur bei einem normalen `<script src>`; bei Tag-Manager oder nachgeladenem Skript ist `data-api` Pflicht). Den siteKey darf auch ein Platzhalter tragen (`data-webklar-zeiten="SITEKEY"`).
- Die Adresse des Skripts kommt aus der Runner-Variable `ZEITEN_PUBLIC_BASE` (Vorgabe `https://project.webklar.com/embed/`, muss mit `https://` beginnen und mit `/` enden). Für die Produktion nichts ändern.
- Das Skript hat keine Abhängigkeiten, setzt keine Cookies (keine Cookies, kein `localStorage`, kein `sessionStorage`), fragt nur `project.webklar.com` ab und lädt nichts von Dritten. Kundentexte setzt es nur als Text ein, nie als HTML. Es wird 5 Minuten gecacht und darf von jeder Domain geladen werden. Ein Abruf je Seite für alle Teile, danach höchstens alle 10 Minuten.
- Hat die Seite eine Content-Security-Policy, muss `project.webklar.com` in `script-src` und `connect-src` stehen, und `style-src` muss Inline-Stile erlauben (`'unsafe-inline'`): das Skript legt ein `<style>`-Element an (Tabelle, Status, Leisten). Ohne diese Erlaubnis erscheinen sie ohne Gestaltung.
## 3. Platzhalter
Ein Platzhalter ist ein Element mit dem Attribut `data-webklar-zeiten`. Innen steht das Ersatz-HTML (Abschnitt 4).
```html
<div data-webklar-zeiten data-modus="tabelle" data-plan="oeffnungszeiten">…Ersatz-HTML…</div>
<div data-webklar-zeiten data-modus="status" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</div>
<span data-webklar-zeiten data-modus="badge" data-plan="oeffnungszeiten">Mo–Fr 9–18 Uhr</span>
```
| `data-modus` | Zeigt | Gedacht für |
| --- | --- | --- |
| `tabelle` | Wochentabelle (echtes `<table>` mit Caption und Zeilenköpfen, heutiger Tag mit `aria-current="date"`), darunter „Abweichungen in den nächsten 14 Tagen“ | Kontaktseite, Footer |
| `status` | Punkt und Text, zum Beispiel „Jetzt geöffnet · schließt um 18 Uhr“, ohne `aria-live` | Kopfbereich, Hero |
| `badge` | Kurzform „Geöffnet“, „Geschlossen“, „Nach Vereinbarung“ | Menü, Header |
- `data-plan` ist der **Kurzname** des Zeitplans (Kleinbuchstaben, Ziffern und Bindestrich, 1–24 Zeichen, nach dem Anlegen unveränderlich). Der Hauptplan heißt `oeffnungszeiten`. Weitere Pläne (zum Beispiel `kueche`, `lieferung`) hat der Kunde im Editor angelegt; die Kurznamen stehen im Editor und im Einrichtungs-Ticket („Pläne (Kurznamen für data-plan)“). Ohne `data-plan` gilt der Hauptplan.
- **Einzelseiten-Apps (Vite/React):** Das Skript beobachtet `<body>` und füllt Platzhalter, die erst nach einem Seitenwechsel auf die Seite kommen, nach etwa 0,2 Sekunden (auch einen wieder eingehängten Knoten). Setze Platzhalter nicht in Teile, die die App beim Rendern entfernt oder neu aufbaut: ein neu gebauter Knoten startet wieder mit seinem Ersatz-HTML, und was das Skript hineingeschrieben hat, kann die App überschreiben.
- Ein **unbekannter** `data-plan` oder `data-modus` lässt das Ersatz-HTML stehen und schreibt eine Warnung in die Konsole. Es gibt kein stilles Umschalten auf den Hauptplan. Bei Fehlern, ohne JavaScript, bei Daten älter als 24 Stunden und nach dem Abschalten bleibt ebenfalls das Ersatz-HTML.
- Das Skript füllt die Platzhalter ab `setup`, damit Du vor dem Schließen des Tickets prüfen kannst.
- Gestaltung: Light DOM, alle Klassen beginnen mit `wkz-`, die Stile stehen in `@layer wkz`. **Jede** Regel der Kundenseite (ohne Layer) gewinnt also gegen die Standard-Stile. Schrift und Farben erbt die Seite. Variablen (alle optional, am Platzhalter oder auf `:root`): `--wkz-offen`, `--wkz-zu`, `--wkz-unklar` (Farben der Punkte), `--wkz-offen-hell`, `--wkz-zu-hell`, `--wkz-unklar-hell`, `--wkz-offen-text`, `--wkz-zu-text`, `--wkz-unklar-text` (Badge), `--wkz-rand`, `--wkz-heute`. Kontrast mindestens 4,5 : 1 prüfen.
- Hat das Theme globale Regeln wie `table{…}` oder `td{…}`, greifen sie auch in der Tabelle. Prüfe das bei der Abnahme und überschreibe gezielt, zum Beispiel `[data-webklar-zeiten] table{ … }`.
## 4. Ersatz-HTML
Jeder Platzhalter behält eine **statische Ersatz-Tabelle**: Sie steht ohne JavaScript, bei einem Fehler und nach der Abschaltung. Ohne sie wäre die Stelle leer.
1. WOMS → Funktionen → Ersatz-HTML kopieren (je Plan ein Knopf; bei mehreren Plänen mit Namen dahinter). Erst nach dem ersten Speichern des Kunden; vorher meldet das WOMS „Der Kunde hat noch keine Zeiten eingetragen“ (die Schnittstelle antwortet 409: `GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=…&plan=<kurz>`).
2. In jeden Platzhalter einsetzen. Das Ersatz-HTML ist eine schlichte Wochentabelle als reines ASCII mit Entities (`&auml;`, `&#8211;`), also in jeder Dateikodierung sicher. Ein Fensterende um Mitternacht steht als `00:00` („18:00–00:00“), ein Fenster von 00:00 bis 24:00 als „rund um die Uhr“.
3. Die Tabelle zeigt den Wochenplan **zum Zeitpunkt des Einbaus**. Ändert der Kunde seinen Wochenplan später, bleibt das Ersatz-HTML zunächst stehen (mit Skript unsichtbar).
4. **Beim Abschalten** der Funktion, wenn der aktuelle Wochenplan des Hauptplans vom Stand beim Einbau abweicht, entsteht ein WOMS-Ticket **„Ersatz-Tabelle aktualisieren – <Projekt>“** mit dem aktuellen Ersatz-HTML. Ersetze die Tabelle in allen Platzhaltern (gehört zum enthaltenen Einbau, Leistungsbeschreibung Nr. 4 Abs. 3). Die Mail K30 sagt dem Kunden, dass wieder feste Zeiten auf der Seite stehen. Liegt die Seite woanders, schicke dem Kunden den Text.
## 5. Header-Selektor (`navSelektor`)
Die Hinweis-Leiste steht als erstes Kind von `<body>` im Fluss und schiebt den Inhalt nach unten. Feste und klebende Elemente mit `top: 0` rückt das Skript, solange die Leiste sichtbar ist, um `max(0, Leistenhöhe − Scrollposition)` nach unten (Inline-`top`, beim Entfernen der Leiste zurückgesetzt) und erhöht `scroll-padding-top`.
Überlappt die Leiste trotzdem den Kopf der Seite (zum Beispiel ein fester Header mit `top: 12px`, mit `transform` oder in einem Wrapper), setze im WOMS bei Kunde → Funktionen → „Öffnungszeiten live“ das Feld **Header-Selektor** (Team-Einstellung `navSelektor`):
- ein CSS-Selektor des Headers, höchstens 120 Zeichen, erlaubt sind nur `A–Z a–z 0–9`, Leerzeichen und `# . - _ > : [ ] = " '`, nicht erlaubt `<`, `{`, `}` und `;`. Beispiel: `header.site-header` oder `#nav`.
- Das Skript wendet ihn mit `querySelectorAll` in einem `try/catch` an; ein falscher Selektor bricht nichts, er bewirkt nur nichts.
- Leer lassen = Standard (nur `top: 0`). Speichern mit `teamVersion` (bei 409 „Inzwischen geändert – bitte neu laden“).
- Prüfe nach dem Setzen: oben auf der Seite, beim Scrollen (die Leiste scrollt weg, der Header klebt wieder oben) und auf dem Handy.
## 6. Leisten ausschalten
Im selben Bereich (WOMS → Funktionen → „Öffnungszeiten live“) gibt es zwei Haken: **`kontaktleisteAus`** („Kontakt-Leiste aus“) und **`hinweisleisteAus`** („Hinweis-Leiste aus“). Sie gelten je Website.
- Die **Kontakt-Leiste** erscheint nur bei `(max-width: 768px)`, unten fest mit `z-index: 40`, Knöpfe „Anrufen“, „Route“ und, wenn der Kunde sie eingerichtet hat, „WhatsApp“. Hat die Seite selbst ein festes Element am unteren Rand mit einem `a[href^="tel:"]`, bleibt sie **automatisch aus**. Setze den Haken, wenn die Seite eine eigene Anruf-Leiste hat, die das Skript nicht erkennt, oder der Kunde die Leiste nicht möchte.
- WhatsApp und die Route (Google Maps) öffnen erst beim Klick. Den Haken „Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen“ setzt der Kunde im Portal; in der Kundenansicht setzt ihn kein Mitarbeiter.
- Die **Hinweis-Leiste** zeigt zu jedem Zeitpunkt den gültigen Hinweis mit dem höchsten Rang (Wichtig, Urlaub, Aktion, Info). Sie hat keinen Schließen-Knopf (das Skript speichert nichts im Browser).
## 7. JSON-LD
Das Skript pflegt die Öffnungszeiten im Seitencode: Gibt es einen `application/ld+json`-Block mit einem Knoten vom Typ `LocalBusiness` oder einem Untertyp (auch in `@graph`), ersetzt es dort **nur** `openingHoursSpecification` und `specialOpeningHoursSpecification` (alles andere im Block bleibt); unlesbare Blöcke bleiben unberührt. Gibt es keinen, legt es einen eigenen Block `[data-wkz-jsonld]` mit `name`, `address` und `url` an. Prüfe auf der Seite, ob JSON-LD mit festen Öffnungszeiten an einer anderen Stelle steht (zum Beispiel als Mikrodaten im Footer); das würde dem Skript widersprechen.
## 8. Prüfliste
Das Einrichtungs-Ticket nennt dieselben fünf Punkte:
1. **Feste Zeiten im Repo suchen und ersetzen**: `grep -ril "öffnungszeit" .` und Ähnliches (`geöffnet`, `Mo-Fr`, `Uhr`), auch Footer, Kontaktseite, `<meta>` und eigene Status-Skripte. Jede Stelle wird ein Platzhalter mit Ersatz-HTML oder fällt weg.
2. **Header**: Überlappt die Hinweis-Leiste den Kopf der Seite? Sonst `navSelektor` setzen (Abschnitt 5).
3. **Anruf-Leiste**: Hat die Seite schon eine eigene feste Anruf-Leiste? Dann `kontaktleisteAus` (Abschnitt 6), damit sie nicht doppelt erscheint.
4. **Vorhandenes JSON-LD** (Öffnungszeiten der Seite) prüfen, damit nichts doppelt oder widersprüchlich steht (Abschnitt 7).
5. **Datenschutz-Absatz auf der Website**: den Absatz „Öffnungszeiten auf dieser Website“ aus `docs/funktionen/website-datenschutz-absatz.md` einsetzen (Variante A bei WEBklar-Hosting, Variante B bei Fremdhosting). Die Bausteine „Route“ und „WhatsApp“ nur, wenn die Kontakt-Leiste die Knöpfe zeigt. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, setzt Du den Absatz ein (gehört zum Einbau, nicht auf das Stundenkontingent anrechnen); liegt sie woanders (externer Generator), schicke dem Kunden den Textbaustein, er fügt ihn selbst ein. Die Texte sind Entwurf, bis die Rechtsrunde sie freigibt.
Zur Abnahme außerdem: Seite in der Vorschau öffnen und mit ausgeschaltetem JavaScript ansehen (Ersatz-Tabelle muss stehen), Konsole auf Warnungen zu `data-plan` prüfen, auf einem Handy (höchstens 768 px Breite) die Kontakt-Leiste, den Regler „Uhrzeit testen“ im Editor gegen die Anzeige halten, Kontrast und Tippflächen (mindestens 44 px) ansehen.
## 9. Sperrliste `ZEITEN_KEIN_NEUBAU` und Seiten ohne automatischen Einbau
**Neubau und Einsetzen.** Bei jedem Übergang in `setup` oder `active` (Einschalten im Abo, Zahlung, Aktivierung durch das Team, Rückkehr des Abos, Behalten) stößt das Portal einen Neubau der Seite an (höchstens einer gleichzeitig). Das Ergebnis steht in `einbau`: `angestossen` (Auftrag angenommen, die Zeile erscheint nach dem Veröffentlichen) oder `einbau: manuell`. Änderungen an Zeiten und Leisten brauchen keinen Neubau.
`einbau: manuell` heißt: kein Repo, Vorschau aus, Dev-Modus, Projekt auf der Sperrliste, Fehler oder Runner ausgelastet. Dann setzt Du die Einbau-Zeile von Hand ins Repo.
**Sperrliste.** `ZEITEN_KEIN_NEUBAU` (Umgebungsvariable des Portals, Liste von Subdomains, durch Komma getrennt): Für diese Projekte gibt es weder einen angestoßenen Neubau noch das automatische Einsetzen bei normalen Deploys; die Funktion zählt sie immer als manuell. Vorgabe ist `emmas-schatzkiste` (Emmas Seiten, deren Dateien teilweise nicht in Git liegen). **Achtung:** Setzt Du die Variable, **ersetzt** sie die Vorgabe. Willst Du eine Seite zusätzlich sperren, nenne `emmas-schatzkiste` mit in der Liste. Änderung in `compose` und `.env` des Portals, danach das Portal mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server` neu erstellen (kein `docker restart`: es liest geänderte Werte nicht neu ein). Für gesperrte Seiten setzt Du die Zeile und die Platzhalter von Hand ein (im Repo, soweit die Dateien dort liegen, sonst direkt auf dem Server) und prüfst nach jedem Deploy, ob sie noch da sind.
**Seite liegt woanders** (anderer Hoster, keine WEBklar-Veröffentlichung): Das Ticket enthält die Einbau-Zeile; Du setzt sie ein, soweit WEBklar Zugriff auf die Seite hat. Hat WEBklar keinen Zugriff, schicke dem Kunden im Chat Einbau-Zeile, Platzhalter-Beispiele und Ersatz-HTML und bitte ihn, sie selbst oder durch seinen Anbieter einzufügen. Schließe das Ticket erst, wenn die Anzeige auf der Live-Seite stimmt: Öffne die Seite, sieh in den Netzwerk-Aufrufen nach der Antwort von `/api/public/zeiten/v1/<siteKey>` (`"aktiv": true`) und nach der gefüllten Tabelle.
## 10. Freischalten und Ende
- **Freischalten:** Der Job setzt `active` (und sendet K16), wenn das Ticket geschlossen ist und der Kunde Zeiten eingetragen hat (Hauptplan gespeichert, mindestens ein Tag nicht „zu“). Sonst bleibt `setup`, das WOMS zeigt „wartet auf die Zeiten des Kunden“. Solange der Kunde nichts eingetragen hat, kannst Du die Tabelle nicht einbauen.
- **Ende des Abos** (Gratisweg): K29 mit Frist zum Behalten (30 Tage), danach `disabled` und K30, Eingaben noch 30 Tage, dann gelöscht. Das Ticket „Ersatz-Tabelle aktualisieren“ gehört zum Abschalten (Abschnitt 4).
- **Abschalten durch das Team** (Grund `team`): keine automatische Löschung.
## 11. Häufige Fehlerbilder
| Symptom | Meist liegt es an |
| --- | --- |
| Es erscheint nur das Ersatz-HTML | Funktion nicht in `setup`/`active`, `einbau: manuell` und Zeile fehlt, unbekannter `data-plan`, kein JavaScript, Daten älter als 24 Stunden, CSP blockiert `project.webklar.com` |
| Hinweis-Leiste verdeckt den Header | `navSelektor` nicht gesetzt (Abschnitt 5) |
| Zwei Anruf-Leisten auf dem Handy | `kontaktleisteAus` setzen (Abschnitt 6) |
| Kontakt-Leiste fehlt auf dem Handy | Seite hat schon eine feste Anruf-Leiste unten, oder Breite über 768 px, oder `kontaktleisteAus` |
| Öffnungszeiten doppelt im Suchergebnis oder Test | festes JSON-LD an anderer Stelle (Abschnitt 7) |
| Tabelle sieht fremd aus | globale Regeln des Themes (Abschnitt 3) |
+155
View File
@@ -0,0 +1,155 @@
# Abo-Bedingungen Fassung 6 (Entwurf)
Fassung 2026-10-16-v6 · Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung · Basis: Fassung 5 (Stand 30.09.2026, `public/vertragsunterlagen/v5/abo-bedingungen.html`)
Dieser Entwurf listet alle Änderungen gegenüber Fassung 5 im Wortlaut. Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben, damit Querverweise in Mails und der Paragraf zum Vertragsende (§ 11) stimmen. Die Sätze unter jeder Überschrift sind die Pflichtsätze; sie stehen unverändert in der Seite `public/neu/rechtliches/abo-bedingungen.html`. Zu jeder Stelle nennt die Zeile „Begründung“ die Entscheidung des Inhabers (E…), die Festlegung nach dem Builder-Check (C…) oder den Spec-Abschnitt und verweist bei offenen Punkten auf die Entscheidungsliste (`docs/rechtliches/kundenansicht-entscheidungen.md`, R… und KQ…). Voraussetzung für den Livegang ist die Freigabe durch den Inhaber (E20).
## Das Wichtigste auf einen Blick
Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).
**Einordnung.** Neue Zeile „Funktionen“ in der Übersichtsbox, hinter der Zeile „Wartungs-Abo“. Die Box behält ihre Preiszeilen und die Anrede „Ihr“.
**Begründung.** E6 (die Funktionen stecken im Abo: Hosting-Abo mit Analyse, Wartungs-Abo mit allen Funktionen), E7 und E17 (Einrichtung vom Stundenkonto, keine Extra-Rechnung), C10 (Analyse im Hosting-Abo), Spec 5.9. Die Zeile nennt für Funktionen keinen Preis. Offen: Geltung für laufende Abos (R2, KQ1), Anrede der Rechtsdokumente (R12).
## § 1 Abs. 2
Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.
**Einordnung.** Hängt an den bestehenden Absatz 2 an („Sie gelten für Verbraucher und Unternehmer“).
**Begründung.** E2 bis E4 und C13: Die Kundenart legt das Team beim Anlegen fest, leer gilt als privat, und die Bestellseiten richten sich danach. Rechtlich entscheidet der Zweck des Vertrags (§§ 13, 14 BGB), nicht die hinterlegte Angabe; der Satz sagt das ausdrücklich, damit die Angabe nicht als Selbstauskunft oder als Rechtsfolge gelesen wird. Offen: Belastbarkeit der Kundenart (R5, KQ4).
## § 2 Abs. 2 c
Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.
**Einordnung.** Ersetzt den bisherigen Buchstaben c (Zustimmung zu den Bedingungen und Wahl des Beginns). Die Buchstaben a, b, d und e bleiben.
**Begründung.** Spec 5.6, E1, E3, E4, C4 und recht.md Kurzfassung 3 und 4: Eine Linkzeile über dem Knopf ersetzt die Häkchen für Bedingungen, Leistungsbeschreibung, Auftragsverarbeitung und Belehrung; übrig bleibt das eine Häkchen „Bitte beginnt sofort …“ nur für Privatkunden in der Widerrufsfrist. Bei Betrieben (kein Widerrufsrecht) beginnt die Leistung mit Vertragsschluss. Die Kundenart kommt vom Server (§ 1 Abs. 2). Offen: Wortlaut des Häkchens ohne Normzitat (R6, KQ5), Belehrung nur per Link und PDF (R11, KQ6), AVV per Klick ohne eigenes Häkchen (R10, KQ7).
## § 2 Abs. 6
Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.
**Einordnung.** Hängt an den bestehenden Absatz 6 (Vertragssprache und Speicherung des Vertragstextes) an.
**Begründung.** Nachweise (Global Constraints der Übersicht, N34): Wortlaut, Fassung, Zeit, IP-Adresse, Browserkennung und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt. Der Satz macht das für den Kunden sichtbar; die Datenschutzerklärung nennt die Speicherung heute schon (Prüfpunkte D1 bis D3). Offen: Datenschutz-Prüfpunkte (R14).
## § 3 Abs. 8
Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.
Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.
**Einordnung.** Neuer Absatz „(8) Funktionen“ nach Absatz (7) mit zwei Absätzen.
**Begründung.** E6, E9, E10, E21, C10, C11, C12 und Spec 5.5, 5.9: Funktionen sind Teil des Abos und kosten keine zusätzliche Gebühr; die Einrichtung läuft nach § 4 Abs. 8 und 9. Eine Funktion läuft nur mit dem Abo, das sie enthält, auch wenn sie schon bei der Website-Erstellung eingebaut wurde (E21). Einzeln gekaufte Funktionen behalten ihre Bedingungen (E10). Offen: Geltung für laufende Abos (R2, KQ1), Einrichtung beim Projektstart (R4, KQ8), Ende und Fristen (R8, KQ8), Analyse im Hosting-Abo (R9).
## § 4 Abs. 2
Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.
**Einordnung.** Hängt als letzter Satz an den bestehenden Absatz 2 (Verfügbarkeit und Verfall der Kontingente) an.
**Begründung.** E17: Ein offener Vorschuss wird vor allem anderen aus den folgenden Monatskontingenten ausgeglichen (FIFO, Spec 5.5). Der Satz verknüpft die bestehende Verbrauchsregel mit Absatz 8. Offen: Vorschuss-Regel insgesamt (R3, KQ2).
## § 4 Abs. 8
Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.
**Einordnung.** Neuer Absatz 8 nach Absatz 7.
**Begründung.** E7, E8 und E17 (E17 ersetzt die Absprache aus E8), Spec 5.5: Die Einrichtung startet ohne Rückfrage und geht mit der echten Arbeitszeit (Absatz 4) vom Stundenkonto ab. Reicht das Kontingent nicht, ist die fehlende Zeit ein Vorschuss, nie Mehraufwand nach Absatz 6; ein Rest bei Vertragsende verfällt zugunsten des Kunden. Normale Arbeit außerhalb von Einrichtungen bleibt beim Mehraufwand nach Absatz 6. Offen: Vorschuss ohne Obergrenze (R3, KQ2).
## § 4 Abs. 9
Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.
**Einordnung.** Neuer Absatz 9 nach Absatz 8.
**Begründung.** E9, E21, C6 und C7, Spec 5.5: Im Hosting-Abo gibt es kein Kontingent (Absatz 7), die Einrichtung der Analyse ist dort kostenlos (C6). Bei einer Funktion, die schon bei der Erstellung der Website eingebaut wird, zählen die Einrichtungs-Arbeitsblätter nicht aufs Stundenkonto, sondern laufen über die Projektkosten, wie mit dem Kunden vereinbart (C7). Offen: Einrichtung beim Projektstart (R4, KQ8), Analyse im Hosting-Abo (R9).
## § 6 Abs. 6
Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** C10 und Spec 5.5 (Analyse): Wer die Website-Analyse einschaltet, ist für die Messung auf seiner Website Verantwortlicher. Der Absatz stellt die Pflichten aus der Abo-Ergänzung Analyse (Cookie-Banner, keine weiteren Mess- oder Tracking-Werkzeuge ohne Abstimmung, Hinweis auf die Zusatzseite) in die Abo-Bedingungen, weil die Analyse jetzt auch im Hosting-Abo liegt. Die Tatsachenangabe zur Zielgruppe bleibt als Zeile mit Häkchen in der Oberfläche (Spec 5.5, N22). Offen: Analyse im Hosting-Abo (R9, KQ8), Ausschlüsse als Vertragsklausel (R13, KQ9).
## § 9 Abs. 6
Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** E14 und C5: Wer im Hosting-Abo eine Funktion einschalten will, die nur das Wartungs-Abo enthält, wird zum Wechsel geführt und schaltet danach in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung mit eigener Widerrufsfrist (§ 2 Abs. 5); das Einschalten ist keine Bestellung. Offen: Sofortbeginn des Wechsels nur mit Häkchen bei Privatkunden (R6, KQ5).
## § 11 Abs. 4
Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.
**Einordnung.** Neuer Absatz 4 nach Absatz 3.
**Begründung.** E21, C12 und Nachtrag N31 (mit P4 und P6 abgestimmt): Funktionen enden mit dem Abo, das sie enthält. Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob); Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter, ein „Behalten“ gegen Zahlung gibt es nicht mehr. Gelöscht wird nach den Fristen der Leistungsbeschreibungen: 30 Tage (Terminplaner, News, Öffnungszeiten live), 90 Tage (Webshop, Buchungskalender), 60 Tage für die Messdaten der Analyse. Ein neues passendes Abo in der Frist holt die Funktion zurück. Beim Wechsel ins Hosting-Abo und beim Widerruf des Wechsels bleibt die Analyse (E6, C10). Offen: R8 (KQ8).
## § 14 Abs. 2
Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.
**Einordnung.** Hängt an den bestehenden Absatz 2 (Auftragsverarbeitungsvertrag in Anlage 1) an.
**Begründung.** Spec 5.9 (AVV je Funktion): Option A der AVV-Struktur, die Auftragsverarbeitungsverträge der Funktionen bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen; der Klick auf „Einschalten“ unter der Linkzeile ist die Zustimmung. Offen: Struktur (R10, KQ7), Klick statt eigenes Häkchen (KQ7).
## § 15
Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.
**Einordnung.** Neuer zweiter Absatz hinter dem bisherigen Text. Der bisherige Text bleibt wörtlich.
**Begründung.** C11 und E10: Fassung 6 gilt für neue Abschlüsse; laufende Abos der Fassungen 3 bis 5 behalten ihren Vertrag. Beim Einschalten gelten die Leistungsbeschreibung und der Auftragsverarbeitungsvertrag der Funktion über die Linkzeile und den Klick, auch mit den Regeln zur Einrichtung und zum Stundenkonto. Die Änderung ist für laufende Abos vorteilhaft (Spec 5.9), deshalb keine Zustimmungsabfrage nach dem bisherigen Text. Offen: Info-Mail an laufende Abos (R2, KQ1).
## Leistungsbeschreibung A Nr. 1
Website-Analyse im Kundenportal (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6 und C10: Das Hosting-Abo enthält die Website-Analyse. Offen: Analyse im Hosting-Abo (R9, KQ8).
## Leistungsbeschreibung B Nr. 1
Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6: Das Wartungs-Abo enthält alle Funktionen und als Obermenge die Analyse.
## Widerruf: Hinweise
Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).
**Einordnung.** Neue Nummer 5 am Ende der „Hinweise zum Widerruf“. Widerrufsbelehrung und Muster-Widerrufsformular bleiben wortgleich mit Fassung 5.
**Begründung.** Spec 5.5 und 5.9, recht.md § 2: Das Einschalten löst keine neue Zahlungspflicht aus und ist kein eigener Vertrag; deshalb gibt es keine eigene Widerrufsfrist. Bei Widerruf des Abos endet die Funktion (§ 11 Abs. 4). Wie sich die Einrichtungszeit innerhalb der Frist auf den Wertersatz auswirkt, regelt das Häkchen für Privatkunden (§ 2 Abs. 2 c); die Entscheidung dazu steht in R7. Offen: R7 (KQ3), Belehrung per Link (R11, KQ6).
## Anlage 3
Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)
Deutschland (Rechenzentrum Falkenstein)
**Einordnung.** Neue Tabellenzeile hinter den beiden Hetzner-Zeilen (Unternehmen wie dort, Leistung und Ort wie im Pflichtsatz).
**Begründung.** Spec 5.9 (AVV je Funktion), Art. 28 Abs. 2 DSGVO: Der Server des Messdienstes der Website-Analyse (Hetzner, Rechenzentrum Falkenstein, wie in der Auftragsverarbeitung der Analyse) wird als Unterauftragsverarbeiter genannt, solange die Analyse eingeschaltet ist. Offen: AVV-Struktur (R10, KQ7).
## Was gleich bleibt
- Preise: Hosting-Abo 20,00 € pro Monat, Wartungs-Abo 50,00 € pro Monat, E-Mail-Zusatzspeicher 1,00 € pro Monat je Paket von 5 GB, Mehraufwand 100 € pro Stunde (alle Endpreise, `§ 19 UStG`). Funktionen bekommen keinen Preis.
- Laufzeiten: Mindestlaufzeit zwölf Monate ab Leistungsbeginn, Kündigung zum Ende der Mindestlaufzeit mit einem Monat Frist, danach monatlich (§ 8), Sonderkündigung bei Geschäftsaufgabe.
- Paragrafenfolge § 1 bis § 16 und alle Anker (`p1` bis `p16`, `wechsel`, `zusatzspeicher`, `hosting`, `wartung`, `widerruf`, `formular`, `widerruf-hinweise`, `avv`, `tom`, `sub`).
- Widerrufsbelehrung und Muster-Widerrufsformular: wortgleich mit Fassung 5; die Hinweise zum Widerruf Nr. 1 bis 4 bleiben.
- § 10 E-Mail-Zusatzspeicher mit Absatz 3 (Bereitstellung sofort, kein Wertersatz) und den Hinweisen zum Widerruf Nr. 4.
- Anlage 1 (Auftragsverarbeitungsvertrag) und Anlage 2 (technische und organisatorische Maßnahmen); Anlage 3 behält ihre beiden Hetzner-Zeilen.
- Die Rechtsdokumente bleiben in der bisherigen Anrede („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ (R12).
+149
View File
@@ -0,0 +1,149 @@
# Allgemeine Geschäftsbedingungen
**Fassung 2026-09-30** · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb
## Kaufvertrag – WEBklar (Modularer Projektvertrag)
**zwischen**
WEBklar (im Folgenden „Anbieter“)
**und**
Kunde laut Angebot (im Folgenden „Kunde“)
## 1. Vertragsgegenstand
(1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.
(2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.
(3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar.
## 2. Leistungsart
(1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.
(2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.
(3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.
## 3. Leistungsmodul A – Webseite (einmalig)
(aktiv, wenn im Angebot enthalten)
(1) WEBklar erstellt eine individuelle Webseite gemäß Angebot.
(2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.
(3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.
(4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.
(5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.
## 4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)
(aktiv, wenn im Angebot enthalten)
(1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.
(2) Die Leistung stellt eine reine Entwicklungsleistung dar.
(3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein.
(4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.
(5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.
## 5. Leistungsmodul C – Hosting
(aktiv, wenn im Angebot enthalten)
(1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor.
(2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen.
(3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.
## 6. Quellcode und Eigentum
### 6.1 Webseite
(1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.
(2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.
(3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.
### 6.2 Apps, Automatisierungen und Backend
(1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.
(2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.
(3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.
## 7. Mitwirkungspflichten des Kunden
(1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.
(2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.
(3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.
## 8. Abnahme
(1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.
(2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.
(3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.
## 9. Vergütung und Zahlung
(1) Die Vergütung ergibt sich aus dem Angebot.
(2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.
(3) Projektleistungen sind nach Vereinbarung fällig.
(4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.
## 10. Zahlungsverzug
(1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.
(2) Danach ist WEBklar berechtigt:
- Leistungen zu sperren
- Verzugszinsen zu berechnen
- den Vertrag außerordentlich zu kündigen
## 11. Haftung
(1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
(2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
(3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte.
(4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar.
## 12. Kündigung
(1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos.
(2) Das Recht zur außerordentlichen Kündigung bleibt unberührt.
## 13. Referenzen
WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.
## 14. Schlussbestimmungen
(1) Es gilt deutsches Recht.
(2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.
(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.
+8
View File
@@ -8,6 +8,14 @@ Jede ausgelieferte Fassung bleibt hier nachvollziehbar. `BANNER_VERSION` im Clie
Änderung der Zusatzseite am 02.10.2026 (Fassung bleibt 2026-10-02, Banner-Text unverändert): Der Knopf zum Wiederöffnen des Banners ist jetzt ein Keks-Symbol (Name für Screenreader und Beschriftung beim Darüberfahren weiter „Datenschutz-Einstellungen“). Die Zusatzseite nennt deshalb bei Widerruf und Widerspruch zusätzlich „(das Keks-Symbol)“. Die vorige Formulierung war bis dahin nur auf der WEBklar-Testseite (test-website.project.webklar.com) ausgeliefert, keiner Kunden-Website: [datenschutz-analyse-2026-10-02-erstfassung.html](datenschutz-analyse-2026-10-02-erstfassung.html). In WOMS → Rechtliches als neue Version der Karte „datenschutz-website-analyse“ hinterlegt.
## Eigene Website webklar.com
Auf webklar.com läuft dieselbe Messung mit WEBklar als Verantwortlichem (keine AVV). Das Skript-Tag trägt `data-variant="eigen"`; der Banner hat dann eine eigene Sie-Fassung (`BANNER_VERSION_EIGEN`), die Zusatzseite liegt im Repository der Website (`public/datenschutz-analyse.html`), die Kopie hier ist byte-gleich zu der ausgelieferten Fassung. Der Abschnitt 4a der Datenschutzerklärung fasst sie zusammen.
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|---|---|---|---|
| 2026-10-17 | offen bis Livegang | [banner-eigen-2026-10-17.md](banner-eigen-2026-10-17.md) (`BANNER_VERSION_EIGEN` 2026-10-17) | [datenschutz-analyse-webklar-com-2026-10-17.html](datenschutz-analyse-webklar-com-2026-10-17.html) |
AVV und Leistungsbeschreibung je Fassung: `public/rechtliches/avv-website-analyse-<Fassung>.html|pdf` und `leistungsbeschreibung-website-analyse-<Fassung>.html|pdf`. „Gültig ab“ wird beim Livegang eingetragen.
## Terminplaner
@@ -0,0 +1,13 @@
# Banner-Text Fassung 2026-10-17 (eigene Websites der WEBklar GbR)
Quelle: `analytics/client/core.js` (Konstanten `BANNER_TEXT_EIGEN`, `BANNER_VERSION_EIGEN`). Gilt nur, wenn das Skript-Tag `data-variant="eigen"` trägt, derzeit nur auf webklar.com. Kunden-Websites behalten die Fassung 2026-10-02 ([banner-2026-10-02.md](banner-2026-10-02.md)).
- `BANNER_VERSION_EIGEN`: 2026-10-17
## Fließtext (BANNER_TEXT_EIGEN)
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
## aria-label, Schaltflächen, Einstellungs-Link
Wie in der Kunden-Fassung: „Einwilligung zur Reichweitenmessung“; „Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst); Keks-Symbol „Datenschutz-Einstellungen“ unten rechts.
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Datenschutz bei der Reichweitenmessung – WEBklar</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 12rem; }
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #6b6b70; font-size: 14px; }
.box { background: #fff; border: 2px solid #6d1fd1; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
a { color: #6d1fd1; }
nav.top { font-size: 14px; margin-bottom: 22px; }
</style>
</head>
<body>
<main>
<nav class="top"><a href="/">← Zur Startseite</a> · <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a> · <a href="/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Datenschutz bei der Reichweitenmessung</h1>
<p class="meta">Ergänzung zu unserer Datenschutzerklärung (Abschnitt 4a) · gilt für webklar.com · Stand 17.10.2026</p>
<p>Diese Seite beschreibt nur die Reichweitenmessung auf webklar.com. Alle übrigen Hinweise finden Sie in unserer <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a>.</p>
<h2>Verantwortlicher</h2>
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler<br>
E-Mail: <a href="mailto:support@webklar.com">support@webklar.com</a></p>
<p>Einen Datenschutzbeauftragten haben wir nicht benannt, weil wir dazu gesetzlich nicht verpflichtet sind.</p>
<h2>Worum es geht</h2>
<p>Wir möchten verstehen, wie unsere Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir unsere eigene Website-Analyse, dieselbe, die wir auch unseren Kunden anbieten. Sie kommt ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken aus. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf unserem Server in Deutschland.</p>
<p>Damit die Messung arbeiten kann, lädt Ihr Browser bei jedem Seitenaufruf ein Skript von unserem Server (project.webklar.com). Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Ihre IP-Adresse und Ihr Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Sie ablehnen oder „Global Privacy Control“ aktiviert haben. Beim Abruf des Skripts und bei der Messung speichern und protokollieren wir beides nicht.</p>
<h2>Solange Sie im Banner nichts ausgewählt haben</h2>
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
<ul>
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
<li>das Land, das wir aus Ihrer IP-Adresse ableiten,</li>
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
</ul>
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Ihrer IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Sie wiedererkennen könnten, speichern wir ohne Ihre Einwilligung nicht.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
<h2>Wenn Sie „Ablehnen“ wählen</h2>
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Ihr Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
<h2>Wenn Sie „Annehmen“ wählen</h2>
<p>Dann messen wir zusätzlich:</p>
<ul>
<li>von welcher Website Sie gekommen sind (nur der Domainname),</li>
<li>die Größenklasse Ihres Browserfensters,</li>
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Ihrer IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
<li>wie lange Sie auf einer Seite bleiben (ab dem Laden der Seite) und wie weit Sie scrollen,</li>
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
<li>ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
<li>einen Zähler dafür, dass Sie „Annehmen“ gewählt haben.</li>
</ul>
<p>Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
<h2>Was in Ihrem Browser gespeichert wird</h2>
<p>Wir speichern Ihre Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Ihres Browsers (Eintrag „wk-consent“), damit wir Sie nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Sie können den Eintrag jederzeit selbst löschen, indem Sie die Website-Daten in Ihrem Browser löschen.</p>
<h2>Speicherdauer</h2>
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten. Beenden wir die Messung, löschen wir alle Messdaten spätestens rund 60 Tage danach. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
<h2>Empfänger</h2>
<p>Die Messung betreiben wir selbst. Die Daten sehen nur unsere Mitarbeiter. Der Server steht bei der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland; Hetzner ist für uns als Auftragsverarbeiter tätig. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
<h2>Freiwilligkeit</h2>
<p>Sie sind weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Sie ablehnen oder Ihre Einwilligung widerrufen, können Sie unsere Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
<div class="box">
<h2 style="border:0;margin-top:0;padding-top:0">Einwilligung widerrufen</h2>
<p>Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicken Sie auf „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts auf jeder Seite) und wählen Sie „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
<h2 style="border:0;padding-top:0">Widerspruchsrecht</h2>
<p>Sie können der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Ihrer besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählen Sie im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivieren in Ihrem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Sie können Ihren Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
</div>
<h2>Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Ihnen zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle unseres Webservers noch vorhanden sind (Abschnitt 3 der Datenschutzerklärung). Wenn Sie uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Sie möchten, Ihre IP-Adresse nennen, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Ihnen das mit.</p>
<p>Mit Ihren Anliegen können Sie sich jederzeit an die oben genannte Adresse wenden. Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO), für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz.</p>
</main>
</body>
</html>
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+157
View File
@@ -0,0 +1,157 @@
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
- der Zustellstatus der E-Mails an den Gast.
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+157
View File
@@ -0,0 +1,157 @@
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Einrichtung des Buchungskalenders bestellt und dabei per Haken diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung zustimmt. Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Vertrags, aufgrund dessen WEBklar die Website des Kunden betreibt; den Buchungskalender schaltet WEBklar dann zum Vertragsende ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
- der Zustellstatus der E-Mails an den Gast.
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit der Bestellung weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(6) Ist der Buchungskalender abgeschaltet oder der Vertrag über den Betrieb der Website beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+138
View File
@@ -0,0 +1,138 @@
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
## 3. Datenarten und Betroffene
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
(7) Betroffene: Besucher der Website des Kunden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
## 9. Löschung und Rückgabe
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
@@ -0,0 +1,23 @@
# Datenschutzerklärung: Prüfpunkte für die Kundenansicht neu
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Diese Liste ändert die Datenschutzerklärung nicht. `public/datenschutz.html` bleibt unverändert (eingefroren); die Stellen unten sind die, die mit der neuen Kundenansicht und den Texten der Fassung 6 geprüft werden müssen. Die Änderung der Datenschutzerklärung erfolgt erst mit dem Umschalten (Entscheidung R14 in `docs/rechtliches/kundenansicht-entscheidungen.md`), als eigene Fassung, sonst würde sie sofort öffentlich. Zeilenangaben beziehen sich auf den Stand von `public/datenschutz.html` am 16.10.2026.
Je Prüfpunkt: **Fundstelle**, was dort steht, was sich ändert, und die Frage an den Inhaber oder die Kanzlei.
**D1** · Fundstelle `public/datenschutz.html:127`. Nachweis bei der Bestellung einer Funktion. Dort steht der „Klick auf ‚Zahlungspflichtig bestellen‘“, die Stripe-Sitzung und eine Funktionsliste nur mit News-Pinnwand und Terminplaner. Künftig löst das Einschalten einer Funktion im Abo keine Zahlung aus und hat keine Stripe-Sitzung; gespeichert werden der Wortlaut der angezeigten Zeilen, die Fassungen, Zeitpunkt des Klicks auf „Einschalten“, IP-Adresse, Browserkennung, Konto, angemeldete Person und die Kundenart (Fassungen: Abo-Bedingungen 2026-10-16-v6, Funktionsunterlagen 2026-10-16). Die Funktionsliste muss alle Funktionen nennen (News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse). Frage: Neuer Absatz „Funktion einschalten“ neben dem Absatz für Bestellungen, damit der Bestand (Funktionen, die einzeln gekauft wurden) unverändert beschrieben bleibt?
**D2** · Fundstelle `public/datenschutz.html:126`. Nachweis bei Abo, Wechsel und Zusatzspeicher. Dort stehen „gegebenenfalls Ihre Angaben zum Leistungsbeginn“ sowie Zeitpunkt, Version, IP-Adresse und Browserkennung. Künftig kommen der Wortlaut der angezeigten Zeilen (Linkzeile, Häkchen für Privatkunden) und die Kundenart zum Klickzeitpunkt hinzu (§ 2 Abs. 6 der Abo-Bedingungen Fassung 6). Frage: Genügt es, die Kundenart in den Aufzählungssatz aufzunehmen, oder braucht sie wegen D3 einen eigenen Hinweis?
**D3** · Fundstelle `public/datenschutz.html:88`. Abschnitt 6 „Kundenportal“, Kundenkonto-Absatz in der Zeile darunter. Die Kundenart („Betrieb“ oder „privat“) ist ein neues Datum im Kundenkonto. Das Team legt sie nach den Angaben des Kunden fest (Entscheidung E3); ist sie leer, gilt der Kunde als privat (E4). Zu ergänzen sind Zweck (Bestellseiten richten sich danach, Nachweis), Rechtsgrundlage (Art. 6 Abs. 1 lit. b und f DSGVO), Quelle (Angaben des Kunden an das Team) und Dauer (wie das Kundenkonto; im Nachweis wie in D1). Frage: Eigener Satz im Kundenkonto-Absatz oder in Abschnitt 7?
**D4** · Fundstelle `public/datenschutz.html:128`, ergänzend `public/datenschutz.html:115` und `public/datenschutz.html:121`. Der Stripe-Absatz beschreibt Zahlungen für Abos und Zusatzspeicher; das Einschalten einer Funktion im Abo löst keine Zahlung und keine Übermittlung an Stripe aus. Die Abschnitte zu Stripe Connect beim Webshop und beim Buchungskalender (Zahlungen der Käufer und Gäste auf das eigene Stripe-Konto des Kunden) bleiben inhaltlich wie sie sind. Frage: Genügt ein Satz, dass Funktionen im Abo ohne Zahlung eingeschaltet werden, oder sollen die Abschnitte nur an der Einleitung („bestellen“ statt „einschalten“) angepasst werden?
**D5** · Fundstelle `public/datenschutz.html:102`. Abschnitt zur Website-Analyse: „Kunden mit Wartungs-Abo“ können die Reichweitenmessung einschalten. Künftig steckt die Website-Analyse im Hosting-Abo und im Wartungs-Abo (Leistungsbeschreibung Website-Analyse Nr. 1 Abs. 1). Der Absatz muss beide Abos nennen und beschreiben, dass die Funktion mit dem Abo endet (Löschfrist 60 Tage, AVV Website-Analyse Nr. 9). Frage: Reicht eine Anpassung der Einleitung?
**D6** · Fundstellen `public/datenschutz.html:105` (Terminplaner), `public/datenschutz.html:113` (Webshop), `public/datenschutz.html:119` (Buchungskalender), außerdem der Abschnitt zur News-Pinnwand und zu Öffnungszeiten live. Die Funktionsabschnitte sprechen von „Kunden mit Wartungs-Abo“, vom „Freischalten“ und vom Ende mit dem Vertrag. Künftig gilt: Funktionen sind Teil des Wartungs-Abos, enden mit dem Abo (Terminplaner, News-Pinnwand, Webshop und Buchungskalender spätestens am Folgetag, Öffnungszeiten live nach 30 Tagen Weiterlauf) und werden nach den Fristen der Leistungsbeschreibungen gelöscht (30 Tage Terminplaner, News-Pinnwand und Öffnungszeiten live; 90 Tage Webshop und Buchungskalender). Frage: Die Fristen in den Abschnitten und in Abschnitt 7 (Speicherdauer) müssen übereinstimmen; soll Abschnitt 7 die Fristen künftig als Tabelle führen?
**D7** · Fundstelle `public/datenschutz.html:138`, ergänzend die Beschreibung des Stundenkontingents im Kundenkonto-Absatz. Das Stundenkonto und der Vorschuss für die Einrichtung (§ 4 Abs. 8 der Abo-Bedingungen Fassung 6) sind Abrechnungsdaten: Arbeitszeit, Verbrauch, Vorschuss, Ablaufdaten. Sie stehen heute unter „gegebenenfalls Ihr Stundenkontingent“. Frage: Ist der Vorschuss als Abrechnungsdatum mit demselben Zweck, derselben Rechtsgrundlage (Art. 6 Abs. 1 lit. b DSGVO) und derselben Dauer ausreichend beschrieben, oder braucht er einen eigenen Satz?
**D8** · Fundstelle `public/datenschutz.html:130`. Abschnitt zu den Seiten Kündigen und Widerrufen. Die Seiten erscheinen in der neuen Kundenansicht in der du-Form (Entscheidung R16) und zeigen angemeldeten Kunden die laufenden Widerrufsfristen aus bereits vorhandenen Daten (Sammelabfrage der Fristen im Kundenportal, `GET /api/fristen`). Das ist ein Lesen vorhandener Daten ohne neues Datum und ohne neuen Zweck; der Absatz bleibt inhaltlich unverändert. Frage: Genügt die Feststellung, dass kein neuer Absatz nötig ist, oder soll die Anzeige der Fristen im Konto erwähnt werden?
@@ -0,0 +1,144 @@
# Rechtsrunde Fassung 6: Entscheidungen, Kanzlei-Fragen und Ablauf
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Dieses Blatt ist die Grundlage für die Freigabe der Rechtstexte der Kundenansicht neu durch den Inhaber (Entscheidung E20). Die Kanzlei-Fragen halten den Livegang nicht auf; die Fragen aus den früheren Rechtsrunden stehen in `/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md` § 4. Die Entwürfe liegen unter `docs/rechtliches/*-v6.md`, die Seiten unter `public/neu/rechtliches/` (Abo-Bedingungen: `abo-bedingungen.html`, Funktionsunterlagen: `<slug>-2026-10-16.html`). Datenschutz-Prüfpunkte D1 bis D8 stehen in `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`.
## Zu entscheiden
| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |
|---|---|---|---|---|
| R1 | Fassungsnummer und Datum | Abo `2026-10-16-v6`, Funktionen `2026-10-16`; bei Freigabe auf den Freigabetag setzen | Entwurfstag bleibt (keine Umbenennung der Dateien und Fassungen) | – |
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15 Abs. 2); Funktionen gelten über Linkzeile und Klick, auch mit der Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, eine freiwillige Mail später | Info-Mail an laufende Abos mit Bitte um Zustimmung nach § 15 | KQ1 |
| R3 | Vorschuss-Regel (E17) | § 4 Abs. 8 und jede Leistungsbeschreibung außer der Analyse: echte Arbeitszeit, nie Mehraufwand, ein Rest bei Vertragsende verfällt zugunsten des Kunden | Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber | KQ2 |
| R4 | Einrichtung beim Projektstart (E9, E21) | § 4 Abs. 9 und Baustein „projekt“ der Leistungsbeschreibungen: Abrechnung über die Projektkosten, die Funktion läuft nur mit dem passenden Abo | nur in den Leistungsbeschreibungen, nicht in den Abo-Bedingungen | KQ8 |
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe des Kunden, leer gilt als privat, maßgeblich ist der Zweck des Vertrags | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
| R6 | Häkchen-Wortlaut für Abo, Wechsel und Einrichtung | Texte aus `WORTLAUT_V6.beginn` ohne Paragrafenzitate; Betriebe setzen nichts | Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen | KQ5 |
| R7 | Einrichtung in der Widerrufsfrist | Teil des Abos, kein eigener Vertrag; Widerruf beendet die Funktion; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos | WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher), das Häkchen für die Einrichtung entfällt | KQ3 |
| R8 | Ende mit dem Abo, Fristen, kein „Behalten“ (C12); mit P4/P6 abgestimmt | Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob), die Analyse wie bisher; Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter und wird dann abgeschaltet (kein Behalten gegen 30 €); Eingaben danach: Terminplaner, News und Öffnungszeiten 30 Tage, Webshop und Buchungskalender 90, Analyse 60; ein neues Abo in der Frist holt die Funktion zurück | Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab (P4 Text Z2, P6 Mail K40 und der Zeiten-Abgleich müssten dafür geändert werden) | KQ8 |
| R9 | Analyse im Hosting-Abo (C10) | in beiden Abos, Einrichtung kostenlos (C6): § 3 Abs. 8, § 6 Abs. 6, Leistungsbeschreibung A | nur im Wartungs-Abo, wie heute | KQ8 |
| R10 | AVV-Struktur | Option A: die AVV je Funktion bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
| R11 | Widerrufsbelehrung der Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibungen | KQ6 |
| R12 | Anrede der Rechtsdokumente (E15, Spec 5.7) | bleiben im bisherigen Stil („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ | Übersichtsbox und Leistungsbeschreibungen auf „du“ | – |
| R13 | Webshop nur für Betriebe, Terminplaner nicht im Gesundheitsbereich (C15) | steht in den Leistungsbeschreibungen, das Team prüft bei der Freigabe | Webshop auch für Privatkunden mit Hinweis | KQ4, KQ9 |
| R14 | Datenschutzerklärung | Prüfpunkte D1 bis D8; `public/datenschutz.html` wird erst mit dem Umschalten als eigene Fassung geändert (sonst sofort öffentlich) | sofort ändern | – |
| R15 | Zusatzspeicher und Übernahmegutschrift | Zusatzspeicher: Linkzeile ohne Häkchen (§ 10 Abs. 3 und Hinweise zum Widerruf Nr. 4: kein Wertersatz); Gutschrift: Satz über dem Knopf (P7) | das Häkchen bleibt | KQ5 |
| R16 | Du-Anrede der Seiten Kündigen und Widerrufen (N38) | beide Seiten sprechen ab Fassung 6 „du“; sie speichern keinen Wortlaut und haben keine eigene Version (C14); Belehrung und Muster-Formular folgen R12 | Sie-Form bleibt bis zur Freigabe von Belehrung und Formular | KQ6 |
| R17 | Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37) | beide Zeilen entfallen im Block; der Anbieter steht im Impressum im Konto und in § 1 der Bedingungen, die Abrechnung (monatlich im Voraus per Stripe) in den Zeilen zu Preis und Fälligkeit und in § 7 | beide Zeilen bleiben im Block über dem Knopf | KQ6 |
| R18 | Länge des Blocks „Was du bestellst“ am Handy (Abschlussprüfung F1) | Der Block über dem Knopf ist knapp: Leistung als eine Zeile mit Link „Leistungen ansehen“ auf die Leistungsbeschreibung, Preis, Fällig, Laufzeit, Beginn und Zahlung in je ein bis drei Zeilen; die ausführlichen Sätze (Leistung, Laufzeit mit Kündigungsfrist, Zahlungsweg, Gutschrift und Abbuchungstag beim Wechsel) stehen unter dem Knopf („Im Einzelnen“), ohne Klappe. So stehen Kerninfo und Knopf auch auf 375 × 667 zusammen im nutzbaren Bereich (zwischen Kopf und Leiste) | voller Wortlaut im Block über dem Knopf: am Handy dann nicht mehr auf einem Bildschirm mit dem Knopf | KQ6 |
## Fragen an die Kanzlei
Jede Frage hat vier Teile wie in `docs/rechtliches/zeiten-kanzlei-fragen.md`: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Annahmen sind keine Rechtsurteile.
### KQ1: Geltung der Fassung 6 und Information an laufende Abos
**Frage.** Fassung 6 gilt nur für neue Abschlüsse. Wer ein laufendes Abo nach Fassung 3 bis 5 hat und eine Funktion einschaltet, stimmt Leistungsbeschreibung und Auftragsverarbeitung der Funktion in der angezeigten Fassung durch den Klick auf „Einschalten“ zu; dazu gehören die Regeln zur Einrichtung (Stundenkontingent) und zum Vorschuss. Reicht der Klick, um diese Regeln in das laufende Abo einzubeziehen, oder braucht es eine Zustimmung nach dem bisherigen § 15 (Mitteilung mit sechs Wochen Frist, Zustimmung)? Muss WEBklar laufende Kunden über Fassung 6 informieren, obwohl sie für sie nur vorteilhaft ist?
**Fundstelle im Text.** § 15 Abs. 2 der Abo-Bedingungen Fassung 6; § 3 Abs. 8; § 4 Abs. 8; Nr. 1 Abs. 1 der Leistungsbeschreibungen (Baustein „geltung“).
**Unsere Annahme.** Der Klick genügt, weil jede Funktion ein zusätzliches Angebot ohne Gebühr ist und die Änderung des Abos nur für neue Abschlüsse gilt. Eine Info-Mail ist keine Pflicht; wir würden später freiwillig informieren.
**Alternative.** Info-Mail an alle laufenden Abos mit Bitte um Zustimmung nach § 15; Funktionen für Altkunden erst nach dieser Zustimmung.
### KQ2: Vorschuss-Regel für die Einrichtung
**Frage.** Die Einrichtung einer Funktion geht mit der tatsächlich aufgewendeten Zeit vom Stundenkontingent ab. Reicht es nicht, entsteht ein Vorschuss, den die folgenden Monatskontingente ausgleichen; ein Rest bei Vertragsende verfällt zugunsten des Kunden, und es wird nie Mehraufwand berechnet. Ist das transparent und wirksam, auch gegenüber Verbrauchern (Überraschungsklausel, Kontingent als Vorleistung)? Braucht es eine Obergrenze oder eine Rückfrage?
**Fundstelle im Text.** § 4 Abs. 2 und 8 der Abo-Bedingungen Fassung 6; Nr. 5 Abs. 2 (Terminplaner), Nr. 3 Abs. 1 (News-Pinnwand), Nr. 4 Abs. 1 (Öffnungszeiten live), Nr. 9 Abs. 1 (Webshop, Buchungskalender).
**Unsere Annahme.** Die Regel ist für den Kunden günstig (keine zusätzliche Rechnung, Verfall zu seinen Gunsten) und wird im Kundenportal mit „Vorschuss für Einrichtung“ angezeigt.
**Alternative.** Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber; oder Einrichtung nur, soweit das Kontingent reicht.
### KQ3: Einrichtung in der Widerrufsfrist
**Frage.** Das Einschalten einer Funktion ist nach unserem Text Teil des Abos und kein eigener Vertrag; es verlängert die Widerrufsfrist nicht. Ist das richtig, oder ist die Einrichtung eine eigene Leistung mit eigenem Widerrufsrecht? Erlischt das Widerrufsrecht nach § 356 Abs. 5 Nr. 2 BGB bei einem Dauerschuldverhältnis überhaupt durch „vollständige Erbringung“, und wie bemisst sich der Wertersatz nach § 357a Abs. 2 BGB für Einrichtungszeit innerhalb der Frist?
**Fundstelle im Text.** Hinweise zum Widerruf Nr. 5 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; Baustein „widerruf“ der Leistungsbeschreibungen; `WORTLAUT_V6.beginn.einrichtung` in `server/services/products.js`.
**Unsere Annahme.** Teil des Abos; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos; das Häkchen gibt es nur für Privatkunden in der Frist.
**Alternative.** WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher nach § 10 Abs. 3) und das Häkchen für die Einrichtung entfällt.
### KQ4: Belastbarkeit der Kundenart
**Frage.** Das Team hinterlegt die Kundenart („Betrieb“ oder „privat“) nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat; maßgeblich bleibt der Zweck des Vertrags (§ 1 Abs. 2). Wie belastbar ist das, wenn sich später ein Verbraucher als Betrieb geführt herausstellt (Existenzgründer, Vereine, Freiberufler)? Wer trägt das Risiko einer fehlenden Belehrung? Das ist die Frage 5 aus `recht.md` § 4.
**Fundstelle im Text.** § 1 Abs. 2 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; `WORTLAUT_V6.beginn`; Nr. 1 Abs. 1 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop.
**Unsere Annahme.** Unsere Kunden sind fast nur Betriebe (E2). Widerrufsbelehrung, Muster-Formular und die Funktion „Vertrag widerrufen“ bleiben für alle erreichbar; nur das Häkchen und der Beginn nach der Frist hängen an der Kundenart.
**Alternative.** Selbstauskunft des Kunden bei der Bestellung („Ich bestelle als Unternehmer“) oder das Häkchen für alle Kunden.
### KQ5: Häkchentexte ohne Normzitat, Wechsel nur mit Häkchen
**Frage.** Dürfen die Häkchen für den Sofortbeginn auf zwei bis drei Sätze ohne Paragrafenzitate gekürzt werden, ohne die Information nach Art. 246a § 1 Abs. 3 EGBGB über das Erlöschen und den Wertersatz zu verlieren (Frage 2 aus `recht.md` § 4)? Ist es zulässig, dass der Wechsel ins Wartungs-Abo für Privatkunden nur mit Häkchen möglich ist, weil er sofort wirkt? Genügt beim Zusatzspeicher und bei der Übernahmegutschrift eine Linkzeile ohne Häkchen, weil WEBklar auf Wertersatz verzichtet?
**Fundstelle im Text.** `WORTLAUT_V6.beginn` (Abo, Wechsel, Einrichtung); § 2 Abs. 2 c und § 9 Abs. 6 der Abo-Bedingungen Fassung 6; § 10 Abs. 3 und Hinweise zum Widerruf Nr. 4.
**Unsere Annahme.** Die Wortlaute der Konstante genügen; für Privatkunden ist der Wechsel ohne Häkchen gesperrt; Zusatzspeicher und Gutschrift brauchen kein Häkchen.
**Alternative.** Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen; Wechsel für Privatkunden mit Beginn nach Ablauf der Frist.
### KQ6: Belehrung und Pflichtinformationen per Link und PDF, Anrede der Seiten Kündigen und Widerrufen
**Frage.** Genügt der Link auf Widerrufsbelehrung und Muster-Formular vor der Bestellung, wenn der Volltext erst im Bestätigungs-PDF steht (Frage 4 aus `recht.md` § 4)? Passen Belehrung und Formular zum Gestaltungshinweis in Anlage 1 EGBGB zu § 356a? Dürfen die Zeilen „Anbieter“ und „Abrechnung“ im Block über dem Knopf entfallen, solange der Anbieter im Impressum und in § 1 steht und die Abrechnung in den Zeilen zu Preis und Fälligkeit? Ändert die Du-Anrede der Seiten Kündigen und Widerrufen etwas an der Wirksamkeit von Kündigung (§ 312k BGB) und Widerruf (§ 356a BGB)?
**Fundstelle im Text.** § 2 Abs. 2 c, Widerrufsbelehrung, Muster-Widerrufsformular und Hinweise zum Widerruf der Abo-Bedingungen Fassung 6; Seiten `kuendigen.html` und `widerrufen.html` in der neuen Ansicht; Block „Was du bestellst“ der Bestellseiten.
**Unsere Annahme.** Der Link genügt, das PDF kommt vor Leistungsbeginn per E-Mail (§ 312f BGB); Anbieter und Abrechnung sind an anderer Stelle erreichbar; die Du-Anrede ändert die Wirksamkeit nicht.
**Alternative.** Volltext der Belehrung auf der Bestellseite; Zeilen „Anbieter“ und „Abrechnung“ bleiben im Block; Sie-Form der beiden Seiten.
### KQ7: AVV per Klick auf die Linkzeile, Struktur A, B oder C
**Frage.** Genügt der Klick auf „Einschalten“ unter der Linkzeile als Abschluss des Auftragsverarbeitungsvertrags nach Art. 28 Abs. 3 und 9 DSGVO („schriftlich, auch elektronisch“), ohne eigenes Häkchen (Frage 3 aus `recht.md` § 4)? Welche Struktur ist am sichersten: A (eigene Verträge je Funktion), B (Anlagen zu den Abo-Bedingungen) oder C (Rahmen-AVV mit Anlage je Funktion)?
**Fundstelle im Text.** § 14 Abs. 2 der Abo-Bedingungen Fassung 6; Vorbemerkung Abs. 1 jedes AVV in `docs/rechtliches/avv-*-v6.md`; Abschnitt „AVV-Struktur“ unten.
**Unsere Annahme.** Der Klick genügt, weil Wortlaut, Fassung, Zeit, Konto und angemeldete Person gespeichert werden; wir bleiben bei Option A.
**Alternative.** Ein eigenes Häkchen für den AVV; Option B oder C.
### KQ8: Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Rückkehr in der Frist
**Frage.** Sind die Funktionen als Teil der Abo-Leistung wirksam geregelt (§ 3 Abs. 8), einschließlich der Kopplung an das Abo (§ 11 Abs. 4: Ende mit dem Abo, Weiterlauf von Öffnungszeiten live für 30 Tage, Löschfristen von 30, 60 und 90 Tagen)? Ist es zulässig, dass ein neues Abo in der Frist die Funktion mit den vorhandenen Daten ohne neue Zustimmung zurückholt? Tragen die Analyse im Hosting-Abo (C10) und die Abrechnung der Einrichtung beim Projektstart über die Projektkosten (E9, E21) ohne weitere Hinweise?
**Fundstelle im Text.** § 3 Abs. 8, § 4 Abs. 9 und § 11 Abs. 4 der Abo-Bedingungen Fassung 6; Abschnitt „Ende der Leistung“ jeder Leistungsbeschreibung; Nr. 10 Abs. 5 der Leistungsbeschreibungen Webshop und Buchungskalender („auf Wunsch in Textform“).
**Unsere Annahme.** Die Regeln sind als Leistungsbeschreibung wirksam; die Rückkehr in der Frist holt die Funktion zurück. Offen ist für uns, ob die Rückkehr bei Webshop und Buchungskalender wie heute „auf Wunsch in Textform“ oder automatisch geschehen soll.
**Alternative.** Öffnungszeiten live wie die übrigen Funktionen am Folgetag abschalten (R8); Rückkehr nur auf Wunsch in Textform; einheitliche Löschfrist von 30 Tagen.
### KQ9: Ausschlüsse als Vertragsklausel
**Frage.** Sind die Ausschlüsse in den Leistungsbeschreibungen als Leistungsgrenzen wirksam: Terminplaner nicht im Gesundheitsbereich, Webshop nur für Betriebe und ohne die ausgeschlossenen Waren, Buchungskalender nur für Unternehmer? Gilt das auch, wenn die Prüfung beim Einschalten nur durch das Team oder durch eine Selbstangabe im Fragebogen erfolgt? Darf WEBklar bei Verstößen abschalten?
**Fundstelle im Text.** Nr. 4 Abs. 3 der Leistungsbeschreibung Terminplaner; Nr. 1 Abs. 1, Nr. 3 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop; Nr. 1 Abs. 1 der Leistungsbeschreibung Buchungskalender.
**Unsere Annahme.** Die Ausschlüsse sind als Leistungsbeschreibung wirksam; das Team prüft bei der Freigabe (R13).
**Alternative.** Webshop auch für Privatkunden mit Hinweis (C15 verschoben); keine automatische Abschaltung, nur ein Hinweis in Textform.
## AVV-Struktur
Die Auftragsverarbeitung der Funktionen ist in Fassung 6 so geregelt, dass der Kunde sie mit dem Klick auf „Einschalten“ unter der Linkzeile abschließt. Drei Aufbauten sind möglich; empfohlen wird Option A.
**Option A, empfohlen: eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen.** Die AVV Terminplaner, Webshop, Buchungskalender und Website-Analyse bleiben eigene Dokumente mit einer neuen Vorbemerkung (Abschluss per Klick) und gelten neben Anlage 1 der Abo-Bedingungen (§ 14 Abs. 2). Vorteil: jede Funktion hat ihre eigenen Datenarten, Fristen und Unterauftragsverarbeiter, und die bestehenden Verträge ändern sich kaum. Nachteil: vier Einzelverträge, deren Gleichlauf mit Anlage 1 bei jeder Änderung geprüft werden muss.
**Option B: Anlagen 4 bis 7 zu den Abo-Bedingungen.** Die AVV der Funktionen werden Anlagen der Abo-Bedingungen. Vorteil: ein Dokument mit einer Fassung, der Kunde stimmt den Bedingungen einmal zu. Nachteil: die Abo-Bedingungen werden lang und müssen bei jeder Änderung einer Funktion neu versioniert werden; Funktionen, die ein Kunde nie nutzt, stehen trotzdem in seinem Vertrag.
**Option C: Rahmen-AVV mit einer Anlage je Funktion.** Ein neuer Rahmenvertrag regelt das Gemeinsame (Weisungen, Sicherheit, Löschung), die Anlagen je Funktion nennen nur Daten und Unterauftragsverarbeiter. Vorteil: einheitliche Regeln, bei Änderungen ändert sich nur die Anlage. Nachteil: ein neuer Vertrag neben Anlage 1, abzustimmen mit der Kanzlei, und mehr Arbeit als jetzt nötig.
## Ablauf nach der Freigabe
Die Reihenfolge ist fest; jeder Schritt erst, wenn der vorige grün ist. Die Skripte laufen nur auf ausdrückliche Anweisung des Controllers nach der Freigabe.
1. Freigabe des Inhabers je Punkt (R1 bis R17). Die Antworten auf die Kanzlei-Fragen KQ1 bis KQ9 halten den Livegang nicht auf (E20). Entschiedene Änderungen werden zuerst in den Texten und Tests nachgezogen.
2. Datum der Fassungen auf den Freigabetag setzen: `config.abo.termsVersionNeu` (Abo), `FUNKTIONEN_FASSUNG_V6` in `server/services/products.js`, die Datei- und Testnamen mit dem Datum (`<slug>-2026-10-16`), die Fassungszeilen der Markdown-Dateien, `Version 6 · Stand …` und den Druckkopf in der Abo-Seite.
3. Entwurfsmarke aus allen Dateien entfernen (Fassungszeilen der Markdown-Dateien, der Absatz unter der Kopfzeile und das meta robots in der Abo-Seite), `ENTWURF_V6 = false` in `scripts/build-legal-html.mjs`, und `ENTWURF = false` in `test/aboBedingungenV6.test.js`, `test/funktionenRechtV6.test.js` und `test/kundenansichtEntscheidungen.test.js`. Auch der Vermerk im Kopf dieser Datei und der Prüfpunkte-Datei fällt weg.
4. `node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6` erzeugt die zehn Seiten neu; `scripts/build-legal-html.mjs funktionen6 --check` meldet danach nichts.
5. `free -m` (mindestens 2000 MB verfügbar), dann `nice -n 10 scripts/build-legal-pdfs.sh abo6` und `nice -n 10 scripts/build-legal-pdfs.sh funktionen6`, einzeln und nie parallel (`FORCE=1` nur nach einer Textänderung). Die PDFs und die `.sha256`-Dateien werden mit committet.
6. `node scripts/rechtliches-hochladen.mjs --probe`. Meldet der Vorflug `STOPP: Karte fehlt: …` (laut `setup-legal.mjs` des WOMS fehlen drei der elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst `node scripts/rechtliches-hochladen.mjs --karten-anlegen`, danach den Vorflug wiederholen. Ist er grün, folgt ein Aufruf je Dokument (elf Aufrufe): `node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT`. Zuletzt `node scripts/rechtliches-hochladen.mjs --pruefen`: Exit 0 heißt, alle elf Dokumente haben eine aktuelle Version mit passender Quelle; sonst weiter bei Schritt 6.
7. Erst danach `KUNDENANSICHT_NEU` auf `on` (P10); P10 prüft vorher, dass `WORTLAUT_STATUS` auf `'freigegeben'` steht und die Gleichheitstests grün sind (Nachtrag N48). Die Verdrahtung der Analyse (`config.analyse.avvVersion`, `legalPaths`, die Weiche der Mail K18) folgt der Freigabe (Nachtrag N33); P10 verschiebt Seite und PDF der Abo-Bedingungen nach `public/`.
@@ -0,0 +1,45 @@
# Leistungsbeschreibung News-Pinnwand
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.
## 2. Leistung
(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.
(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.
(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.
## 3. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.
(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.
## 5. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.
(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).
## 6. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
@@ -0,0 +1,125 @@
# Leistungsbeschreibung Terminplaner
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
## 2. Leistung
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
## 3. Nicht enthalten
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
## 4. Voraussetzungen und Pflichten des Kunden
(1) Der Kunde hat ein laufendes Wartungs-Abo.
(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
## 5. Einrichtung
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
## 6. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 7. Haftung für verpasste Termine
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
## 8. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.
## 9. Änderungen und Einstellung
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 10. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 11. Hinweise
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
@@ -0,0 +1,192 @@
# Leistungsbeschreibung Buchungskalender
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.
## 2. Leistung
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
(3) So läuft eine Buchung ab:
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
(4) Im Kundenportal verwaltet der Vermieter:
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
- Kalender mit manuellen Sperren,
- die Verbindungen zu anderen Kalendern (Nr. 4),
- seine Rechnungen als PDF,
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
(7) Nicht enthalten sind:
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
- Sofortbuchung ohne Freigabe,
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
- die Buchführung des Vermieters.
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
## 5. Pflichten des Vermieters
(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
## 8. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Haftung
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
## 12. Änderungen und Einstellung
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 13. Hinweise
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
@@ -0,0 +1,194 @@
# Leistungsbeschreibung Buchungskalender
**Fassung 2026-10-07** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Vertrag über den Betrieb der Website
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Leistungsbeschreibung ist eine Zusatzleistung zum Vertrag über den Betrieb der Website: Sie setzt voraus, dass WEBklar die Website des Vermieters aufgrund eines Vertrags betreibt, etwa im Hosting-Abo, im Wartungs-Abo oder aufgrund eines anderen Vertrags über den Betrieb der Website. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
(2) Sie wird vereinbart, wenn der Vermieter die Einrichtung des Buchungskalenders im Kundenportal bestellt und dabei dieser Leistungsbeschreibung, dem Auftragsverarbeitungsvertrag Buchungskalender („AVV Buchungskalender“) und der Widerrufsbelehrung zustimmt. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Bestellen kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, ein Einrichtungspreis fällt nur an, wenn er vereinbart ist.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders. Das gilt auch im Hosting-Abo.
## 2. Leistung
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter die Einrichtung bestellen.
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
(3) So läuft eine Buchung ab:
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche „Zahlungspflichtig buchen“. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
(4) Im Kundenportal verwaltet der Vermieter:
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
- Kalender mit manuellen Sperren,
- die Verbindungen zu anderen Kalendern (Nr. 4),
- seine Rechnungen als PDF,
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
(7) Nicht enthalten sind:
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
- Sofortbuchung ohne Freigabe,
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
- die Buchführung des Vermieters.
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 5).
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit der Zustimmung bei der Bestellung, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Die Zustimmung zu diesem Auftrag gibt der Vermieter bei der Bestellung und bestätigt sie in den Einstellungen des Buchungskalenders. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
## 5. Pflichten des Vermieters
(1) Der Vermieter hat einen laufenden Vertrag mit WEBklar über den Betrieb seiner Website (Hosting-Abo, Wartungs-Abo oder ein anderer Vertrag über den Betrieb der Website), ist Unternehmer und vermietet in eigenem Namen. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab.
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche „Zahlungspflichtig buchen“ die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
## 8. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 9. Preis, Abrechnung und Widerruf
(1) Die Einrichtung kostet 199 € einmalig je Website-Projekt, ohne Umsatzsteuer gemäß § 19 UStG. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen. Der Buchungskalender läuft, solange WEBklar die Website betreibt (Nr. 10).
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab der Bestellung. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Der Vermieter bezahlt bei der Bestellung im Kundenportal über Stripe und erhält eine Rechnung. Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt (Nr. 1 Abs. 2), gilt die dafür getroffene Vereinbarung.
(4) Der Preis steht neben den Leistungen eines Abos: Die Einrichtung wird weder auf ein Stundenkontingent angerechnet noch damit bezahlt.
(5) Mehraufwand, der nicht zur Einrichtung gehört (Nr. 2 Abs. 7), führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus. WEBklar nennt vorher den geschätzten Aufwand und berechnet 100 € je Stunde in Schritten von 15 Minuten, ohne Umsatzsteuer gemäß § 19 UStG (§ 4 der Abo-Bedingungen). Der Mehraufwand wird gesondert berechnet und nicht auf ein Stundenkontingent angerechnet.
(6) Verbraucher können die Bestellung der Einrichtung nach der Widerrufsbelehrung widerrufen. Belehrung und Muster-Widerrufsformular stehen in der Anlage „Widerrufsbelehrung und Muster-Widerrufsformular (Einrichtung von Funktionen)“. Ob das Widerrufsrecht erlischt und ob der Vermieter einen angemessenen Betrag für die bis dahin erbrachte Leistung schuldet, richtet sich nach der Zustimmung, die er bei der Bestellung gibt, und nach den Hinweisen zum Widerruf in der Anlage. Mit dem Widerruf endet der Buchungskalender. Dieses Widerrufsrecht des Vermieters gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
## 10. Laufzeit und Ende
(1) Der Buchungskalender läuft, solange WEBklar die Website des Vermieters betreibt. Endet der Vertrag über den Betrieb der Website, endet auch der Buchungskalender; WEBklar schaltet ihn zum Vertragsende ab und informiert den Vermieter vorher per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen. Das gilt auch beim Widerruf dieses Vertrags.
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Vertrag über den Betrieb der Website, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (neuer Preis nach Nr. 9).
(6) Endet der Vertrag über den Betrieb der Website, bevor WEBklar den Buchungskalender freigeschaltet hat, erstattet WEBklar den gezahlten Einrichtungspreis vollständig.
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab. Eine Erstattung des Einrichtungspreises folgt daraus nicht; für den Widerruf gilt Nr. 9 Abs. 6.
## 11. Haftung
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
## 12. Änderungen und Einstellung
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 13. Hinweise
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
@@ -0,0 +1,179 @@
# Leistungsbeschreibung Webshop
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.
## 2. Leistung
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
(3) Im Kundenportal verwaltet der Händler:
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Ausgeschlossene Waren und Leistungen
(1) Über den Webshop dürfen nicht verkauft werden:
- alkoholische Getränke,
- Tabakwaren, E-Zigaretten und Zubehör,
- Arzneimittel und Medizinprodukte,
- Waffen, Munition, Feuerwerk und Sprengstoffe,
- Waren mit erotischem oder pornografischem Inhalt,
- lebende Tiere,
- digitale Inhalte und Downloads,
- Gutscheine, Geschenkkarten und Guthaben,
- Abonnements und andere wiederkehrende Zahlungen,
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.
## 4. Zahlungen über Stripe
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
## 5. Pflichten des Händlers
(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
## 6. Muster-Rechtstexte, keine Rechtsberatung
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
## 7. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 8. Haftung
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Änderungen und Einstellung
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
@@ -0,0 +1,109 @@
# Leistungsbeschreibung Website-Analyse
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.
## 2. Leistung
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
- ein Messskript in jede HTML-Datei der Website,
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
(3) Gemessen wird:
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
## 3. Voraussetzungen
(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
## 5. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 6. Hinweis zur Rechtslage
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
## 7. Ende der Leistung
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
## 8. Änderungen und Einstellung
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 9. Hinweise
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
@@ -0,0 +1,135 @@
# Leistungsbeschreibung Öffnungszeiten live
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.
## 2. Leistung
(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).
(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:
- eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),
- auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,
- die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,
- eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und
- im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.
(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.
(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.
## 3. Einbau und Freischaltung
(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.
(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.
(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.
(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.
## 4. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 5. Nicht enthalten
(1) Nicht enthalten sind:
- die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,
- Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,
- eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,
- die Pflege der Zeiten und der Hinweise durch WEBklar und
- Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).
## 6. Pflichten des Kunden
(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.
(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.
(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.
(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).
## 7. Verfügbarkeit und Aktualität
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.
(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.
(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.
## 8. Haftung
(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.
(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.
(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.
(4) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
(5) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.
(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.
## 9. Ende der Leistung
(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.
(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.
(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.
(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.
(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.
## 10. Änderungen und Einstellung
(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.
(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 11. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.
(2) **Datenschutz.** Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).
Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.
@@ -0,0 +1,103 @@
Normen, auf die sich die Texte der Auftragsbestätigung, der Kundenseiten und der Mails stützen (Auszüge, unverändert, abgerufen 2026-10-06 von gesetze-im-internet.de).
Die Datei dient als Nachweis der Zählung; test/auftraegeRechtstexte.test.js gleicht die Zitate in den Texten mit ihr ab.
Prüfung vom 06.10.2026 (zweiter Abruf von § 649 und § 650 BGB): Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“). Absatz 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB regelt dagegen den Werklieferungsvertrag und Verbraucherverträge über die Herstellung digitaler Produkte; ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch. Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (AB_TEXTE.aufwand) stützt sich auf § 649 Absatz 2.
Quelle: https://www.gesetze-im-internet.de/bgb/__312f.html (Auszug: § 312f Absatz 1 und 2 BGB)
(1) Bei außerhalb von Geschäftsräumen geschlossenen Verträgen ist der Unternehmer verpflichtet, dem Verbraucher alsbald auf Papier zur Verfügung zu stellen
1.
eine Abschrift eines Vertragsdokuments, das von den Vertragsschließenden so unterzeichnet wurde, dass ihre Identität erkennbar ist, oder
2.
eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist.
Wenn der Verbraucher zustimmt, kann für die Abschrift oder die Bestätigung des Vertrags auch ein anderer dauerhafter Datenträger verwendet werden. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben nur enthalten, wenn der Unternehmer dem Verbraucher diese Informationen nicht bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt hat.
(2) Bei Fernabsatzverträgen ist der Unternehmer verpflichtet, dem Verbraucher eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist, innerhalb einer angemessenen Frist nach Vertragsschluss, spätestens jedoch bei der Lieferung der Ware oder bevor mit der Ausführung der Dienstleistung begonnen wird, auf einem dauerhaften Datenträger zur Verfügung zu stellen. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben enthalten, es sei denn, der Unternehmer hat dem Verbraucher diese Informationen bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt.
Quelle: https://www.gesetze-im-internet.de/bgb/__355.html (Auszug: § 355 Absatz 1 BGB)
(1) Wird einem Verbraucher durch Gesetz ein Widerrufsrecht nach dieser Vorschrift eingeräumt, so sind der Verbraucher und der Unternehmer an ihre auf den Abschluss des Vertrags gerichteten Willenserklärungen nicht mehr gebunden, wenn der Verbraucher seine Willenserklärung fristgerecht widerrufen hat. Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen. Der Widerruf muss keine Begründung enthalten. Zur Fristwahrung genügt die rechtzeitige Absendung des Widerrufs.
Quelle: https://www.gesetze-im-internet.de/bgb/__356.html (Auszug: § 356 Absatz 4 bis 6 BGB)
(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt. Bei Verträgen über Finanzdienstleistungen gilt Satz 1 nicht, wenn der Verbraucher nicht nach Artikel 246b § 1 Absatz 1 Nummer 16 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche über sein Widerrufsrecht belehrt wurde.
(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer die Dienstleistung vollständig erbracht hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung
a)
ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,
b)
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und
c)
seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,
3.
bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat, ihn aufzusuchen, um Reparaturarbeiten auszuführen, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher die in Nummer 2 Buchstabe a und b genannten Voraussetzungen erfüllt hat,
4.
bei einem Vertrag über die Erbringung von Finanzdienstleistungen, wenn der Vertrag von beiden Seiten auf ausdrücklichen Wunsch des Verbrauchers vollständig erfüllt ist, bevor der Verbraucher sein Widerrufsrecht ausübt.
(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung von nicht auf einem körperlichen Datenträger befindlichen digitalen Inhalten auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer mit der Vertragserfüllung begonnen hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, wenn
a)
der Unternehmer mit der Vertragserfüllung begonnen hat,
b)
der Verbraucher ausdrücklich zugestimmt hat, dass der Unternehmer mit der Vertragserfüllung vor Ablauf der Widerrufsfrist beginnt,
c)
der Verbraucher seine Kenntnis davon bestätigt hat, dass durch seine Zustimmung nach Buchstabe b mit Beginn der Vertragserfüllung sein Widerrufsrecht erlischt, und
d)
der Unternehmer dem Verbraucher eine Bestätigung gemäß § 312f zur Verfügung gestellt hat.
Quelle: https://www.gesetze-im-internet.de/bgb/__356a.html (Auszug: § 356a Absatz 2 und 4 BGB)
(2) Die Widerrufsfunktion muss dem Verbraucher ermöglichen, eine Widerrufserklärung an den Unternehmer zu übermitteln und dem Unternehmer in oder mit der Widerrufserklärung ohne Weiteres folgende Informationen bereitzustellen oder zu bestätigen:
1.
den Namen des Verbrauchers,
2.
Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,
3.
Angaben zum elektronischen Kommunikationsmittel, mit welchem dem Verbraucher eine Eingangsbestätigung für den Widerruf zu übermitteln ist.
(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.
Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html (Auszug: § 357a Absatz 2 BGB)
(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht, oder die bis zum Widerruf erfolgte Lieferung von Wasser, Gas oder Strom in nicht bestimmten Mengen oder nicht begrenztem Volumen oder von Fernwärme zu leisten, wenn
1.
der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,
2.
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag der Verbraucher das Verlangen nach Nummer 1 auf einem dauerhaften Datenträger übermittelt hat und
3.
der Unternehmer den Verbraucher nach Artikel 246a § 1 Absatz 2 Satz 1 Nummer 1 und 3 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche ordnungsgemäß informiert hat.
Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen. Ist der vereinbarte Gesamtpreis unverhältnismäßig hoch, so ist der Wertersatz auf der Grundlage des Marktwerts der erbrachten Leistung zu berechnen.
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_246a__1.html (Auszug: Artikel 246a § 1 Absatz 3 EGBGB)
(3) Der Unternehmer hat den Verbraucher auch zu informieren, wenn
1.
dem Verbraucher nach § 312g Absatz 2 Nummer 1, 2, 5 und 7 bis 13 des Bürgerlichen Gesetzbuchs ein Widerrufsrecht nicht zusteht, dass der Verbraucher seine Willenserklärung nicht widerrufen kann, oder
2.
das Widerrufsrecht des Verbrauchers nach § 312g Absatz 2 Nummer 3, 4 und 6 sowie § 356 Absatz 5 und 6 des Bürgerlichen Gesetzbuchs vorzeitig erlöschen kann, über die Umstände, unter denen der Verbraucher ein zunächst bestehendes Widerrufsrecht verliert.
Quelle: https://www.gesetze-im-internet.de/bgb/__640.html (Auszug: § 640 Absatz 2 BGB)
(2) Als abgenommen gilt ein Werk auch, wenn der Unternehmer dem Besteller nach Fertigstellung des Werks eine angemessene Frist zur Abnahme gesetzt hat und der Besteller die Abnahme nicht innerhalb dieser Frist unter Angabe mindestens eines Mangels verweigert hat. Ist der Besteller ein Verbraucher, so treten die Rechtsfolgen des Satzes 1 nur dann ein, wenn der Unternehmer den Besteller zusammen mit der Aufforderung zur Abnahme auf die Folgen einer nicht erklärten oder ohne Angabe von Mängeln verweigerten Abnahme hingewiesen hat; der Hinweis muss in Textform erfolgen.
Quelle: https://www.gesetze-im-internet.de/bgb/__649.html (§ 649 BGB, erneut abgerufen 2026-10-06; die amtliche Überschrift lautet „Kostenanschlag“)
§ 649 Kostenanschlag
(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden, ohne dass der Unternehmer die Gewähr für die Richtigkeit des Anschlags übernommen hat, und ergibt sich, dass das Werk nicht ohne eine wesentliche Überschreitung des Anschlags ausführbar ist, so steht dem Unternehmer, wenn der Besteller den Vertrag aus diesem Grund kündigt, nur der im § 645 Abs. 1 bestimmte Anspruch zu.
(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.
Quelle: https://www.gesetze-im-internet.de/bgb/__650.html (Auszug: § 650 Absatz 2 BGB, zur Abgrenzung: § 650 ist nicht der Kostenanschlag; er regelt den Werklieferungsvertrag und Verbraucherverträge über digitale Produkte)
§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte
(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet,
1.
digitale Inhalte herzustellen,
2.
einen Erfolg durch eine digitale Dienstleistung herbeizuführen oder
3.
einen körperlichen Datenträger herzustellen, der ausschließlich als Träger digitaler Inhalte dient,
sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden. An die Stelle der nach Satz 1 nicht anzuwendenden Vorschriften treten die Vorschriften des Abschnitts 3 Titel 2a. Die §§ 641, 644 und 645 sind mit der Maßgabe anzuwenden, dass an die Stelle der Abnahme die Bereitstellung des digitalen Produkts (§ 327b Absatz 3 bis 5) tritt.
Quelle: https://www.gesetze-im-internet.de/ao_1977/__147.html (Auszug: § 147 Absatz 1 Nummer 2 bis 4, Absatz 3 Satz 1 und Absatz 4 AO)
2.
die empfangenen Handels- oder Geschäftsbriefe,
3.
Wiedergaben der abgesandten Handels- oder Geschäftsbriefe,
4.
Buchungsbelege,
(3) Die in Absatz 1 Nummer 1 und 4a aufgeführten Unterlagen sind zehn Jahre, die in Absatz 1 Nummer 4 aufgeführten Unterlagen acht Jahre und die sonstigen in Absatz 1 aufgeführten Unterlagen sechs Jahre aufzubewahren, sofern nicht in anderen Steuergesetzen kürzere Aufbewahrungsfristen zugelassen sind.
(4) Die Aufbewahrungsfrist beginnt mit dem Schluss des Kalenderjahrs, in dem die letzte Eintragung in das Buch gemacht, das Inventar, die Eröffnungsbilanz, der Jahresabschluss oder der Lagebericht aufgestellt, der Handels- oder Geschäftsbrief empfangen oder abgesandt worden oder der Buchungsbeleg entstanden ist, ferner die Aufzeichnung vorgenommen worden ist oder die sonstigen Unterlagen entstanden sind.
@@ -0,0 +1,22 @@
# Entwurf: Abschnitt 6f der Datenschutzerklärung (Buchungskalender)
Stand 2026-10-07, Fassung der Rechtstexte 2026-10-07. **Entwurf – Prüfung durch Kanzlei offen.** Der Text kommt als Abschnitt `6f` in `public/datenschutz.html`, hinter 6d (Webshop; 6e ist für die Öffnungszeiten reserviert und noch nicht live) und vor Abschnitt 7. Er wird erst mit dem Livegang übernommen (`docs/funktionen/unterkunft-livegang.md`, Schritt 9), danach die PDF-Fassung `scripts/build-legal-pdfs.sh datenschutz` neu drucken. Mit der Übernahme zugleich im Abschnitt 12 „Empfänger im Überblick“ die Zeile zu Stripe und in Abschnitt 3 die Verweise prüfen. Der Text ist an Abschnitt 6d angelehnt und beschreibt, was die Software tut; ändert sich der AVV Buchungskalender, hier mitändern.
## Text (als HTML-Absätze übernehmen)
**6f. Buchungskalender auf Websites unserer Kunden**
Kunden können auf ihrer eigenen Website einen Buchungskalender für ihre Unterkünfte betreiben („Buchungskalender“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort eine Unterkunft anfragen oder buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist (Auftragsverarbeitungsvertrag Buchungskalender). Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal und für seine Bestellung der Funktion. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden anfragt oder bucht, findet die Informationen dazu (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.
Für den Betrieb des Buchungskalenders speichern wir im Auftrag des Kunden: bei einer Anfrage Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und Nachricht, die Unterkunft, den Zeitraum und die Zahl der Personen sowie bis zur Entscheidung des Kunden einen Hashwert der IP-Adresse (siehe unten); nach der Freigabe die Posten und den Preis des Aufenthalts, die Stornobedingungen und die Fassung der Rechtstexte, die galten; bei der Buchung die Rechnungsadresse, die Zahlart und die Rechnung (und bei einer Stornierung die Stornorechnung) mit lückenloser Rechnungsnummer, in der der Kunde als Aussteller genannt ist; den Stand der Buchung (zum Beispiel gebucht, bezahlt, storniert) mit den Zeitpunkten, bei einer Online-Zahlung die Kennungen der Zahlung bei Stripe und erstattete Beträge, auch automatische Erstattungen (siehe unten). Zahlungsdaten wie die Kartennummer gibt der Gast nur auf der Zahlungsseite von Stripe ein; wir erhalten sie nicht (Online-Zahlung derzeit nur per Karte, auch mit Apple Pay und Google Pay). Der Kunde sieht und bearbeitet Anfragen, Buchungen und Rechnungen im Kundenportal. Im Namen des Kunden stellen wir die Rechnungen aus (Rechnung im Namen und im Auftrag des Kunden, § 14 Abs. 2 Satz 4 UStG) und senden E-Mails an Gäste (Eingang der Anfrage, Freigabe mit Link zur Buchung, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf einer Reservierung, Stornierung) und an den Kunden (neue Anfrage, neue Buchung, Hinweis bei fehlgeschlagenem Kalenderabgleich) über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8). Als Absender erscheint der Kunde, Antworten gehen an ihn. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern; bei einer Online-Zahlung übergeben wir den Link mit dem Kennzeichen als Rücksprungadresse an Stripe, sodass er für Stripe und im Stripe-Dashboard des Kunden sichtbar ist (er gilt bis 90 Tage nach der Abreise, bei nicht zustande gekommenen Buchungen 30 Tage, und öffnet nur diese eine Buchungsseite). Das Widget setzt keine Cookies. Der Kunde kann Kalender anderer Plattformen einbinden und die geheime Kalenderadresse des Buchungskalenders bei ihnen eintragen; wir lesen aus fremden Kalendern nur belegte Zeiträume, nicht Angaben zu Gästen, und die Adresse des Buchungskalenders enthält nur belegte Nächte. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe merken wir uns nur im Arbeitsspeicher des Servers, nicht dauerhaft. Mit der Anfrage speichern wir nicht die IP-Adresse, sondern einen Hashwert davon, der mit einem geheimen Schlüssel unseres Servers berechnet wird (Pseudonym); ohne den Schlüssel lässt sich daraus keine IP-Adresse ableiten. Wir verwenden ihn nur für den Missbrauchsschutz und entfernen ihn, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Im Übrigen gelten die Protokolle in Abschnitt 3.
Online-Zahlungen im Buchungskalender laufen über Stripe Connect unmittelbar auf das eigene Stripe-Konto des Kunden; wir erhalten und verwahren kein Geld der Gäste und beteiligen uns nicht am Umsatz. Beim Start einer Zahlung übergeben wir Stripe die Bezeichnung des Aufenthalts mit Rechnungsnummer, den Betrag, die Kennungen der Buchung, die E-Mail-Adresse des Gastes zur Vorbelegung und den Rücksprung-Link zur Buchungsseite. Wird ein Aufenthalt storniert, schließen wir eine offene Zahlungsseite. Geht eine Zahlung nach einer Stornierung oder dem Ablauf der Reservierung ein oder zahlt der Gast eine bezahlte Buchung ein zweites Mal, erstatten wir den Betrag im Namen des Kunden und nach seiner dauerhaften Weisung automatisch in voller Höhe über sein Stripe-Konto; der Stand der Buchung ändert sich dadurch nicht. Um dieses Konto zu eröffnen, übermitteln wir, sobald der Kunde im Kundenportal die Verbindung mit Stripe auslöst, seine E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“), zusammen mit internen Kennzeichen, mit denen wir das Konto seinem Projekt zuordnen. Die weiteren Angaben für das Konto gibt der Kunde direkt bei Stripe ein. Wir speichern die Kennung des Stripe-Kontos und seinen Stand (ob Zahlungen und Auszahlungen möglich sind), den uns Stripe meldet, und zeigen ihn dem Kunden im Kundenportal an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag mit dem Kunden). Stripe verarbeitet die Daten für sein Konto als eigener Verantwortlicher; dabei können Daten an die Stripe, LLC in den USA übermittelt werden, die nach dem EU-US Data Privacy Framework zertifiziert ist (Abschnitt 7). Es gelten die Datenschutzhinweise von Stripe: stripe.com/de/privacy.
Löschfristen: Den IP-Hash entfernen wir, sobald der Kunde eine Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Anfragen, aus denen keine Buchung wurde, anonymisieren wir nach sechs Monaten. Bei gebuchten Aufenthalten anonymisieren wir Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht drei Jahre nach dem Ende des Jahres der Abreise; die Rechnungen und die Rechnungsadresse bewahren wir für den Kunden acht Jahre auf (Buchungsbelege, § 147 AO), solange der Buchungskalender läuft. Schaltet der Kunde den Buchungskalender ab oder endet der Vertrag über den Betrieb seiner Website, schalten wir den Buchungskalender ab und weisen den Kunden danach per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen herunterzuladen. Der Kunde kann Anfragen, Buchungen und Rechnungen danach 90 Tage lang im Kundenportal lesen; danach löschen wir alle Daten des Buchungskalenders (Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen). Wird das Kundenkonto gelöscht, schalten wir den Buchungskalender ab und löschen seine Daten 90 Tage danach; einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schalten wir den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen. Löschen wir Daten oder anonymisieren wir sie, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3). Die Bestellung der Funktion und ihren Nachweis beschreibt Abschnitt 7.
## Hinweise für die Übernahme
- Online-Zahlung gibt es derzeit nur per Karte (kein SEPA); bei einer Erweiterung die Texte (6f, AVV Nr. 3 und 7, LB Nr. 3, Absatz) mitändern.
- Die Aufbewahrung der Rechnungen für 8 Jahre durch WEBklar ist ein Dienst für den Kunden und endet mit der Löschung 90 Tage nach dem Abschalten (Frage U6 im Kanzlei-Paket).
- Abschnitt 12 „Empfänger im Überblick“ der Datenschutzerklärung: Stripe-Zeile um „Buchungskalender“ ergänzen.
- Abschnitt 7 (Bestellung der Funktion): Der Satz zu Zustimmungsnachweisen gilt für den Buchungskalender entsprechend; prüfen, ob die Aufzählung der Funktionen dort ergänzt werden muss.
@@ -0,0 +1,34 @@
# Fragen an die Kanzlei: Buchungskalender
Stand 2026-10-07. Gegenstand der Prüfung: Leistungsbeschreibung Buchungskalender und AVV Buchungskalender (Fassung 2026-10-07, beide als „Entwurf – Prüfung durch Kanzlei offen“ gekennzeichnet), Abschnitt 6f der Datenschutzerklärung (`unterkunft-datenschutz-6f-entwurf.md`), Muster-Datenschutz-Absatz für die Website des Vermieters (`docs/funktionen/unterkunft-datenschutz-absatz.md`), Muster-Stornobedingungen (`docs/funktionen/unterkunft-storno-muster.md`), die Zustimmungen der Bestellung (Wortlaut in `server/config.js`, Register `unterkunft`) und die Buchungsseite. Der Buchungskalender ist für Ferienwohnungen, Monteurzimmer und kleine Pensionen gedacht; erste Kundin ist eine Vermieterin von Monteurunterkünften (Unternehmer als Gäste, Verbraucher möglich). Die Arbeitsweise der Software steht im Entwurf der Spezifikation `docs/superpowers/specs/2026-10-07-buchungskalender-design.md`; Muster ist das Kanzlei-Paket zum Webshop (`webshop-kanzlei-paket.md`).
## Fragen
**U1. Button-Lösung über Mail-Link (§ 312j Abs. 3 BGB).** Die Anfrage des Gastes ist unverbindlich (Schaltfläche „Anfrage senden“). Der Vertrag entsteht erst, wenn der Gast nach der Freigabe durch den Vermieter über den Link in der E-Mail die Buchungsseite öffnet und „Zahlungspflichtig buchen“ klickt; unmittelbar darüber stehen alle Pflichtangaben. Genügt das für die Button-Lösung, auch wenn der Vermieter den Preis vor der Freigabe ändern kann und der Gast ihn erst auf der Buchungsseite verbindlich sieht? Ist die Beschriftung „Zahlungspflichtig buchen“ auch bei der Zahlart „vor Ort“ (keine Zahlung im Moment der Buchung) zulässig, oder braucht es eine andere Beschriftung („Verbindlich buchen“)? Muss die Anfrage-Schaltfläche anders heißen, damit niemand sie für verbindlich hält?
**U2. Pflichtinformationen (Art. 246a § 1 EGBGB, § 312d BGB) und Rolle des Vermieters.** Welche Angaben muss die Buchungsseite zeigen, welche stehen in der Buchungsbestätigung (dauerhafter Datenträger, § 312f BGB), und welche muss der Vermieter auf seiner Website selbst vorhalten (Impressum, Informationen zum Anbieter, Verbraucherstreitbeilegung nach § 36 VSBG)? Reicht die Buchungsseite (Anbieter, Unterkunft, Zeitraum, Leistung, Gesamtpreis, Zahlart, Stornobedingungen, Hinweis zum Widerrufsrecht), und was fehlt für Beherbergung typischerweise (zum Beispiel Preisangabenverordnung, Pflichtangaben zur Unterkunft, Hausordnung)? Welche Pflichten treffen WEBklar als Betreiber der Seite im Namen des Vermieters? Insbesondere: Die Bestätigungsmail (G19) nennt den Anbieter mit Name und Anschrift aus den Einstellungen, aber ohne Impressum-Link und ohne Steuernummer; muss der Anbieter dort vollständig mit Impressum und Steuernummer genannt werden?
**U3. Kein Widerrufsrecht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).** Wir weisen auf der Buchungsseite darauf hin, dass für die Buchung zu einem bestimmten Zeitraum kein Widerrufsrecht besteht. Gilt die Ausnahme auch für Monteurunterkünfte, die langfristig oder für Unternehmer und Verbraucher gemischt vermietet werden, und für Aufenthalte, deren Zeitraum über Wochen reicht? Insbesondere: Ist ein langer Monteursaufenthalt (Wochen bis Monate, eher Wohnzweck als Beherbergung) noch „Beherbergung zu Freizeitzwecken“, oder entfällt die Ausnahme, und was bedeutet das für die Belehrung? Ist die Formulierung („Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).“) ausreichend, und wie ist die Belehrungspflicht zu verstehen, wenn eine Ausnahme vorliegt?
**U4. Umsatzsteuer der Endreinigung.** Die Software kennt je Vermieter einen Steuersatz für die Übernachtung (Standard 7 %) und einen für die Endreinigung (Standard 7 %) und rechnet die Steuer aus den Bruttobeträgen heraus. Ist es zulässig, die Endreinigung als unselbständige Nebenleistung der Beherbergung mit demselben Satz zu behandeln (§ 12 Abs. 2 Nr. 11 UStG), oder wäre 19 % der sichere Standard? Wie ist es bei Kleinunternehmern (§ 19 UStG, Hinweistext auf der Rechnung) und bei langfristigen Vermietungen, bei denen die Steuerbefreiung nach § 4 Nr. 12 UStG greifen kann? Wie weit darf WEBklar Voreinstellungen vorgeben, ohne dass daraus eine Steuerberatung wird?
**U5. Rechnung im Namen des Vermieters (§ 14 Abs. 2 Satz 4 UStG).** WEBklar stellt die Rechnungen in Namen und Auftrag des Vermieters aus; er stimmt bei der Bestellung und mit einem Haken in den Einstellungen zu. Genügt das als Zustimmung (Textform?) und ist die Abrechnung durch einen Dritten damit sauber geregelt, auch für die Stornorechnung (Rechnungskorrektur mit eigener Nummer) und die lückenlose Nummerierung je Vermieter und Jahr? Gilt das Konstrukt auch für Rechnungen an Verbraucher (dann ist die Rechnung keine Pflichtrechnung, sondern Beleg) und reichen die Pflichtangaben nach § 14 Abs. 4 UStG, wenn der Vermieter unvollständige Stammdaten hinterlegt? Wer haftet für falsche Steuerangaben auf der Rechnung (§ 14c UStG)?
**U6. Aufbewahrung durch WEBklar (8 Jahre).** WEBklar speichert die Rechnungen und die Rechnungsadresse der Gäste 8 Jahre lang, solange der Buchungskalender läuft, und löscht sie 90 Tage nach dem Abschalten; der Vermieter wird vorher aufgefordert, sie herunterzuladen. Reicht dieser Dienst, ohne dass WEBklar selbst aufbewahrungspflichtig oder zum Buchführungssystem wird (§ 147 AO, GoBD), und wie ist die Spannung zwischen der 8-Jahres-Frist und der Löschung nach 90 Tagen zu regeln? Die Gästedaten ohne Rechnungsbezug werden 3 Jahre nach dem Ende des Abreisejahres anonymisiert und Anfragen ohne Buchung nach 6 Monaten: Sind die Fristen angemessen (Art. 5 Abs. 1 lit. e DSGVO)?
**U7. Rolle im Datenschutz, AVV Buchungskalender.** Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter (Muster AVV Webshop). Ist die Rollenverteilung für die Rechnungserstellung und die Aufbewahrung richtig (kein eigener Zweck von WEBklar)? Ist die Behandlung von Stripe (Vertragspartner des Vermieters, kein Unterauftragsverarbeiter) und der importierten Kalender anderer Plattformen (nur Zeiträume, keine Gästedaten) vertretbar? Genügt die Bezeichnung des IP-Hash als pseudonymes Datum mit Löschung bei der Entscheidung? Der geheime Buchungslink (Token, nur als Prüfsumme gespeichert) gilt bis 90 Tage nach der Abreise, bei abgelehnten oder abgelaufenen Anfragen 30 Tage nach der Entscheidung; er ist für Stripe und im Stripe-Dashboard des Vermieters sichtbar (Rücksprungadresse). Ist die Frist angemessen? Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht die Daten 90 Tage später ohne E-Mail-Hinweis (die Adresse ist dann gelöscht): ausreichend?
**U8. Haftung bei Doppelbuchung durch iCal-Verzögerung.** Der Abgleich mit Booking.com, Airbnb und ähnlichen Plattformen geschieht über iCal mit Verzögerung von Minuten bis Stunden; jede Anfrage braucht die Freigabe, und beim Freigeben und Buchen prüft die Software erneut. Eine Doppelbuchung ist trotzdem möglich. Ist der Haftungsausschluss in Nr. 4 und Nr. 11 der Leistungsbeschreibung (keine Echtzeit-Verfügbarkeit, kein Ersatz von Folgeschäden außer im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen) wirksam, auch gegenüber Verbrauchern? Die Leistungsbeschreibung richtet sich nur an Vermieter, die Unternehmer sind; für deren Gäste gilt das nicht. Ist eine hervorgehobene Warnung im Portal sinnvoll oder nötig?
**U9. Zustimmungen der Bestellung.** Genügen die sechs Haken (Unterlagen, Verantwortung, Rechnung im Auftrag, Stripe, Beginn der Leistung und Erlöschen des Widerrufsrechts, Pflichtinformationen) und die Widerrufsbelehrung der Funktionen, wenn der Vermieter in der Praxis Unternehmer ist? Muss der Haken zu Stripe entfallen, wenn der Vermieter keine Online-Zahlung nutzt?
**U10. Kein Meldeschein, keine Kurtaxe, keine Anzahlung.** Die Leistungsbeschreibung nimmt Meldeschein, Kurtaxe und Anzahlungen aus dem Umfang und weist die Pflichten dem Vermieter zu (Nr. 2 Abs. 7, Nr. 5 Abs. 6). Ist das ausreichend deutlich, und wie ist eine Haftung von WEBklar wegen fehlender Hinweise ausgeschlossen?
**U11. Muster-Stornobedingungen.** Der Text `unterkunft-storno-muster.md` ist eine Vorlage für Vermieter (Staffel nach Zeit bis zur Anreise, Erstattungen, Verhältnis zu § 537 BGB und § 308 Nr. 7 BGB bei Verbrauchern, Pauschalen nach § 309 Nr. 5 BGB). Ist er als Vorlage mit Hinweis „keine Rechtsberatung“ vertretbar? Welche Pauschalen sind üblich und zulässig?
**U12. Datenschutz-Absatz für die Vermieter-Website.** Ist der Muster-Absatz `unterkunft-datenschutz-absatz.md` (Verantwortlicher, Rechtsgrundlagen Art. 6 Abs. 1 lit. b und f, Stripe, E-Mails, Speicherdauer) und der Abschnitt 6f der WEBklar-Erklärung in sich schlüssig und vollständig?
## Prüfpunkte vor dem Livegang
- Fassung der Rechtstexte: Ändert die Kanzlei etwas, gibt es eine neue Fassung (Datum, `config.unterkunft.fassung`, Zustimmungs-Pin in `test/featureConsentTexte.test.js`); ausgelieferte Fassungen werden nie überschrieben.
- Danach Upload in WOMS „Rechtliches“ und Schalter `UNTERKUNFT_ENABLED` von `pilot` auf `on` (Livegang-Schritt 10).
@@ -0,0 +1,52 @@
# Datenschutzerklärung von WEBklar: Abschnitt 6e „Öffnungszeiten live auf Websites unserer Kunden“ (Entwurf)
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitt 9 Nr. 3), Stand 05.10.2026. Keine Rechtsberatung. **`public/datenschutz.html` ist nicht geändert**: Der Abschnitt wird erst nach der Freigabe durch den Inhaber (und nach der Kanzlei, soweit sie die Fragen aus `zeiten-kanzlei-fragen.md` bearbeitet) in die Erklärung übernommen. Reihenfolge der Rechtsrunde: Leistungsbeschreibung, dann dieser Abschnitt samt Website-Absatz (`docs/funktionen/website-datenschutz-absatz.md`), dann Mailtexte, dann Schalter `ZEITEN_ENABLED=on`.
Aufbau wie Abschnitt 6c (`termine-datenschutz-6c.md`): Rolle, gespeicherte Angaben, E-Mails, Aufruf der Kundenwebsite, Löschfristen. Die Leistungsbeschreibung verweist auf diesen Abschnitt (`/datenschutz.html#zeiten`, Nr. 12 Abs. 2): **Sie darf erst live gehen, wenn der Abschnitt mit dem Anker `id="zeiten"` in der Erklärung steht.**
Einbau in `public/datenschutz.html`: zwischen 6c und „7. Abos und Zahlung über Stripe“. Beim Übernehmen ändert sich der Stand der Erklärung (Abschnitt 15); die Tests `test/legalTexts.test.js` (Abschnitt 6c und Kundenansicht) und `test/zeitenRecht.test.js` (dieser Entwurf) werden dann angepasst.
**Nachtrag 06.10.2026 (nach dem Zusammenführen mit main):** Die Nummer 6d ist seit dem Webshop-Livegang vergeben („6d. Webshop auf Websites unserer Kunden“, Anker `id="webshop"`). Dieser Abschnitt heißt deshalb **6e** und steht hinter dem Webshop-Abschnitt und vor „7. Abos und Zahlung über Stripe“; der Anker `id="zeiten"` bleibt.
## Abschnitt 6e (HTML)
```html
<h2 id="zeiten">6e. Öffnungszeiten live auf Websites unserer Kunden</h2>
<p>Kunden können auf ihrer eigenen Website ihre Öffnungszeiten live anzeigen lassen („Öffnungszeiten live“): als Tabelle und als Status („Jetzt geöffnet“), als Hinweis-Leiste am Seitenanfang, auf dem Handy als Kontakt-Leiste mit Knöpfen zum Anrufen, für die Route und für WhatsApp sowie als Angaben für Suchmaschinen im Seitencode. Für die Besucher dieser Websites verarbeiten wir Daten ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist. Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Angaben, die der Kunde im Kundenportal für die Funktion einträgt, für die E-Mails an den Kunden und für seine Bestellung. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer eine Kundenwebsite besucht, findet die Informationen zum Aufruf (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website.</p>
<p>Im Kundenportal speichern wir, was der Kunde für die Funktion einträgt: seine Öffnungszeiten (benannte Zeitpläne, Zeiten je Wochentag, Abweichungen an Feiertagen, Betriebsurlaub und Sondertage sowie das gewählte Bundesland), Hinweise für die Hinweis-Leiste (Text, Art, Zeitraum und gegebenenfalls ein Knopf mit Link oder Telefonnummer), Kontaktangaben für die Kontakt-Leiste (Telefonnummer, Anschrift für die Route und auf Wunsch WhatsApp-Nummer und Starttext der Nachricht, dazu der Zeitpunkt, zu dem der Kunde bestätigt hat, den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen zu haben), die Anzeige-Einstellungen, die unser Team für die Website des Kunden setzt, sowie Zeitpunkt und Urheber (Kunde oder Mitarbeiter) der letzten Änderung. Das sind überwiegend Betriebsdaten; personenbezogene Daten enthalten sie nur, soweit der Kunde sie einträgt, zum Beispiel die Telefonnummer eines Inhabers. Was auf der Website erscheinen soll (Zeiten, Hinweise, Telefonnummer, Anschrift, WhatsApp-Nummer, Name des Betriebs), ist dort öffentlich sichtbar. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, wie bei den übrigen Portal-Daten (Abschnitt 6). Unsere Mitarbeiter können die Zeiten im Auftrag des Kunden ansehen und ändern (Abschnitt 6); den Haken für den WhatsApp-Hinweis setzen sie nicht. Die Bestellung und ihren Nachweis beschreibt Abschnitt 7.</p>
<p>Hat der Kunde die Funktion eingeschaltet, erinnern wir ihn per E-Mail an die E-Mail-Adresse seines Kundenkontos vor Feiertagen: Die E-Mail nennt die Tage der nächsten sieben Tage, an denen ein Feiertag seines Bundeslands liegt oder an denen wir um Prüfung bitten, und fragt, ob seine Zeiten für diese Tage stimmen. Für jeden Tag senden wir höchstens eine Erinnerung; dafür speichern wir den Tag als Merker (ohne weitere Angaben) und löschen den Merker nach 30 Tagen. Außerdem senden wir dem Kunden E-Mails zum Stand der Funktion (eingeschaltet, Ende des Wartungs-Abos mit der Möglichkeit, die Funktion zu behalten, abgeschaltet). Der Versand läuft über den E-Mail-Dienst der Hetzner Online GmbH (Abschnitt 8). Die E-Mails enthalten keine Werbung. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, weil die Erinnerung zur Funktion gehört.</p>
<p>Beim Aufruf einer Kundenwebsite mit Öffnungszeiten live lädt der Browser ein kleines Programm von unserem Server (project.webklar.com) und die Öffnungszeiten des Kunden. Das Programm ruft die Daten einmal je Seitenaufruf ab und danach höchstens alle zehn Minuten erneut. Dabei gelten die Protokolle in Abschnitt 3. Das Programm setzt keine Cookies, speichert nichts im Browser des Besuchers und lädt nichts von anderen Anbietern. Die Knöpfe „Route“ und „WhatsApp“ sind Links: Erst der Klick des Besuchers öffnet Google Maps oder WhatsApp; vorher übertragen wir nichts an diese Dienste, und danach gelten deren Datenschutzhinweise. Zum Schutz vor Missbrauch begrenzen wir die Abrufe je IP-Adresse (höchstens 120 je Minute); dafür merken wir uns die Zeitpunkte der Abrufe je IP-Adresse nur im Arbeitsspeicher des Servers, nicht dauerhaft. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist der sichere und störungsfreie Betrieb.</p>
<p>Löschfristen: Haben Sie die Funktion im Wartungs-Abo ohne Aufpreis eingeschaltet und endet das Wartungs-Abo, läuft sie noch 30 Tage weiter; in dieser Zeit können Sie sie für einmalig 30 € behalten. Andernfalls schalten wir sie danach ab, bewahren Ihre Eingaben noch weitere 30 Tage auf und löschen sie dann; beginnt in dieser Zeit wieder ein Wartungs-Abo, schalten wir die Funktion mit den vorhandenen Eingaben wieder ein. Haben Sie die Funktion für 30 € bestellt, bleiben Ihre Eingaben, solange die Funktion läuft. Schalten wir sie auf Ihren Wunsch ab, löschen wir Ihre Eingaben auf Ihr Verlangen, spätestens mit der Löschung Ihres Kundenkontos. In unseren Datensicherungen bleiben gelöschte Daten noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3).</p>
```
## Ergänzung Abschnitt 6 (Mitarbeiter im Kundenauftrag)
Heute steht in Abschnitt 6 (wörtlich, `public/datenschutz.html`; der Satz ist durch `legal_datenschutz_kundenansicht` in `test/legalTexts.test.js` gepinnt):
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen und, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern.
Vorgeschlagen:
> Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern sowie, wenn Sie Öffnungszeiten live nutzen, Ihre Öffnungszeiten, Hinweise und Kontaktangaben ansehen und ändern.
Der folgende Satz („Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen.“) und das Protokoll (Mitarbeiter und Zeitpunkt, 12 Monate) bleiben unverändert und decken auch das Speichern der Zeiten durch Mitarbeiter ab (Protokollaktion `zeiten_gespeichert`). Beim Übernehmen die beiden Pins in `test/legalTexts.test.js` (`legal_datenschutz_kundenansicht`) auf den neuen Satz umstellen.
## Folgeänderung Abschnitt 7 (Nachweis beim Einschalten)
Abschnitt 7 nennt heute die Funktionen und den Klick auf „Zahlungspflichtig bestellen“. Beim Einschalten im Wartungs-Abo gibt es keine Zahlung und keine Stripe-Sitzung, gespeichert wird der Nachweis der Zustimmung trotzdem. Heute (wörtlich):
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.
Vorgeschlagen (Ergänzung der Klammer und des Klicks; der Rest bleibt):
> Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner, Öffnungszeiten live) oder schalten Sie Öffnungszeiten live im Wartungs-Abo ein, speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“ oder „Einschalten“, bei einer Bestellung die Stripe-Sitzung, jeweils die IP-Adresse und die Browserkennung sowie Ihr Konto und die angemeldete Person.
Diese Folgeänderung steht nicht in Spec 9 Nr. 3; sie ergibt sich daraus, dass der Gratisweg einen Nachweis speichert. Gegen den Code geprüft (`speichereNachweisAusAnfrage` in `server/services/featureConsents.js`, Zeilen 196 und 197): Der Gratisweg speichert IP-Adresse und Browserkennung genauso wie die Bestellung; nur die Stripe-Sitzung entfällt (ohne `sessionId` trägt der Nachweis keine). Der Satz nennt deshalb die Stripe-Sitzung nur bei der Bestellung, IP-Adresse und Browserkennung bei jeder Zustimmung.
## Offene Punkte für die Rechtsrunde
1. **Rolle bei Websites ohne Abo oder bei Fremdhosting.** Der Entwurf sagt wie 6c: Wir sind Auftragsverarbeiter der Kunden für die Besucherdaten (Server-Protokolle). Mit Abo deckt das Anlage 1 der Abo-Bedingungen, ohne Abo oder bei Fremdhosting besteht kein Auftragsverarbeitungsvertrag (gleiche Lücke wie bei der News-Pinnwand). Frage `ZQ1` in `zeiten-kanzlei-fragen.md`.
2. **Löschung bei Abschaltung durch das Team** (Spec Abschnitt 5: keine automatische Löschung bei Grund `team`). Der Entwurf sagt „auf Verlangen, spätestens mit der Löschung des Kundenkontos“. Soll stattdessen eine feste Frist gelten (zum Beispiel 30 Tage wie beim Ende des Abos)? Entscheidung des Inhabers; dann auch Leistungsbeschreibung Nr. 9 Abs. 5 und das Programm (`abschalten.js`) anpassen.
3. **Erinnerungs-E-Mail.** Rechtsgrundlage Art. 6 Abs. 1 lit. b DSGVO (Teil der Funktion). Es ist keine Werbung; ob sie wegen des Hinweises „bei Google auch eintragen“ als Werbung gilt, ist nicht anzunehmen, steht aber in der Kanzlei-Frage `ZQ6`.
4. **Nachweis beim Einschalten** (Folgeänderung Abschnitt 7, siehe oben): beantwortet. Beim Einschalten im Wartungs-Abo werden IP-Adresse und Browserkennung ebenfalls gespeichert, nur die Stripe-Sitzung entfällt (der Satz ist entsprechend gefasst). Offen bleibt allein die rechtliche Prüfung des Satzes.
5. **Kontaktdaten öffentlich.** Die Telefonnummer und Anschrift stehen im Seitencode der Kundenwebsite (Abschnitt 6e, zweiter Absatz). Das ist die Entscheidung des Kunden; die Leistungsbeschreibung (Nr. 6 Abs. 3) verlangt, nur veröffentlichungsfähige Angaben einzutragen.
@@ -0,0 +1,43 @@
# K02 und K03: Block `funktionen` mit Öffnungszeiten live (Entwurf)
Entwurf für die Rechtsrunde (Spec „Öffnungszeiten live“, Abschnitte 7 und 9; Task 33), Stand 05.10.2026. **Keine Codeänderung:** Der Block `funktionen` in `server/mail-templates/K02 Kuendigungsbestaetigung.html` und `K03 Eingang Kuendigung.html` ist Rechtstext mit Pins; dieser Entwurf schlägt den neuen Satz nur vor. Der Inhaber entscheidet, die Kanzlei sieht ihn mit der Mailrunde (Spec 9: Oberflächen- und Mailtexte). Erst danach werden die Vorlagen und die Pins geändert.
Beide Vorlagen sind Kündigungsbestätigungen an den Kunden und stehen in der Sie-Form. Der Block erscheint nur beim Wartungs-Abo (`server/routes/abo.js`: K02 mit `funktionen: isWartung(sub)`, K03 mit `funktionen: !storage && isWartung(sub)`), nicht bei Hosting-Abo und nicht bei der Kündigung von Zusatzspeicher.
## Heute (gepinnt)
Der Block besteht aus diesem Satzpaar, in beiden Vorlagen wörtlich gleich:
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.
Pins, die der Satz heute hat:
- `test/mailTemplates.test.js`, Test „K02, K03: Block funktionen nennt Terminplaner und Löschfrist, nur mit Block (in K03 nur innerhalb von abo)“: prüft das ganze Satzpaar mit einem regulären Ausdruck (beginnend mit „Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm.“ bis „… ansehen und exportieren.“), dass der Block ohne `funktionen` fehlt und dass „Terminplaner“ bei Hosting-Abo und bei Speicher-Kündigung nirgends steht.
- `test/publicForms.test.js`, Konstante `FUNKTIONEN_SATZ` (nur der erste Satz, „… und enden mit ihm.“).
- Die Vorlagen-Kommentare (Zeile 12 beider Vorlagen) beschreiben den Block als „Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden“.
## Vorschlag
Der heutige Satz bleibt unverändert der Anfang (damit beide Pins ohne Änderung grün bleiben); angehängt wird ein Satz für Öffnungszeiten live, der die 30 Tage zum Behalten und die 30 Tage Datenfrist nach Z11 und Leistungsbeschreibung Nr. 9 nennt:
> Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren. Haben Sie Öffnungszeiten live im Wartungs-Abo eingeschaltet, läuft die Funktion nach dem Ende des Abos noch 30 Tage weiter; in dieser Zeit können Sie sie im Kundenportal für einmalig 30 € behalten. Behalten Sie sie nicht, schalten wir sie danach ab, auf Ihrer Website stehen dann wieder feste Öffnungszeiten, und Ihre Eingaben löschen wir 30 Tage später.
Warum so:
- Der Satz ist bedingt („Haben Sie … eingeschaltet“), weil der Block bei jeder Kündigung eines Wartungs-Abos erscheint, auch bei Kunden ohne die Funktion.
- Wer die Funktion für 30 € bestellt hat, behält sie ohne Abo (Leistungsbeschreibung Nr. 9 Abs. 2 letzter Satz); für ihn gilt der Satz nicht, was die Bedingung „im Wartungs-Abo eingeschaltet“ ausdrückt. Ein eigener Satz dazu wäre in einer Kündigungsbestätigung verwirrend.
- Die Einzelheiten (Frist mit Datum, Knopf „Behalten“) sagt K29 („Wartungs-Abo beendet“), sobald das Ende des Abos erkannt ist; K02 und K03 sagen nur, dass es das gibt.
- „Abschalten“ bedeutet: Hinweis-Leiste, Kontakt-Leiste, Status und Google-Angaben verschwinden, die feste Ersatz-Tabelle der Website bleibt stehen (Leistungsbeschreibung Nr. 9 Abs. 4).
Im HTML ändert sich nur der Inhalt der Zelle zwischen `<!-- WENN funktionen -->` und `<!-- ENDE funktionen -->`; Aufbau und Blockkommentare bleiben. Die Kopfkommentare beider Vorlagen sagen dann „Website-Analyse, Terminplaner und Öffnungszeiten live“.
## Alternative
Ein eigener Block `zeiten` (nur für Kunden, bei denen die Funktion im Abo läuft): kein Text für Kunden ohne Funktion und die Zeile steht ohne „Haben Sie …“. Dafür braucht das Programm in `server/routes/abo.js` an beiden Stellen eine zusätzliche Abfrage der Funktion, beide Vorlagen einen neuen Block, und die Pins im Mail-Pin-Test (K01 bis K30) ändern sich. Nur sinnvoll, wenn der Inhaber den bedingten Satz nicht möchte.
## Beim Übernehmen
1. Vorlagen K02 und K03 ändern (Zelle des Blocks, Kopfkommentar).
2. `test/mailTemplates.test.js` (K02/K03-Test): zusätzlicher Wortlaut-Pin für den neuen Satz; die bestehenden Prüfungen bleiben.
3. Den Mail-Pin-Test (K01 bis K30, ebenfalls `test/mailTemplates.test.js`) und, falls sich der Block ändert, die Prüfung in `test/publicForms.test.js` nachziehen.
4. Kanzlei-Frage `ZQ5` (Ende mit dem Abo, 30 Tage zum Behalten) in `zeiten-kanzlei-fragen.md` abwarten, weil sie den Inhalt des Satzes betrifft.
+134
View File
@@ -0,0 +1,134 @@
# Fragen an die Kanzlei: Öffnungszeiten live (⚖)
Stand 05.10.2026, Entwurf (Spec „Öffnungszeiten live“, Abschnitt 9, und Task 33). Dies sind die Fragen zu den Texten dieser Funktion; sie ergänzen das Kanzlei-Paket des Terminplaners (`docs/rechtliches/termine-kanzlei-paket.md`, Fragen Q1 bis Q26).
**Warum eine eigene Datei.** Das Kanzlei-Paket des Terminplaners liegt auf dem Zweig `feature/terminplaner-tp2` und ist in diesen Zweig (`feature/oeffnungszeiten`) noch nicht eingeflossen; ändern wir es hier, entsteht beim Zusammenführen ein Konflikt. Die Fragen tragen deshalb die Kennung `ZQ1` bis `ZQ12`. Beim Zusammenführen: als `Q27` bis `Q38` in Abschnitt 4 des Kanzlei-Pakets übernehmen, die Fundstellen in Abschnitt 5 ergänzen, die Fassung der Leistungsbeschreibung in Abschnitt 3.4 daneben stellen. In veröffentlichten Dateien stehen keine ⚖-Marker.
Jede Frage hat vier Teile wie im Paket des Terminplaners: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Einordnungen wie „Annahme“ sind keine Rechtsurteile. Die Texte, um die es geht: `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html` (Leistungsbeschreibung, im Folgenden „LB“), `docs/rechtliches/zeiten-datenschutz-6e-entwurf.md` (Abschnitt 6e), `docs/rechtliches/zeiten-k02-k03-entwurf.md` (K02/K03), `docs/funktionen/website-datenschutz-absatz.md` (Website-Absatz) und die Haken im Register (`server/config.js`, `features.zeiten`).
Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block F) → Leistungsbeschreibung → Datenschutz § 6e/§ 6 und Website-Absatz → Oberflächen- und Mailtexte → diese Fragen ans Kanzlei-Paket → Inhaber lädt die Fassungen in WOMS „Rechtliches“ hoch → Schalter `ZEITEN_ENABLED=on`.
### ZQ1: ⚖ AVV Funktionen per Haken?
**Frage.** Für Öffnungszeiten live verarbeitet WEBklar beim Aufruf einer Kundenwebsite Besucherdaten (IP-Adresse, Zeitpunkt, Browserangaben in Server-Protokollen, 14 Tage). Mit einem Wartungs-Abo deckt Anlage 1 der Abo-Bedingungen (Auftragsverarbeitung, Server-Protokolle der Website) das ab. Beim 30-€-Weg ohne Abo oder bei Websites, die woanders liegen, gibt es keinen Auftragsverarbeitungsvertrag; das ist dieselbe Lücke wie bei der News-Pinnwand. Soll sie mit einem zusätzlichen Haken „AVV Funktionen“ beim Bestellen und Einschalten geschlossen werden (wie der Haken für den AVV Terminplaner und den AVV Website-Analyse), und genügt ein gemeinsamer AVV „Funktionen“ für News-Pinnwand und Öffnungszeiten live? Oder ist WEBklar für die Protokolle des eigenen Servers ohnehin selbst Verantwortlicher, sodass kein AVV nötig ist?
**Fundstelle im Text.** Spec Abschnitt 9 Nr. 5; Register `features.zeiten.avv: null`; Abschnitt 6e Absatz 1 (Rolle als Auftragsverarbeiter) und Offener Punkt 1; Website-Absatz Variante B (`[Prüfen: AVV mit WEBklar erforderlich]`); LB Nr. 12 Abs. 2.
**Unsere Annahme.** Mit Abo genügt Anlage 1. Ohne Abo und bei Fremdhosting braucht es einen AVV; wir würden einen kurzen AVV „Funktionen“ per Haken einführen (Aufbau und Nummern wie AVV Terminplaner Nr. 4 bis 9, ohne Termindaten, Unterauftragsverarbeiter nur Hetzner).
**Alternative.** Nichts ändern, weil die IP-Adresse in Server-Protokollen beim Abruf eines Skripts von `project.webklar.com` in der Verantwortung des Betreibers dieses Servers liegt; oder Öffnungszeiten live nur mit Wartungs-Abo oder bei WEBklar-Hosting verkaufen; oder der Rahmen-AVV mit Anlage je Funktion (TP2 Spec 7.4 „Später“).
### ZQ2: Behalten-Zustimmung nutzt den Haken „Einbau beginnt“
**Frage.** Endet das Wartungs-Abo, kann der Kunde Öffnungszeiten live im Kundenportal für einmalig 30 € behalten (LB Nr. 9 Abs. 2). Der Weg „Behalten“ verlangt dieselbe Zustimmung wie die Neubestellung, also auch den zweiten Haken (`beginn`): „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger 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).“ Beim Behalten gibt es keinen Einbau; die Funktion läuft bereits, es entsteht kein neues Ticket. Passt der Haken dann? Ist das Behalten ein eigener Vertrag mit eigenem Widerrufsrecht für Verbraucher (Frist ab dem Klick), und was ist die „Leistung“, deren vollständige Erbringung das Widerrufsrecht erlöschen lässt (der Weiterbetrieb ohne Abo hat kein Ende)?
**Fundstelle im Text.** Register `features.zeiten.consent.items` (Haken `leistung` und `beginn`); Programm: `POST /api/funktionen/zeiten/behalten` (`pruefeZustimmung(feature, body)`: „die der Bestellung, beide Haken“), `server/services/zeiten/behalten.js`; Spec Abschnitt 3.3; LB Nr. 9 Abs. 2 und Nr. 11.
**Unsere Annahme.** Der Haken ist für das Behalten unschädlich, weil er nur das Verlangen des Beginns und die Kenntnis vom Erlöschen bestätigt. Der Wortlaut „mit dem Einbau“ ist aber ungenau; wir würden für das Behalten „mit der Leistung“ oder „mit dem Weiterbetrieb“ sagen. Der Widerruf eines Behalten-Vertrags führt zur Abschaltung (LB Nr. 11 Abs. 3).
**Alternative.** Ein eigener Haken für das Behalten („Ich verlange, dass die Funktion nach dem Ende meines Wartungs-Abos ohne Unterbrechung weiterläuft …“); oder kein Beginn-Haken, wenn für das Behalten kein Widerrufsrecht besteht (Kunde ist Unternehmer, Leistung sofort).
### ZQ3: Beginn-Haken der bezahlten Bestellung: Wertersatz-Satz und Fassung
**Frage.** Der Haken `beginn` der Bestellung für 30 € trug zunächst nur das Zitat zum Erlöschen des Widerrufsrechts. Nach der Abschlussprüfung ist der Satz zum Wertersatz ergänzt, wörtlich wie in den Haken der News-Pinnwand und des Terminplaners (TP2 Q3 und Q5). Er lautet jetzt: „Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger 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).“ Die Fassung der Zustimmung ist dafür auf `2026-10-05` gesetzt (die übrigen Funktionen-Unterlagen tragen ebenfalls `2026-10-05`; die Gratis-Zustimmung `consentFrei` und die LB bleiben bei `2026-10-04`, weil ihr Text unverändert ist). Bitte bestätigen: (a) Genügt der Satz in dieser Form, auch mit „der Leistung“ statt „des Einbaus“ im ersten Satzteil (bei der News-Pinnwand steht „des Einbaus“)? (b) Dürfen die Fassungen je Funktion unterschiedlich nummeriert sein, oder soll der Nachweis eine gemeinsame Fassung der Funktionen-Unterlagen tragen? Die LB nennt den Wertersatz weiterhin in Nr. 11 Abs. 2.
**Fundstelle im Text.** Register `features.zeiten.consent` (Fassung `2026-10-05`, Haken `leistung` und `beginn`) und `consentFrei` (Fassung `2026-10-04`); LB Nr. 11 Abs. 2; Widerrufsbelehrung der Funktionen (Hinweise zum Widerruf, Wertersatz als Anteil am Gesamtpreis); Test `test/zeitenRegister.test.js` pinnt den Wortlaut und prüft, dass der Satz mit dem der News-Pinnwand und des Terminplaners übereinstimmt.
**Unsere Annahme.** Der Wertersatz-Satz gehört wie beim Terminplaner in den Haken (Frage Q3 des Terminplaners gilt entsprechend) und ist jetzt enthalten; der Wortlaut bleibt Entwurf bis zur Freigabe, danach wird die Fassung auf den Tag der Freigabe gesetzt (Pins in `test/zeitenRegister.test.js` mitziehen).
**Alternative.** Den Satz wieder aus dem Haken nehmen und nur in der Belehrung und der LB lassen; eine gemeinsame Fassung für alle Funktionen.
### ZQ4: Haftung, ausgeschrieben für Kunden ohne Abo (LB Nr. 8)
**Frage.** Die LB schreibt die Haftung aus, statt auf § 13 der Abo-Bedingungen zu verweisen, weil Kunden ohne Wartungs-Abo die Abo-Bedingungen nicht haben. Nr. 8 Abs. 4 und 5 geben § 13 Abs. 1 und 2 wörtlich wieder, Abs. 6 den Satz zugunsten der Mitarbeiter und Beauftragten (§ 13 Abs. 5) und den Hinweis, dass der Abschnitt auch für Kunden ohne Abo gilt. Abs. 1 bis 3 sagen: Die Live-Anzeige ist ein Hilfsmittel, die Richtigkeit der Angaben liegt beim Kunden, Fehler in Feiertagsdaten und Softwarefehler beseitigt WEBklar nach Meldung, für entgangenen Gewinn und Folgeschäden (etwa weil ein Besucher wegen einer Fehlanzeige vor verschlossener Tür steht) gelten Abs. 4 und 5. Fragen: (a) Ist die Klausel gegenüber Verbrauchern mit § 309 Nr. 7 BGB vereinbar und hinreichend transparent? (b) Braucht es gegenüber Unternehmern einen Höchstbetrag? (c) Fehlen § 13 Abs. 3 (Datenverlust, Wiederherstellungsaufwand) und Abs. 4 (kein Schadensersatz nach § 536a Abs. 1 BGB für anfängliche Mängel) für Kunden ohne Abo? (d) Ist die Zusage „Fehler beseitigt WEBklar in angemessener Frist“ (Nr. 7 Abs. 1, Nr. 8 Abs. 2) für Kunden ohne Abo zu weit?
**Fundstelle im Text.** LB Nr. 7 und Nr. 8; Abo-Bedingungen § 13 (`public/abo-bedingungen.html`); der Test `test/zeitenRecht.test.js` hält den Wortlaut beider Klauseln gleich; TP2 Q8 (Haftung für verpasste Termine).
**Unsere Annahme.** Die Spiegelung von § 13 Abs. 1 und 2 genügt; ein Höchstbetrag ist nicht nötig, weil Abs. 2 den Schaden auf den vertragstypischen, vorhersehbaren begrenzt. Die Abs. 3 und 4 sind für diese Funktion nicht nötig (die Funktion speichert keine Daten Dritter; ein Schadensersatz für anfängliche Mängel ist bei einem Einmalpreis von 30 € kein praktisches Thema).
**Alternative.** Abs. 3 und 4 des § 13 ebenfalls ausschreiben; einen Höchstbetrag (zum Beispiel den Preis der Funktion oder den Jahresbetrag des Wartungs-Abos) gegenüber Unternehmern festlegen.
### ZQ5: Ende mit dem Abo: 30 Tage zum Behalten, Abschalten, Rückkehr (LB Nr. 9)
**Frage.** Wer Öffnungszeiten live im Wartungs-Abo ohne Aufpreis eingeschaltet hat, behält die Funktion nach dem Ende des Abos noch 30 Tage; in dieser Zeit kann er sie für einmalig 30 € behalten, sonst schaltet WEBklar sie ab (auf der Website bleibt die feste Ersatz-Tabelle), die Eingaben bleiben noch weitere 30 Tage und werden dann gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, läuft die Funktion mit den Eingaben weiter. Wer sie für 30 € bestellt hat, behält sie auch ohne Abo. Fragen: (a) Ist die Kopplung der Gratis-Funktion an das Abo mit dieser Frist zulässig, obwohl sie ein „freiwilliges Extra“ ist und die Abo-Bedingungen unverändert bleiben (Z19)? (b) Darf die Rückkehr bei neuem Abo ohne neue Zustimmung automatisch erfolgen? (c) Frist und Anker der Löschung (30 Tage nach dem Abschalten) und der Sicherungsüberhang von sieben Tagen. (d) Bei Abschaltung durch WEBklar auf Wunsch des Kunden oder aus anderem Grund als dem Ende des Abos gibt es im Programm keine automatische Löschung; die LB sagt „auf Verlangen, spätestens mit dem Löschen des Kundenkontos“ (Nr. 9 Abs. 5). Genügt das, oder braucht es eine feste Frist? (e) Gehört das einmalige Aktualisieren der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4) zu einer Leistung, die WEBklar auch nach dem Ende des Abos schuldet?
**Fundstelle im Text.** LB Nr. 9 Abs. 1 bis 5, Nr. 4 Abs. 3; Abschnitt 6e (Löschfristen) und Offener Punkt 2; K02/K03 (`zeiten-k02-k03-entwurf.md`); Programm: `server/services/zeiten/aboAbgleich.js` (30 Tage Frist zum Behalten, 30 Tage Datenfrist), `abschalten.js`; Spec Z11 und Abschnitt 5; Parallele: TP2 Q7 und Q9 (Terminplaner endet mit dem Abo).
**Unsere Annahme.** Ja zu (a) bis (c): Der Kunde hat die Funktion nur durch das Abo ohne Aufpreis; die Frist zum Behalten und die Datenfrist sind angemessen, die Rückkehr stellt den früheren Stand wieder her. Zu (d) genügt „auf Verlangen“, weil die Eingaben Betriebsdaten sind und der Kunde die Löschung jederzeit verlangen kann. Zu (e) ja, weil es zum Einbau gehört, den der Preis enthält (Z20).
**Alternative.** Löschung auch bei Abschaltung durch das Team nach 30 Tagen (das Programm müsste dafür angepasst werden); keine Frist zum Behalten (Abschaltung mit dem Ende des Abos); Rückkehr nur auf Antrag.
### ZQ6: Mails zur Funktion: Erinnerung vor Feiertagen und Zustandsmails
**Frage.** WEBklar sendet dem Kunden E-Mails: K27 (eingeschaltet), K28 (Erinnerung 1 bis 7 Tage vor einem Feiertag oder einem Prüftag: „Stimmen Ihre Zeiten an diesen Tagen?“, mit einem Knopf zum Editor und dem Hinweis, die Zeiten auch bei Google einzutragen), K29 (Wartungs-Abo beendet, Frist, „für einmalig 30 € behalten“), K30 (abgeschaltet). Sind das Vertragsmails (Art. 6 Abs. 1 lit. b DSGVO) ohne Einwilligung, oder wirkt K28 wegen des Hinweises „bei Google auch eintragen“ oder des Verweises auf die Funktion als Werbung (§ 7 UWG, Art. 6 Abs. 1 lit. f DSGVO mit Widerspruchsmöglichkeit)? Muss K28 einen Hinweis zum Abbestellen tragen? Die Erinnerung lässt sich im Programm nicht abschalten; der Kunde kann nur die Funktion abschalten lassen.
**Fundstelle im Text.** Abschnitt 6e Absatz 3; LB Nr. 2 Abs. 2; Mailvorlagen K27 bis K30 (`server/mail-templates/`), Spec Abschnitt 7 (K28: eine Mail für alle neuen Tage, Merker, 08:00 bis 20:00 Uhr Berliner Zeit, kein BCC ans Team).
**Unsere Annahme.** Die Erinnerung ist Teil der vertraglichen Leistung (LB Nr. 2 Abs. 2) und deshalb eine Vertragsmail ohne Einwilligung; der Hinweis auf Google dient der Durchführung (die Zeiten dort aktuell zu halten), nicht der Werbung.
**Alternative.** Hinweis auf Google aus K28 streichen; einen Abbestell-Link („Erinnerungen ausschalten“) mit Einstellung im Editor vorsehen.
### ZQ7: Einstellung ohne Grund bei Unternehmern im Gratisweg (LB Nr. 10 Abs. 4)
**Frage.** Die LB folgt in Nr. 10 der Einstellungsregel der Website-Analyse (Nr. 8 dort): Beenden oder Einstellen mit vier Wochen oder drei Monaten nur bei Rechtslage oder nicht behebbarer Sicherheitslücke (Abs. 2), zusätzlich nach Abs. 4 **ohne** besonderen Grund mit drei Monaten gegenüber Unternehmern, die die Funktion im Wartungs-Abo ohne Aufpreis haben; der Kunde kann das Abo dann innerhalb von 30 Tagen zum Einstellungszeitpunkt kündigen, auch vor Ablauf der Mindestlaufzeit. Wer die Funktion für 30 € bestellt hat, kann sie nur aus den Gründen von Abs. 2 verlieren (Abs. 5). Fragen: (a) Ist die Einstellung ohne Grund für ein kostenloses Zusatzangebot zulässig, auch wenn die Funktion im Abo „enthalten“ beworben wird (Z4)? (b) Ändert das Sonderkündigungsrecht in Abs. 4 die Abo-Bedingungen so, dass das Verfahren nach § 15 nötig wäre (Z19: freiwilliges Extra, keine Änderung der Abo-Bedingungen)? (c) Braucht es bei Einstellung einer gekauften Funktion durch WEBklar eine (anteilige) Erstattung des Preises (vgl. TP2 Q8)?
**Fundstelle im Text.** LB Nr. 10 Abs. 1 bis 5; Abo-Bedingungen §§ 3 Abs. 7, 8, 15; LB der Website-Analyse Nr. 8 (`docs/rechtliches/leistungsbeschreibung-website-analyse.md`); Spec Abschnitt 9 Nr. 1 („Einstellung/Änderung nach Vorbild Analyse Nr. 8“).
**Unsere Annahme.** Ja zu (a) und (b) wie bei der Analyse: Das Sonderkündigungsrecht stellt den Kunden besser; die Abo-Bedingungen bleiben unverändert. Zu (c): Bei Einstellung aus den Gründen von Abs. 2 erstattet WEBklar nichts, weil die Funktion bis dahin lief; die Rechte aus § 327r BGB bleiben.
**Alternative.** Abs. 4 streichen (Einstellung nur aus Gründen); bei gekauften Funktionen anteilig erstatten.
### ZQ8: Website-Absatz: WhatsApp und Google Maps erst nach Klick, Haken als Nachweis
**Frage.** (a) Der Absatz „Öffnungszeiten auf dieser Website“ (Variante A und B) sagt, dass die Knöpfe „Route“ und „WhatsApp“ einfache Links sind, dass Google Maps und WhatsApp erst nach dem Klick geöffnet werden und dass vorher nichts an Google oder Meta übertragen wird; das Skript setzt keine Cookies und speichert nichts im Browser. Genügt das, oder braucht es für diese Links eine Einwilligung (§ 25 TDDDG, Art. 6 Abs. 1 lit. a DSGVO)? (b) Der Kunde bestätigt im Portal mit einem Haken, dass er den „WhatsApp-Hinweis“ (Baustein WhatsApp) in seine Datenschutzerklärung aufgenommen hat; der Zeitpunkt wird gespeichert, Mitarbeiter setzen den Haken in der Kundenansicht nicht. Reicht der Haken als Nachweis? (c) Wenn WEBklar den Absatz beim Einbau in die Datenschutzerklärung einfügt (LB Nr. 6 Abs. 4), ist das Einfügen eines Mustertextes eine Rechtsdienstleistung (§ 2 RDG)? Das ist dieselbe Regel wie TP2 Q18 und soll für alle Funktionen gleich bleiben. (d) Die Angaben im Seitencode (strukturierte Daten zu den Öffnungszeiten) enthalten keine Daten von Besuchern; ist ein Satz dazu nötig?
**Fundstelle im Text.** `docs/funktionen/website-datenschutz-absatz.md` (Variante A und B, Bausteine Route und WhatsApp); LB Nr. 2 Abs. 4, Nr. 6 Abs. 3 und 4; Abschnitt 6e Absatz 4; Programm: `public/embed/zeiten.js` (keine Cookies, kein Storage, keine Anfragen an Dritte; Links erst beim Klick), Spec Z23 (Route öffnet Google Maps erst beim Klick).
**Unsere Annahme.** (a) Ein Link, den der Besucher anklickt, ist keine Übertragung durch WEBklar oder den Kunden vor dem Klick; eine Einwilligung ist nicht nötig. (b) Der Haken genügt als Nachweis. (c) Wie TP2 Q18: Technik und Mustertext, keine Rechtsdienstleistung im Einzelfall. (d) Ein Satz genügt, wie im Entwurf.
**Alternative.** Vor dem ersten Klick einen Hinweis einblenden („Sie verlassen diese Website“); die Schaltfläche „WhatsApp“ nur auf ausdrücklichen Wunsch einschalten.
### ZQ9: Vertragsrahmen ohne Abo (LB Nr. 1 Abs. 3)
**Frage.** Für Kunden ohne Wartungs-Abo gilt die LB „allein“. Sie regelt Leistung, Preis, Haftung, Ende, Einstellung und Widerruf, verweist für den Widerruf auf die Belehrung der Funktionen und für Datenschutz auf die Datenschutzerklärung. Fehlen Regeln, die sonst die Abo-Bedingungen liefern (Zahlung und Zahlungsverzug, Mängelrechte für digitale Dienstleistungen §§ 327 ff. BGB, Verjährung, Gerichtsstand, Textform, Änderungen der Bedingungen)? Sollen die Abo-Bedingungen auch für Kunden ohne Abo teilweise vereinbart werden (Verweis auf bestimmte Paragrafen), oder reichen die gesetzlichen Regeln?
**Fundstelle im Text.** LB Nr. 1 Abs. 3, Nr. 4 (Zahlung über Stripe), Nr. 7 Abs. 1 (Mängel in angemessener Frist), Nr. 8 (Haftung ausgeschrieben); Spec Abschnitt 9 Nr. 1 und Z19.
**Unsere Annahme.** Die gesetzlichen Regeln genügen für eine Einmalleistung von 30 €; die Haftung ist wegen der AGB-Kontrolle ausgeschrieben.
**Alternative.** Ausgewählte Paragrafen der Abo-Bedingungen (Zahlung, Mängel, Textform, Änderungen) in die LB aufnehmen oder auf sie verweisen.
### ZQ10: Hinweis zu Preisangaben in der Hinweis-Leiste
**Frage.** Der Editor zeigt beim Anlegen eines Hinweises, dessen Text „%“ oder „statt“ enthält, einen Hinweis zu Preisangaben (Endpreise, bei Waren der niedrigste Preis der letzten 30 Tage). LB Nr. 6 Abs. 2 verpflichtet den Kunden, Preise und Rabatte in Hinweisen rechtmäßig anzugeben, zum Beispiel als Endpreise und „bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage“; Nr. 12 Abs. 1 schließt eine Rechtsberatung aus. Sind diese Aussagen zutreffend und ausreichend, insbesondere die Beschränkung der 30-Tage-Regel auf Waren (Preisangabenverordnung) und die Abgrenzung, dass die Angaben des Kunden nicht von WEBklar geprüft werden (Nr. 8 Abs. 1)? Braucht es einen Hinweis zur Preisangabe auch für Leistungen, die Dienstleistungen sind?
**Fundstelle im Text.** LB Nr. 6 Abs. 2, Nr. 8 Abs. 1, Nr. 12 Abs. 1; Spec Abschnitt 3.4 Nr. 4 (Hinweis-Leiste); die genaue Zeile im Portal-Editor steht in `public/zeiten-leisten.js`.
**Unsere Annahme.** Der Hinweis im Editor und die Pflicht in der LB reichen als Warnung; die Prüfung bleibt beim Kunden. Die Zitate zur Preisangabenverordnung werden mit der Freigabe geprüft.
**Alternative.** Keinen Hinweis im Editor, nur die Pflicht in der LB; oder den Hinweis auf Preisermäßigungen insgesamt ausweiten.
### ZQ11: Aktivierung durch das Team ohne Zustimmung des Kunden
**Frage.** Das Team kann Öffnungszeiten live im WOMS für einen Kunden einschalten (Aktion „Aktivieren“, auch aus dem Zustand „abgeschaltet“). Das Programm speichert dabei keinen Zustimmungsnachweis (`einschaltenZeiten` mit `by: 'team'` ohne `nachweis`; Spec Abschnitt 5 und Ruling R32 lassen die Zustimmung für diesen Weg entfallen). Die LB setzt aber voraus, dass der Kunde den Haken setzt und auf „Einschalten“ oder „Zahlungspflichtig bestellen“ klickt (Nr. 1 Abs. 1). Beim Terminplaner verlangt TP2 für die Aktivierung durch das Team die Zustimmung des Kunden in Textform (F13a, `textformFuerAktivierung` in `server/routes/admin/funktionenAdmin.js` auf dem Zweig `feature/terminplaner-tp2`, Zeile 99: `if (key !== 'termine') return null`; die Prüfung gilt also nur für `termine`, und dort nur im Modus „on“, nicht für Pilotadressen). Soll diese Regel auf `zeiten` ausgeweitet werden (Kunde bestätigt in Textform, das Team vermerkt es), oder genügt es, dass die Funktion im Abo ohne Aufpreis ein freiwilliges Zusatzangebot ist und der Kunde die Aktivierung durch das Team ausdrücklich wünscht (Chat, Telefon)? Gilt das auch, wenn das Team ein Dokument wieder einschaltet, das der Kunde einmal gekauft hat?
**Fundstelle im Text.** LB Nr. 1 Abs. 1 und 2; Spec Abschnitt 5 (Team-Aktivierung), Ruling R32; Programm: `server/services/zeiten/einschalten.js` (`by: 'team'`), `server/routes/admin/funktionenAdmin.js` (`zeitenAktivieren`); TP2 F13a (Textform bei Aktivierung durch das Team, nur Terminplaner).
**Unsere Annahme.** Für die Gratis-Funktion genügt der ausdrückliche Wunsch des Kunden, den das Team im Feld „Notiz“ vermerkt; ein Zustimmungsnachweis fehlt dann bewusst. Bei einem Kunden ohne Wartungs-Abo wäre die Textform sauberer.
**Alternative.** Die Textform-Prüfung von TP2 auch für `zeiten` verlangen (Aktivieren nur mit Hinweis „Kunde hat in Textform zugestimmt“ und Notiz); die Aktivierung durch das Team nur für Kunden mit laufendem Wartungs-Abo zulassen.
### ZQ12: Pflichtinformationen als Haken und in K15
**Frage.** Beim Terminplaner gibt es neben `beginn` den Haken `information` („Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung …, Gesamtpreis …, voraussichtliche Dauer …“; `server/config.js`, `features.termine.consent`, TP2 Q20), und K15 (Bestätigung der Bestellung) trägt die Pflichtinformationen als Anhang. Für Öffnungszeiten live gibt es beides nicht: Das Register hat nur die Haken `leistung` und `beginn`, und K15 trägt für diese Funktion keine Vertragsunterlagen (der Block `anlagen` mit Fassung, Links und PDF-Anhang entfällt, weil das Register weder `avv` noch `widerruf` hat und `unterlagenAdressen` in `server/services/siteFeatures.js` dann `null` liefert; die Mail nennt nur Funktion, Preis und Dauer im Text). Genügt das für einen Verbrauchervertrag im elektronischen Geschäftsverkehr (§ 312d BGB, Art. 246a EGBGB, § 312j BGB: Information vor Vertragsschluss, Bestätigung auf einem dauerhaften Datenträger), weil die LB und die Bestellseite die Pflichtinformationen enthalten, oder braucht es wie beim Terminplaner einen eigenen Haken `information` und die Pflichtinformationen als Anhang oder Abschnitt der K15?
**Fundstelle im Text.** Register `features.zeiten.consent` (Haken `leistung`, `beginn`) gegen `features.termine.consent` (Haken `information`); Bestellseite `public/zeiten-bestellen.js` (Preis, Belehrung, Leistungsbeschreibung vor dem Knopf); K15 (`server/mail-templates/K15 Bestellung Funktion.html`, `bestellMail` und `unterlagenAdressen` in `server/services/siteFeatures.js`); LB Nr. 2 bis 4 und 11; TP2 Q20.
**Unsere Annahme.** Die Bestellseite zeigt Leistung, Preis, Umsatzsteuerhinweis, Widerrufsbelehrung und LB-Link vor dem Knopf; das genügt für die vorvertragliche Information. Ob die Bestätigungsmail (K15) die Pflichtinformationen und die Belehrung zusätzlich tragen muss (Textform auf einem dauerhaften Datenträger), ist offen; für den Terminplaner hat TP2 es mit dem Block `anlagen` gelöst.
**Alternative.** Wie beim Terminplaner: Haken `information` im Register (Fassung hochzählen) und die Pflichtinformationen als Abschnitt in K15.
## Nicht Teil der Kanzleiprüfung
- Der Hinweis in der Hinweis-Leiste („Aktion“ verlangt ein Enddatum, Texte höchstens 140 Zeichen) ist Oberfläche, keine Rechtsaussage.
- Technische Beobachtung beim Schreiben der Texte (kein Kanzlei-Thema, inzwischen behoben): Die öffentliche Widerrufsseite `widerrufen.html` kannte die Auswahl „Terminplaner (Einrichtung)“, aber keine „Öffnungszeiten live“; ein Verbraucher, der sie für 30 € bestellt hat, hätte „Funktion (z. B. News-Pinnwand)“ wählen müssen, und die Abwicklung hätte das der News-Pinnwand zugeordnet. Die Auswahl „Öffnungszeiten live“ gibt es jetzt (`ORDERS`, `FEATURE_OF_ORDER` und `orderLabel` in `server/routes/abo.js`, Radioknopf mit `data-text` in `public/widerrufen.html`).
@@ -0,0 +1,135 @@
# Öffnungszeiten live – Livegang-Notiz
Stand 05.10.2026, für Kenso. Grundlage: Spec Abschnitt 13.6, das Protokoll der Umsetzung (`.superpowers/sdd/2026-10-04-oeffnungszeiten-live/progress.md`, alle LIVEGANG-Hinweise und Rechtsrunde-Punkte) und die Gesamtprüfung (Task 37, Bericht `task-37-report.md` im selben Ordner). Nichts davon ist gepusht. Keine Rechtsberatung.
## Stand der drei Branches
| Repo | Worktree / Branch | Stand | Basis und Abstand |
|---|---|---|---|
| Portal (+ Runner) | `dev-zeiten`, `feature/oeffnungszeiten` | Inhalt von `90e8607` plus diese Notiz und die Fixrunde nach der Abschlussprüfung (`ecf67d6` Server, `aa9ee35` Skript und Editor, danach die Doku) | enthält TP2 bis `a629b25`; TP2 steht inzwischen auf `af63b69` (34 Commits weiter) |
| WOMS | `ticket.webklar.com/dev-zeiten`, `feature/oeffnungszeiten` | `919d96f` (Fixrunde: Statustext der Einrichtung) auf `3d67aa8` | auf WOMS-TP2 `72eedeb`; TP2 steht inzwischen auf `4c3f67e` (5 Commits weiter) |
| Relaunch | `webklar.com/dev-zeiten-station`, `feature/zeiten-station` | `8f5d522` | auf `main` `4139f31` (kein Abstand, Stand 05.10.) |
Letzte Prüfung vor der Fixrunde 05.10.2026, alles grün: Portal 4544/4545 (der eine Fehler war der bekannte wacklige `previewDevRouter`-Test, einzeln 12/12), Portal unter Node 20 (wie in Produktion) 4543/4543 ohne `analyticsContract`, Runner 177/177, UI-Prüfungen 31/31 + 8/8 + alle OK, WOMS 377/377 + `vite build`, Relaunch 349/349 + Build (Haupt-JS 128 084 Byte gz, Grenze 133 120).
Nach der Fixrunde (Abschlussprüfung, 05.10.2026), alles grün: Portal 4564/4564 (`npm test`), Runner 177/177, UI-Prüfungen Skript 34/34 und Portal 9/9, WOMS 378/378 + `vite build`. Der Node-20-Lauf (Schritt A.2) und die Relaunch-Prüfung (A.4) stehen für nach der TP2-Zusammenführung an, sie laufen ohnehin neu.
## A. Reihenfolge am Livegang-Tag
1. **TP2 geht zuerst live** (Portal und WOMS) nach seiner eigenen Reihenfolge (Kanzlei-Paket Abschnitt 7.6, mit `termine-setup.mjs` und `feature-setup.mjs`). Öffnungszeiten live baut auf TP2 auf und braucht dessen Schema (`aboEndeMailAt`, `disabledReason`, `featureConsents`).
2. **Portal neu aufsetzen.** In `dev-zeiten`: das jetzt live gegangene TP2 einmal hineinführen, mit `git fetch origin && git merge origin/main`. **Nicht** `git merge main`: die lokale Ref `main` steht noch auf `f1b43ca` (vor TP2), das gäbe „Already up to date“, und der Push würde abgelehnt. Ist TP2 noch nicht in `origin/main`: `git merge feature/terminplaner-tp2`. Es gibt einen Merge statt Rebase, damit die Commit-Nummern des Protokolls gültig bleiben. Eine Probe-Zusammenführung mit TP2 `af63b69` (`git merge-tree --write-tree`) ergab im Portal drei Textkonflikte, die so zu lösen sind (bei der ersten Zusammenführung `1d5a565` überlappten 14 Dateien):
- `server/routes/admin/funktionenAdmin.js`: unser Zeiten-Zweig (`zeitenAktivieren`, `zeitenAbschalten`, `ersatzTicket` in der Antwort) **und** TP2s Textform-/K16-Warnung (`warnung`) bleiben beide; die Antwort trägt beides.
- `server/services/retention.js`: unser `zeitenIds` (Zeiten vor dem Abschalten des Kunden löschen) **und** TP2s `consentIds`/`featureFieldIds` bleiben beide. TP2 `9a2a70b` („Fehlschlag hält den Kunden für den nächsten Lauf“) löst vermutlich die Retention-Hälfte von N9 (eine fehlgeschlagene `loescheZeitenVonKunde` wird nicht wiederholt): bei der Konfliktlösung prüfen, ob `loescheZeitenVonKunde` bei jedem Lauf aufgerufen wird, und nicht vorher separat patchen.
- `server/services/siteFeatures.js`: Textkonflikt, beide Seiten behalten. TP2 `b974b3a` legt `disable()` unter die Sperre `feature-activate:<id>` (Sperren sind nicht wiederbetretbar): nach der Zusammenführung prüfen, dass **kein Halter von `feature-activate` `disable()` aufruft** (`aboAbgleich` und `zeitenAbschalten` rufen es unter `feature-admin` auf: erlaubte Reihenfolge).
Danach alles erneut laufen lassen:
- `nice npm test` (nur `previewDevRouter` darf wackeln, dann einzeln wiederholen), `cd runner && nice npm test`
- Node 20: `docker run --rm --cpus 3 -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20 sh -c 'node --test --test-concurrency=3 $(ls test/*.test.js | grep -v analyticsContract)'`
- UI, je einzeln mit `nice` und vorher `free -m`: `test/ui/zeiten-embed.ui.mjs`, `test/ui/zeiten-portal.ui.mjs`, `test/ui/funktionen.ui.mjs`
3. **WOMS neu aufsetzen.** In `ticket.webklar.com/dev-zeiten`: die WOMS-Hauptlinie (`test`, mit TP2) hineinführen (`git fetch origin && git merge origin/test`). Erwarteter Konflikt (1 Datei): `src/components/customer/CustomerFunktionen.jsx`; dort bleiben unser `ersatzTicket` **und** TP2s `speicherStatus`/`K16_WARNUNG` (TP2 `4c3f67e` ändert dieselben `save()`-Zeilen sowie das Ende von `src/lib/funktionenView.js` und seinen Test, das Portal fügt dieselbe POST-Route um `warnung` ein). `K16_WARNUNG` nennt einen „AVV“, den Öffnungszeiten live nicht hat: nach der Zusammenführung prüfen und für die Zeiten anpassen. Danach `nice npm test` und `nice npx vite build`. `node_modules` ist dort im Git verfolgt und wurde von `npm ci` verändert: **nie stagen**.
4. **Relaunch prüfen.** `git log --oneline 4139f31..main` im Relaunch-Repo; gibt es neue Commits, `feature/zeiten-station` darauf aufsetzen, dann `nice npm test`, `nice npm run build` und `gzip -9 -c dist/assets/index-*.js | wc -c` (höchstens 133 120).
5. **Schema anlegen, vor dem Portal-Deploy.** `scripts/zeiten-setup.mjs` legt `oeffnungszeiten` und `zeitenErinnerungen` an und ergänzt `portalFeatures` um `unlockedBy`, `aboEndeAt`, `einbau`, `behaltenSessionId`, `einschaltMailAt`. Seit der Fixrunde schreibt `activate()` `aboEndeAt` und `behaltenSessionId` nur noch für Öffnungszeiten live (Parameter `extra`); News und Terminplaner brauchen die Felder nicht mehr und laufen auch vor dem Skript. Trotzdem erst das Schema, dann der Deploy: die Zeiten-Pfade schreiben die Felder. Das Skript braucht den neuen Code, also aus dem Worktree in einem Wegwerf-Container (kein `docker compose run`: der Container bekäme die Traefik-Labels des Portals). Die Env-Datei enthält Zugangsdaten: mit `umask 077` anlegen, nicht erst danach `chmod`. Erst `--check` (beim ersten Lauf Exit 1, erwartet: es fehlt ja noch alles), dann ohne, dann **noch einmal `--check`** (muss grün sein; die Index-Reihenfolge der Schema-Skripte hat am 30.09. schon einmal überrascht):
```
(umask 077; docker inspect webklar-kundenbereich-server -f '{{range .Config.Env}}{{println .}}{{end}}' > /root/zeiten-env.tmp)
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs --check
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs
docker run --rm --env-file /root/zeiten-env.tmp --network webklar-network -v /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten:/app:ro -w /app node:20-alpine node scripts/zeiten-setup.mjs --check
rm /root/zeiten-env.tmp
```
(Der Aufruf ist so noch nie gegen die Produktion gelaufen; er folgt dem Muster der früheren Schema-Skripte. Das Skript ist idempotent, `--check` schreibt nichts.)
6. **Stripe prüfen.** `node scripts/stripe-products.mjs --pruefen` (gleicher Wegwerf-Container). Erwartet wird ein Preis `webklar_zeiten_einrichtung_3000` (30 €, einmalig, `tax_behavior` inclusive, Standard-Produktbild). Fehlt er: `--anlegen`; danach `--pruefen` nochmal.
7. **Einstellungen, vor dem Push.** Zuerst Sicherungen: `cp /home/webklar/apps/docker-compose.yml /home/webklar/apps/docker-compose.yml.bak-2026-10-05-zeiten` und `(umask 077; cp /home/webklar/apps/.env /home/webklar/apps/.env.bak-2026-10-05-zeiten)` (`.env` enthält Zugangsdaten). Dann in `/home/webklar/apps/.env`: `ZEITEN_ENABLED=pilot`, `ZEITEN_PILOT_EMAILS=kenso.gri@gmail.com`, `ZEITEN_KEIN_NEUBAU=emmas-schatzkiste`. In `/home/webklar/apps/docker-compose.yml`, Dienst `kundenbereich-server`, drei Zeilen neben `TERMINE_ENABLED`: `- ZEITEN_ENABLED=${ZEITEN_ENABLED:-off}`, `- ZEITEN_PILOT_EMAILS=${ZEITEN_PILOT_EMAILS:-}`, `- ZEITEN_KEIN_NEUBAU=${ZEITEN_KEIN_NEUBAU:-emmas-schatzkiste}`. Prüfen mit `docker compose -f /home/webklar/apps/docker-compose.yml config -q` (keine Ausgabe = gültig), dann übernehmen mit `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Ein bloßes `docker restart` liest neue Umgebungswerte nicht. Das alles **vor dem Push** (Schritt 9): dann startet der Container mit dem alten Code und den neuen Werten (die kennt er nicht, harmlos), und der Neustart durch `deploy.sh` nach dem Push ist der einzige mit dem neuen Code (jeder Containerstart führt `apk add` und `npm install` aus).
8. **Runner: kein Image-Bau nötig.** Der Runner ist `node:20-alpine` mit eingehängtem `build/runner` (das Protokoll sagt „Image neu bauen“, das stimmt für die echte Konfiguration nicht). `deploy.sh` startet ihn neu, **nur wenn sich `runner/` in der Deploy-Spanne ändert** und die Warteschlange leer ist. `ZEITEN_PUBLIC_BASE` nicht setzen: die Vorgabe `https://project.webklar.com/embed/` stimmt. **Nach dem Push prüfen:** `docker inspect -f '{{.State.StartedAt}}' webklar-preview-runner` muss nach dem Push liegen (vorher 2026-10-02T16:17Z); sonst von Hand `docker restart webklar-preview-runner` (das Deploy-Log im Container `webklar-kundenbereich-build` wird täglich um 00:00 rotiert und ist als Beleg unzuverlässig; „naechster Deploy versucht es erneut“ stimmt nicht, ohne Änderung an `runner/` kommt kein Neustart). Ein alter Runner verwirft das neue Feld `zeiten` stillschweigend, der Einbau meldete dann „angestoßen“, setzt aber nichts ein.
9. **Pushen, in dieser Reihenfolge** (du pushst, ich nicht). Die Ziel-Ref steht immer dabei (`<lokaler Branch>:<Ziel>`), damit nichts unter dem falschen Namen ankommt:
1. Portal samt Runner: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-zeiten && git push origin feature/oeffnungszeiten:main`. **Der Portal-Webhook deployt jeden gepushten Branch** (`webhook-server.js` setzt `DEPLOY_BRANCH` aus `payload.ref`, `deploy.sh` macht `reset --hard origin/<branch>`): den Feature-Branch nie unter seinem eigenen Namen pushen (`git push origin feature/oeffnungszeiten`), das würde ihn auf die Produktion bringen.
2. WOMS erst **nach** dem Portal, sonst antwortet das Portal auf die neuen Zeiten-Aktionen mit 404: `cd /home/webklar/apps/ticket.webklar.com/dev-zeiten && git push origin feature/oeffnungszeiten:test`. Der WOMS-Webhook deployt nur `test` (`TICKET_DEPLOY_BRANCH`, Vorgabe `test`; Pushes auf andere Branches ignoriert er); `test` also nie vor dem Portal. Danach `setup-legal.mjs` (legt die Rechtliches-Karte `leistungsbeschreibung-zeiten` an, Schlüssel ≤ 36 Zeichen): **ungetestet gegen die Produktion**, zuerst `node scripts/setup-legal.mjs --dry-run` im WOMS-Worktree, dann `APPWRITE_API_KEY=… APPWRITE_ENDPOINT=… node scripts/setup-legal.mjs`. Der Schlüssel (`APPWRITE_API_KEY`) ist nötig; der Standard-Endpoint des Skripts ist `https://appwrite.webklar.com/v1`, das WOMS-Frontend nutzt in der Vorgabe `https://ticket.webklar.com/v1`: den Endpoint vorher gegen den ausliefernden Container prüfen und ausdrücklich setzen.
3. Relaunch: `cd /home/webklar/apps/webklar.com/dev-zeiten-station && git push origin feature/zeiten-station:main`. Die Station „Öffnungszeiten live“ steht dort auf `bald: true` („Bald verfügbar“) und bleibt so bis Schritt C.
10. **Live-Checks nach dem Deploy** (Portal und WOMS sind da):
- Öffentliche Schnittstelle kennt einen unbekannten Schlüssel nicht: `curl -s https://project.webklar.com/api/public/zeiten/v1/aaaaaaaaaaaaaaaa` antwortet `{"aktiv":false}`.
- Skript: `curl -sI https://project.webklar.com/embed/zeiten.js` ist 200, mit `Access-Control-Allow-Origin` und `Cache-Control`.
- Leistungsbeschreibung: `curl -sI https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html` ist 200.
- WOMS: Kunde → Reiter „Funktionen“ lädt ohne Fehler (auch für ein Projekt ohne Zeiten).
- Portal-Log: `docker logs --since 15m webklar-kundenbereich-server 2>&1 | grep -i "unknown attribute"` bleibt leer (sonst fehlt ein Schema-Feld, Schritt 5).
- Stripe: Webhook-Adresse und Ereignisliste im Dashboard sind unverändert (die Funktion nutzt dieselben Ereignisse wie News und Terminplaner; es kommt nichts Neues dazu).
- **Rückweg**, falls etwas hakt: `ZEITEN_ENABLED=off` in `.env`, dann `docker compose -f /home/webklar/apps/docker-compose.yml up -d --no-deps kundenbereich-server`. Die Kachel verschwindet, die Schnittstelle antwortet `aktiv: false`, die Platzhalter zeigen ihr Ersatz-HTML, der Abo-Abgleich läuft weiter. Das Schema ist rein additiv (neue Collections und Felder), es gibt nichts zurückzubauen; ein Code-Rollback ist bei `off` nicht nötig.
11. **Pilot-Probelauf** (Abschnitt B), danach Rechtsrunde abschließen (Abschnitt D), dann Abschnitt C.
## B. Probelauf mit `ZEITEN_ENABLED=pilot` auf `test-website`
Nur dein Konto sieht die Kachel. Beachte: Das Portal prüft nur „Leistungsbeschreibung vorhanden“, nicht „freigegeben“ (`unterlagenBereit` ist für Zeiten immer wahr). Der Pilot ist deshalb der einzige Schutz vor dem Entwurfstext, bis `on` kommt.
1. Kachel im Funktionen-Tab sichtbar. Je nach Konto: „Einschalten“ (Wartungs-Abo aktiv) oder „30 € einmalig“. Den Bezahlweg nur mit echter 30-€-Zahlung testen (danach erstatten) oder weglassen; die Tests decken ihn ab.
2. Einschalten: Ticket „Öffnungszeiten live einbauen“ in WOMS, Mail K27, `einbau` = `angestossen` (Runner-Log) oder `manuell`.
3. Editor: Wochenplan mit Fenstern und Mitternacht, zweiter Plan, Bundesland und Feiertage, Urlaub (Hinweis-Vorschlag), Hinweis-Leiste, Kontakt-Leiste mit Telefon und Adresse, „Für Google kopieren“, Uhrzeit-Regler.
4. Preview der `test-website` nach dem Neubau: im Quelltext `<script data-wk-zeiten … data-site="…">`; Hinweis-Leiste oben (schiebt den Header?), Kontakt-Leiste auf dem Handy, JSON-LD im Quelltext (kein Duplikat), Platzhalter mit Status, Tabelle und Badge; Ersatz-HTML aus WOMS („Ersatz-HTML kopieren“) in die Platzhalter. Dazu der Datenschutz-Absatz aus `docs/funktionen/website-datenschutz-absatz.md` (Entwurf) auf der Test-Seite.
5. WOMS: Panel (Zeiten-Einstellungen `navSelektor`, Leisten aus), „Aktivieren ohne Zahlung“ und „Abschalten“ am Testprojekt; Ticket schließen ⇒ nach spätestens 5 Minuten `active` und Mail K16; Abschalten ⇒ Ticket „Ersatz-Tabelle aktualisieren“ (nur wenn der Wochenplan vom Ersatz-HTML abweicht).
6. Mails K27 bis K30 ansehen (K28 über einen Feiertag in 1–7 Tagen, K29/K30 nur mit einem Abo-Ende; sonst über die Tests bewertet).
7. Aufräumen: Funktion am Testprojekt abschalten, Ticket schließen. Das Abschalten durch das Team löscht **nie** Daten; die Testdaten löschst du von Hand in der Appwrite-Konsole (Datenbank des Portals): im Dokument der Funktion in `portalFeatures` (`featureKey` = `zeiten`, `projectId` = das Testprojekt) die ID notieren; in der Collection `oeffnungszeiten` das Dokument mit genau dieser ID löschen (es trägt die Eingaben, das Team-Feld und den Ersatz-Hash); in `zeitenErinnerungen` alle Dokumente mit `featureId` = dieser ID löschen (die Erinnerungs-Merker). Danach ist die Schnittstelle des Testprojekts nach spätestens 60 Sekunden (Zwischenspeicher) wieder leer. (Automatisch gelöscht wird nur beim Ende des Abos nach 30 Tagen und beim Löschen des Kundenkontos.)
## C. Schalter `on` (letzter Schritt)
Erst wenn Abschnitt B ohne offene Fehler und Abschnitt D vollständig erledigt ist:
1. `ZEITEN_ENABLED=on` in `.env`, `docker compose up -d kundenbereich-server`.
2. Relaunch: in `src/components/home/Kundenportal.tsx` den Eintrag `oeffnungszeiten` auf `bald: false` (die Marke „Bald verfügbar“ verschwindet). **Dabei den Test anpassen**: `relaunch-kundenportal.test.tsx` (um Zeile 142 bis 156) pinnt genau zwei „Bald verfügbar“; nach dem Umschalten ist es eines weniger. Dann Tests und Build, du pushst. Das Handy-Bild `phone-12-zeiten` zeigt den Einleitungstext des Editors (und einen laufenden Urlaubs-Hinweis): nach der Textrunde **neu aufnehmen**, bevor `bald: false` live geht.
3. Logs beobachten (`[zeiten]`, `[zeiten-jobs]`): Abo-Abgleich täglich, Feiertags-Erinnerung stündlich zwischen 08:00 und 20:00.
4. Zurück auf `off`: Kachel verschwindet, die öffentliche Schnittstelle antwortet `aktiv: false`, die Platzhalter zeigen ihr Ersatz-HTML; der Abo-Abgleich läuft weiter.
## D. Rechtsrunde und Texte (Voraussetzung für `on`)
Reihenfolge laut Spec Abschnitt 9: gemeinsame TP2-Bausteine, Leistungsbeschreibung, Datenschutz, Oberflächen- und Mailtexte, Fragen an die Kanzlei, Uploads, `on`.
1. **Gemeinsame TP2-Bausteine** (Block F: Widerrufsbelehrung, AVV) zuerst.
2. **Leistungsbeschreibung** `public/vertragsunterlagen/zeiten-leistungsbeschreibung.html`: Entwurf („ENTWURF – vor Livegang Rechtsrunde“ im Kopf). Wortlaut-Punkte der Gesamtprüfung: Nr. 4 Abs. 2 „sobald das Wartungs-Abo aktiv ist“; Nr. 1 Abs. 1 (wann die Vereinbarung bei Team-Aktivierung entsteht, siehe ZQ11); Nr. 3 Abs. 2 „oder stehen sollen“ streichen; Nr. 3 Abs. 3 „bestätigt den Einbau als abgeschlossen“ und „Tag, der nicht geschlossen ist“; Nr. 8 Abs. 7 zweiter Satz unklar; „Behalten nach Abschaltung“. Danach Fassung festlegen (Register `leistungsbeschreibung.version` und `consent.version` hochzählen, Tests nachziehen).
3. **Datenschutz Portal § 6e (6d ist seit dem Webshop vergeben)** (`docs/rechtliches/zeiten-datenschutz-6e-entwurf.md`) in `public/datenschutz.html` einbauen, mit dem Anker `id="zeiten"`, plus der Satz in § 6 (Öffnungszeiten im Kundenauftrag). **Er muss live sein, bevor der Link der Leistungsbeschreibung funktioniert.** Danach `scripts/build-legal-pdfs.sh datenschutz` (nicht `all`), Prüfsumme nachziehen, `test/legalTexts.test.js` und `test/zeitenRecht.test.js` anpassen. Wortlaut-Punkte: „Ihre Eingaben“ gegen „der Kunde“, „keine Werbung“ gegen ZQ6.
4. **Widerrufsbelehrung für das 30-€-Produkt freigeben.** Die Bestellseite muss genau `featureHaken('zeiten').belehrung()` vor dem Knopf zeigen (Ruling R32), sonst belegt der Nachweis einen Text, den der Kunde nie sah.
5. **Wertersatz-Satz** (§ 357a Abs. 2 BGB) im Beginn-Haken der bezahlten Bestellung, wie bei News und Terminplaner (ZQ3): im Code ergänzt (Fixrunde, Fassung `2026-10-05`); der Wortlaut bleibt Entwurf bis zur Rechtsrunde, danach die Fassung auf den Tag der Freigabe setzen (Pins in `test/zeitenRegister.test.js`).
6. **Widerruf-Seite:** in der Fixrunde ergänzt (`public/widerrufen.html`, `server/routes/abo.js` mit `ORDERS`, `FEATURE_OF_ORDER`, `orderLabel`); ein zahlender Verbraucher, der widerruft, wird jetzt den Öffnungszeiten zugeordnet. Nichts mehr zu tun, außer beim Livegang die Seite einmal zu öffnen.
7. **Fragen an die Kanzlei:** `docs/rechtliches/zeiten-kanzlei-fragen.md` (ZQ1 bis ZQ12) ins Kanzlei-Paket des Terminplaners als Q27 ff. übernehmen (AVV Funktionen per Haken, Behalten-Haken, Wertersatz, Haftung ohne Abo, Ende mit dem Abo, Mails, Einstellung bei Unternehmern, Website-Absatz, Vertragsrahmen ohne Abo, Preisangaben in der Hinweis-Leiste, Aktivierung durch das Team ohne Zustimmung, Pflichtinformationen als Haken und in K15).
8. **K02/K03:** Entwurf für den Block `funktionen` in `docs/rechtliches/zeiten-k02-k03-entwurf.md`. Nach der Freigabe die Vorlagen ändern und die Pins nachziehen (`test/mailTemplates.test.js`, `test/publicForms.test.js` `FUNKTIONEN_SATZ`).
9. **Website-Absatz** `docs/funktionen/website-datenschutz-absatz.md` (Varianten A/B, Baustein Route und WhatsApp nur wenn die Leiste die Knöpfe zeigt): freigeben; der Einbau auf einer Kundenseite erst, wenn er in deren Datenschutzerklärung steht.
10. **K15 für bezahlte Öffnungszeiten** trägt keine Vertragsunterlagen und keine Belehrung als Anhang (dauerhafter Datenträger): entscheiden, ob nötig (Frage ZQ12 zusammen mit dem Haken `information`).
11. **Mailtexte K27 bis K30** und die Texte im Editor in deiner Textrunde: K28 sagt „deine Zeiten gelten normal“, obwohl die Seite an gesetzlichen Feiertagen „geschlossen“ zeigt, wenn du nichts änderst; K27-Variante „wieder eingeschaltet“ (ohne „per Ticket“ / „von Hand“); T19-Text spricht von „Einrichtung“; `disabledMessage` schreibt „du“ klein; eigene Texte des Editors (Preishinweis, Vorschau); Betreffzeile K29 nennt „Wartungs-Abo“ (so gewollt).
12. **Uploads:** die Fassungen (Leistungsbeschreibung, Datenschutzerklärung) in WOMS „Rechtliches“ hochladen (Karte `leistungsbeschreibung-zeiten`).
## E. Offene Code-Punkte (Checkliste für die letzte Fixrunde)
Mit ☐ markiert, was noch im Code offen ist; die Fixrunde des Controllers kann einzelne schließen (dann hier abhaken).
**Vor `on` nötig**
- [x] N1 Wertersatz-Satz im Beginn-Haken `features.zeiten.consent` (`server/config.js`), Fassung `2026-10-05`, Pins in `test/zeitenRegister.test.js` (Fixrunde `ecf67d6`). Der Wortlaut bleibt Entwurf (ZQ3).
- [x] N2 „Öffnungszeiten live“ in `ORDERS`, `FEATURE_OF_ORDER` und im Bestell-Label von `server/routes/abo.js`, im Radioknopf von `public/widerrufen.html` und in den Pins (`widerrufen_html_radio`, neuer Test `widerruf_zeiten`); `public/legal-form.js` braucht keine Änderung, es liest `data-text` (Fixrunde `ecf67d6`).
- [ ] N3 Rechtstexte aus den Entwürfen übernehmen (Abschnitt D, Nr. 2 und 3) und den Entwurfskommentar der Leistungsbeschreibung entfernen.
- [ ] N4 Entscheidung zu Anhängen in K15 für Zeiten (D, Nr. 10); K02/K03-Block (D, Nr. 8).
**Härtung (vor `on` empfohlen, für den Pilot nicht nötig)**
- [x] N5 Website-Skript: im JSON-LD `<` als `\u003c` schreiben (`jsonLdText`, Test); Kopf-Elemente bekommen bei Scroll ≥ Leistenhöhe ihren eigenen Inline-`top` zurück (UI-Prüfung `fester_header_versteckt_beim_scrollen`); dazu späte Platzhalter (Einzelseiten-Apps) über einen entprellten MutationObserver (Fixrunde `aa9ee35`).
- [x] N6 Abo-Abgleich: K30 gilt als offen, solange `aboEndeMailAt` vor `disabledAt` liegt (das Zurücksetzen nach `disable()` entfällt); K30 geht erst nach dem Ersatz-Ticket raus, scheitert es, wiederholt der nächste Lauf beides (K30 so höchstens einmal); `activatedAt` wird bei der Löschung geleert (Fixrunde `ecf67d6`).
- [ ] N7 Team: Sperre `zeiten-ersatz:<docId>` gegen zwei Ersatz-Tickets (paralleles Abschalten und Abgleich); `PATCH …/zeiten/team` nur für `setup`/`active`/`disabled` (409 sonst); `createFeatureTicket` mit `fallbackDetails` für das lange Ersatz-Ticket.
- [x] N8 WOMS „Ticket nachlegen“ bei Abo- oder Team-Zeiten ohne `ticketId`: das Portal geht über `holeZeitenNach` (Fixrunde `ecf67d6`). Offen bleiben die Texte „keine Zahlung vermerkt“ (bei gekauften Zeiten ohne Zahlungsvermerk) und der 409 der Abnahme („noch keine Öffnungszeiten eingetragen“).
- [ ] N9 Löschen: in `loescheFunktion` erst die Merker, dann das Dokument; fehlgeschlagene Löschung in `retention.js` beim nächsten Lauf wiederholen.
- [ ] N10 `heileZeiten` in `try/finally`, wenn `activateClosedTickets` wirft.
- [ ] N13 Nach dem Löschen (Abo-Ende + 30 Tage, Kundenlöschung) setzt `loesche()` keinen Rückgabe-Marker wie TP2 (`purchaseStatus: 'zurueckgesetzt'`): eine von Stripe erneut zugestellte `checkout.session.completed` einer früher bezahlten Zeiten-Bestellung würde neu einrichten (Ticket, K15, T16). Im Pilot praktisch ausgeschlossen (Merge-Bericht 06.10.2026, Punkt 3).
**Qualität (kann nach `on`)**
- [ ] N11 Zeitlogik: `wechselListe` um Schwellen (Ende minus 24 h und minus 7 Tage) ergänzen (Texte wie „bis Sonntag Mitternacht“ bleiben sonst den ganzen Sonntag stehen); Mitternachts-Einträge nicht zusammenfassen („heute“ hinkt sonst bis zu 10 Minuten nach).
- [ ] N12 Oberfläche: Portal-Knopf auf dem Desktop 40 px hoch (Ziel 44 px); Relaunch-Stationsliste kann bei 721–780 px Fensterhöhe überlaufen (Schwelle prüfen).
**Weitere Punkte der Fixrunde nach der Abschlussprüfung (erledigt)**
- [x] `activate()` schreibt die Zeiten-Felder nur noch über `extra` (Abo-Rückkehr und Team-Aktivierung der Zeiten): News und Terminplaner brechen auch vor dem Schema-Skript nicht mehr mit „Unknown attribute“.
- [x] Die Einschalten-Route (`/api/funktionen/zeiten/einschalten`) nimmt nur `{ zustimmung }`, nicht die Altform `{ consent: true }` ohne Fassungsprüfung.
- [x] Die Team-Aktivierung behält `kauf` und `unlockedByPurchase`, wenn das Dokument vorher gekauft war (die Widerrufs-Zuordnung bleibt richtig; `einbau` bleibt dabei stehen, weil für gekaufte Dokumente das Nachholen nicht neu baut).
- [x] WOMS: „Leisten live – Einbau läuft“ nur bei `einbau` = `angestossen`, sonst „Einbau läuft (Team setzt die Skript-Zeile von Hand)“ (`919d96f`).
- [x] Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar; Website-Skript: Links mit Zugangsdaten werden verworfen, die Adresse der Schnittstelle bestimmt nur das eigene Script-Tag.
## F. Bewusste Abweichungen und Hinweise
- Schema der neuen `portalFeatures`-Felder liegt in `scripts/zeiten-setup.mjs` (nicht in `feature-setup.mjs`), weil dessen Tests genau drei Attribute festschreiben.
- Das Angebot liefert `GET /api/funktionen` als `zeiten` auf oberster Ebene plus `zeitenStatus` je Projekt (die Spec nennt `zeitenAngebot` je Projekt); die Kachel entscheidet nach `inklusive` und `grund` (Ruling R20).
- Der Editor einer abgeschalteten Funktion (`disabled`) ist nur lesbar (Spec Z13 sagt „nie gesperrt“); die Website zeigt dann ohnehin nichts mehr.
- Der Block `funktionen` in K02/K03 ist bewusst nur ein Entwurf (Rechtstext mit Pins).
- Schalter aus (`off`): `GET /api/public/zeiten/v1/<siteKey>` antwortet `aktiv: false`; die Jobs für Abo-Abgleich laufen unabhängig vom Schalter (ein beendetes Abo muss abschalten und löschen können).
- Wie im Terminplaner-Livegang: den Code zu einer ruhigen Zeit deployen.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,405 @@
# Aufträge und Rechnungen – Umsetzungsplan, Übersicht und Verträge
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> Dieser Plan ist auf **parallele Stränge** ausgelegt (Nutzerwunsch 06.10.2026: „so viele Sub-Agents wie möglich, viel nebenbei, zum Schluss die Kontrolle“). Jeder Strang hat eine eigene Plan-Datei, einen eigenen Worktree und **eigene Dateien**. Diese Übersicht ist der Vertrag zwischen den Strängen: Namen, Signaturen, Datenformen und Endpunkte stehen nur hier und gelten für alle.
**Goal:** Admins legen in WOMS beim Kunden Auftragsbestätigungen (AB) und Rechnungen zu einem Projekt an, sehen je Projekt ein Projektkonto (vereinbart, berechnet, bezahlt, offen, Zeit) und rechnen per Anzahlung, Schlussrechnung, nach Aufwand oder als Festbetrag ab.
**Architecture:** Der Portal-Server (`kundenbereich.webklar.com`) bekommt einen Baustein `server/services/auftraege/` (Speicher, Projektkonto, Positionsbau, AB-Logik, PDF über pdfkit, Bestätigungs-Token, Mails) und erweitert die bestehende Stripe-Rechnung (`stripeFinance.js`). WOMS (`ticket.webklar.com`) zeigt Projektkonten, AB- und Rechnungsdialoge im neuen weißen Design; das Kundenportal bekommt eine Bestätigungsseite, einen Abschnitt „Aufträge“ und die Vertragsart „Projektauftrag“ auf der Widerrufsseite. Der AB-Teil hängt hinter dem Schalter `AUFTRAEGE_ENABLED` (Vorgabe `off`), damit der Rechnungsteil vor der Rechtsrunde live gehen kann.
**Tech Stack:** Node 20.19 (Container) und 24 (Host), Express 4, Appwrite-REST über `server/services/appwriteAdmin.js`, Stripe SDK 19 (API 2025-10-29.clover), pdfkit (neu), nodemailer mit HTML-Mailvorlagen (`server/mail-templates`), Tests mit `node --test` und den Nachbauten in `test/support/` (Appwrite, Stripe, Mail-Senke, Test-App); WOMS: React 18, Vite, Tests mit `npm test` (`tests/unit/*.test.mjs`).
**Spec:** `docs/superpowers/specs/2026-10-06-auftraege-und-rechnungen-design.md` (Commit e00b4ae). Jeder Implementierer liest Spec **und** diese Übersicht **und** seine Strang-Datei.
---
## Ausführungsmodell
```
Phase A L0 Fundament (1 Agent) ───────────────────────────────┐
Phase B L1 Projektkonto · L2 Rechnungen · L3 AB-Kern · L4 AB-PDF · L5 Bestätigung+Mails · L6 Kundenportal · L7 Rechtstexte (7 Agents parallel)
Phase W [WOMS-Redesign P4+P6+P7 zusammengeführt] → W0 WOMS-Basis (1 Agent) → W1 Rechnungs-UI · W2 AB-UI (2 Agents parallel)
Phase Z Z1 Portal-Integration ‖ Z2 WOMS-Integration → Z3 Kontrolle (5 Prüfer parallel → Bestätigung → Korrektur) → Übergabe
```
- **Ein Implementierer je Strang** (Modell `sonnet`), Tasks im Strang der Reihe nach, TDD. **Keine Reviewer je Task** (Nutzerwunsch); jeder Strang endet mit seiner **Selbstprüfung** (letzter Task der Strang-Datei: eigene Tests grün, Vertragsliste abgehakt, `git status` sauber).
- **Phase W** startet, sobald die Redesign-Stränge Kunden (P4, P6) und Finanzen (P7) in `feature/design-weiss` zusammengeführt und getaggt sind (Tags `design-p4` … `design-p7`, siehe Memory „woms-theme-weiss“). Fehlen sie beim Start von Phase W, fragt der Orchestrator den Nutzer (warten oder auf `test` bauen und eine Mitnahme-Notiz in `/root/reports/ticketsystem-design-2026-10-04/strang/mitnehmen.md` hinterlegen).
- **Phase Z**: Z1 und Z2 dürfen parallel laufen; Z3 erst, wenn beide fertig sind. Kleine Prüfungen in Z3 (Bestätigung einzelner Befunde, Ein-Datei-Korrekturen) mit `haiku`. Kein `opus`/`fable`.
- Ausführung wahlweise mit dem Workflow-Werkzeug (Phasen wie oben) oder mit parallelen Agent-Aufrufen; die Plan-Dateien sind für beides gleich.
### Stränge, Plan-Dateien, Worktrees
| Strang | Plan-Datei (`docs/superpowers/plans/`) | Worktree | Branch | Start |
|---|---|---|---|---|
| L0 Fundament | `2026-10-06-auftraege-01-fundament.md` | `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege` | `feature/auftraege` | sofort |
| L1 Projektkonto | `…-02-projektkonto.md` | `…/dev-auftraege-l1` | `auftraege/l1-projektkonto` | nach L0 |
| L2 Rechnungen | `…-03-rechnungen.md` | `…/dev-auftraege-l2` | `auftraege/l2-rechnungen` | nach L0 |
| L3 AB-Kern | `…-04-ab-kern.md` | `…/dev-auftraege-l3` | `auftraege/l3-ab-kern` | nach L0 |
| L4 AB-PDF | `…-05-ab-pdf.md` | `…/dev-auftraege-l4` | `auftraege/l4-ab-pdf` | nach L0 |
| L5 Bestätigung + Mails | `…-06-bestaetigung.md` | `…/dev-auftraege-l5` | `auftraege/l5-bestaetigung` | nach L0 |
| L6 Kundenportal | `…-07-kundenportal.md` | `…/dev-auftraege-l6` | `auftraege/l6-kundenportal` | nach L0 |
| L7 Rechtstexte | `…-08-rechtstexte.md` | `…/dev-auftraege-l7` | `auftraege/l7-rechtstexte` | nach L0 |
| W0 WOMS-Basis | `…-09-woms-basis.md` | `/home/webklar/apps/ticket.webklar.com/dev-auftraege` | `feature/auftraege` (WOMS) | Redesign P4+P6+P7 da |
| W1 Rechnungs-UI | `…-10-woms-rechnungen.md` | `…/ticket.webklar.com/dev-auftraege-w1` | `auftraege/w1-rechnungen` | nach W0 |
| W2 AB-UI | `…-11-woms-auftraege.md` | `…/ticket.webklar.com/dev-auftraege-w2` | `auftraege/w2-auftraege` | nach W0 |
| Z Integration + Kontrolle | `…-12-integration-kontrolle.md` | Basis-Worktrees | `feature/auftraege` (beide Repos) | nach Phase B bzw. W |
Lane-Worktrees entstehen aus dem Stand **nach L0**:
`git -C /home/webklar/apps/kundenbereich.webklar.com/dev-auftraege worktree add ../dev-auftraege-l1 -b auftraege/l1-projektkonto feature/auftraege`
und bekommen `ln -s ../dev-auftraege/node_modules node_modules` (L0 legt dort ein echtes `node_modules` an). Der Symlink ist unversioniert: **nie `git add -A`**, immer Dateien einzeln.
### Dateizuständigkeit (Konfliktschutz)
Jede Datei hat genau einen Besitzer-Strang. Ein Strang ändert **nur** seine Dateien; braucht er etwas aus einer fremden Datei, nutzt er den Vertrag unten. Gemeinsame Dateien (`server/config.js`, `server/index.js`, `server/routes/team/index.js`, `package.json`, `package-lock.json`, `test/support/*`, `server/services/auftraege/konstanten.js`) ändert **nur L0**. Ausnahmen stehen in der Tabelle.
| Datei | Besitzer |
|---|---|
| `server/services/auftraege/schema.js`, `konstanten.js`, `schalter.js`, `auftragStore.js`, `projektRechnungenStore.js`, `auftragSicht.js`, `arbeitsblaetter.js`, `ablage.js`, `fehler.js` | L0 |
| `server/services/auftraege/texte.js` | L0 legt an (vorläufige Wortlaute), **danach nur L7** (nur Wortlaute, Exportnamen bleiben) |
| `scripts/auftraege-setup.mjs`, `server/services/finance/settings.js` (Feld `steuernummer`) | L0 |
| Router-Gerüste `server/routes/admin/projektkonten.js`, `server/routes/team/arbeitsblaetter.js` | L0 legt an, L1 füllt |
| Router-Gerüst `server/routes/admin/auftraege.js` | L0 legt an, L3 füllt |
| Router-Gerüste `server/routes/auftrag.js`, `server/routes/auftraegeKunde.js` | L0 legt an, L5 füllt |
| `server/services/auftraege/projektkonto.js` | L1 |
| `server/services/auftraege/positionen.js`, `rechnungsbau.js`; `server/services/finance/stripeFinance.js`; `server/routes/admin/finanzen.js`; `test/support/fakeStripe.js` (Ausnahme von „nur L0“: nur Ergänzungen für Rechnungsposten mit `amount`, `custom_fields`, `rendering`) | L2 |
| `server/services/auftraege/nummern.js`, `auftraege.js` | L3 |
| `server/services/auftraege/pdf.js`, `server/assets/fonts/*` | L4 |
| `server/services/auftraege/token.js`, `bestaetigung.js`, `mails.js`; `server/mail-templates/K31 …`, `K32 …`, `T22 …`; `server/routes/abo.js` (Widerruf `projekt`); `test/mailTemplates.test.js` (falls er die Vorlagenliste pinnt) | L5 |
| `public/auftrag.html`, `public/auftrag.js`, `public/auftrag.css`, `public/dashboard.html`, `public/app.js`, `public/widerrufen.html`, `server/services/customerInvoices.js` | L6 |
| `public/rechtliches/*` (neue Dateien), `scripts/build-legal-html.mjs`, `scripts/build-legal-pdfs.sh`, `public/datenschutz.html`, `docs/WEBklar-Datenschutzerklaerung.pdf` (+ `.sha256`), `konstanten.js` **nur** `AGB_FASSUNG` | L7 |
| Tests: jede Lane legt eigene Dateien `test/auftraege<Thema>.test.js` an; bestehende Testdateien nur, wenn die Tabelle sie nennt | je Lane |
### Nachträge nach der Planrunde (verbindlich, 06.10.2026)
Die Strang-Pläne wurden parallel geschrieben; ihre „Vertragsfragen“ sind hier entschieden. Wo ein Strang-Plan davon abweicht, gilt dieser Abschnitt.
**Zusätzliche Dateien je Strang (Zuständigkeit):**
- L0: zusätzlich `.gitignore` (Eintrag `node_modules` **ohne** Schrägstrich, damit auch der Symlink ignoriert wird) und `fontkit` als direkte Abhängigkeit in `package.json` (Version wie die pdfkit-Abhängigkeit).
- L2: zusätzlich `test/financeStripeRead.test.js` (erwarteter Schlüsselsatz von `mapInvoice`).
- L5: zusätzlich `server/mail-templates/K04 Eingang Widerruf.html` und `T09 Widerruf eingegangen.html` (nur Block `projekt`).
- L6: zusätzlich `public/auftrag-ansicht.js`, `public/auftrag-dom.js`, `public/auftrag-portal.js`, `public/auftrag-widerruf.js`, `test/auftraegeKundenportalDaten.js`, `test/ui/auftraege-kundenportal.ui.mjs`, je eine Zeile in `test/customerInvoices.test.js` und `test/publicForms.test.js`.
- L7: zusätzlich `docs/rechtliches/agb.md`, `docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt`, Stand-Zeile in `test/legalTexts.test.js`.
- Alle: Testhilfen ohne `.test.js` (z. B. `test/helpers/auftraegeAb.js`, `test/auftraege…Hilfe.js`) gehören dem anlegenden Strang.
- WOMS: W0 `src/lib/financeApi.js`, `src/lib/financeRequest.js` (Blob), `src/lib/teamApi.js`, `src/lib/auftraegeEndpoints.js`, `src/lib/auftraegeAnsicht.js`, `src/lib/customerTabs.js` (+ Test), Container und vier Startdateien unter `src/components/auftraege/`, `CustomerDetailPage.jsx` (nur Reiterzeile), `CustomerInvoices.jsx` (gelöscht); W1 `ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `projektkonto.css`, `rechnung-dialog.css`, `src/lib/projektkontoView.js`, `src/lib/rechnungDialogView.js` (+ weitere eigene Libs), `InvoiceTable.jsx`, `WorksheetList.jsx`, `WorksheetTimesModal.jsx`, `InvoiceCreateModal.jsx` (gelöscht); W2 `AuftragListe.jsx`, `AuftragDialog.jsx`, `auftrag.css`, `src/lib/auftragView.js`, `src/lib/auftragPdf.js`, `src/lib/kundenFelder.js`, `CustomerDetailPage.jsx` (Feld Kundenart), `SettingsCard.jsx`.
**Sperren:** siehe V2 (zusammengesetzte Einzel-Schlüssel). Reihenfolge: wer beide braucht, nimmt erst `sperreRechnung`, darin `sperreAuftrag`; innerhalb von `sperreAuftrag` nie `sperreRechnung`. Mails (K32/T22) erst nach Freigabe der Sperre.
**L0:** `ablage.entfernePdf(pdfDatei)` (räumt eine abgelegte PDF weg, wenn danach etwas scheitert; L3 nutzt genau diesen Namen); Export `AUFTRAEGE_DATETIMES`; `schemaKeys` auch für `financeSettings`; `oeffentlicheAnsicht` liefert `texte.kleinunternehmer`; Steuernummer mit Buchstaben (V4).
**L1:** Zeitwert = Summe der je Tag gerundeten Werte (wie `aufwandPositionen`); `projektTyp` = Anzeigename der Projektart; `offenCents` bei Stripe-Ausfall `null`; „Ohne Projekt“ = finalisierte Rechnungen mit `source: 'projekt'` ohne aktive Projektrechnung plus verwaiste Projektrechnungen; archivierte Projekte nur mit Geldbezug; bei ausgeschaltetem Schalter werden ABs nicht gelesen.
**L2:** `baueRechnung` bekommt zusätzlich `createdBy`; `zeilentexte[schluessel]` ersetzt den ganzen Text vor dem letzten „ – “ (einschließlich Datum bzw. „KW …“); **Selbstheilung**: stößt eine Prüfung („bereits abgerechnet“, offener Entwurf zur AB) auf eine Projektrechnung, deren Stripe-Rechnung storniert oder gelöscht ist, wird sie `aktiv: false` und die Prüfung wiederholt; nur finalisierte Rechnungen zählen als „bereits berechnet“, ein Schluss-Entwurf blockiert weitere Anzahlungen; Schlussrechnung bis 40 Zeilen; Aufwand ohne `auftragId` nimmt die jüngste gültige Aufwands-AB nur bei eingeschaltetem Schalter (sonst 100 €); Buchungsfehler nach einem Stripe-Erfolg werden protokolliert, nicht geworfen; Schreibweisen „KW 05/2026“ (Schlüssel `KW-2026-05`), `stundenText` ohne Endnullen, Texte eines Tages mit „; “ verbunden.
**L3:** Senden: Sperren `SPERRE_NUMMER` + `sperreAuftrag(id)` → **innerhalb** der Sperre Eingabe und Kunde prüfen → Nummer → PDF (`AuftragError` aus `erzeugeAuftragPdf`, z. B. 409 „Entwurf unvollständig …“, wird durchgereicht; andere Fehler 500) → `speicherePdf` → Token → speichern (scheitert danach etwas: `entfernePdf`) → Sperren frei → Mail mit dem **gespeicherten** Dokument. „Erneut senden“ läuft über `/erinnern`: nach einem Mail-Fehler als normale K31, sonst als Erinnerung; `gesendetAn` = aktuelle Kunden-E-Mail. Neu: `ordneProjektZu(id, projektId)` + Route `POST …/auftraege/:id/projekt` (V5); hängt unter derselben `sperreAuftrag(id)` zuerst die Projektrechnungen der AB mit `projektId === ''` um (Stripe unverändert), zuletzt die AB. Stripe-Metadaten wandern beim Ersetzen **nicht** mit (nur `projektRechnungen.auftragId`).
**L4:** statische Figtree-TTFs aus `@expo-google-fonts/figtree` (OFL) unter `server/assets/fonts/`; reicht `privat` an `AB_TEXTE.termine` durch; leere Steuernummer → Zeile entfällt.
**L5:** K31-Link mit `#t=`; K31 ohne Team-BCC, Teamkopie ohne Link (Block `teamkopie`); T22-Betreff mit Präfix „[Aufträge]“; K32 mit `fruehbeginn_text`; K04/T09 Block `projekt`; `fruehbeginnFassung` bei Privatpersonen immer gespeichert, bei Unternehmen leer; Widerruf unabhängig vom Schalter, AB-Nummer optional.
**L6:** öffentliche Seite liest `#t=` (Rückfall `?t=`), sendet `credentials: 'omit'`; Abschnitt „Aufträge“ im Portal in Du-Form; Kleinunternehmer-Satz aus `texte.kleinunternehmer`; AB-Nummer auf der Widerrufsseite optional (Hinweis „falls zur Hand“); rechtlich gefärbte Sätze der Seiten stehen im Freigabe-Dokument von L7.
**L7:** Norm der Frühbeginn-Erklärung § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB; § 649 BGB (Kostenanschlag) ist die richtige Norm für den Überschreitungssatz; Datenschutz-Abschnitt als **letzter** Commit des Strangs (wird erst beim AB-Livegang gemergt); alte AGB-PDF-Fassungen bleiben liegen; Karten in WOMS → Rechtliches laut Freigabe-Dokument (Karte ohne Datei entfällt).
**WOMS:** Bildschirmfotos nur mit eigenen Skripten im Scratchpad (das Prüfwerkzeug des Redesigns unter `/root/reports/ticketsystem-design-2026-10-04/werkzeug*` wird nicht geändert); der WOMS-Push erfolgt erst, wenn das Redesign auf `test` liegt, und vorher wird `feature/auftraege` auf den dann aktuellen `origin/test` gebracht.
**Z:** Node-20-Läufe mit `node:20-alpine`; Merge mit `--no-ff` (kein Rebase der Strang-Branches); Gegenprüfung (haiku) nur für Critical/Important, Minors gehen ungeprüft als Liste an den Nutzer; die Prüfbilder des Endstands werden nach `/root/reports/auftraege-rechnungen-2026-10-06/bilder/` kopiert (der Scratchpad überlebt keinen Neustart); ein Löschlauf für alte ABs nach Ablauf der Aufbewahrung ist nicht im Umfang (Minor-Liste).
**Livegang:** Das Setup-Skript läuft **vor** dem Push (Wegwerf-Container mit dem Worktree, siehe Plan 12) oder als Ausweichweg sofort danach per `docker exec`; es legt `projektRechnungen` in jedem Fall an. Der WOMS-Push setzt das Attribut `customers.kundenart` voraus (Setup vorher).
---
## Global Constraints
- Sprache: alle Texte Deutsch; Mails, PDF und die öffentliche Bestätigungsseite in der **Sie**-Form; im angemeldeten Kundenportal (Dashboard, Abschnitt „Aufträge“) die **Du**-Form wie im übrigen Portal; WOMS wie bestehend.
- Schreibweise im Code: nie ein Unicode-Escape mit genau vier Hexziffern (Backslash, `u`, vier Ziffern) schreiben – das Schreibwerkzeug macht daraus das Zeichen selbst (z. B. ein unsichtbares geschütztes Leerzeichen); stattdessen `\u{…}` oder `String.fromCodePoint(…)`.
- Kleinunternehmer: keine Umsatzsteuer; jede AB trägt „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“
- Beträge immer ganze **Cent** (`Number.isSafeInteger`); Anzeige über `euroDe(cents)` aus `server/services/finance/format.js` (bzw. WOMS `euro`).
- Datum intern als Berliner Kalendertag `YYYY-MM-DD` (`berlinDate` aus `server/services/finance/dates.js`), Anzeige `TT.MM.JJJJ` (`formatDe`); Zeitpunkte als ISO-UTC (`toISOString()`), Appwrite liefert `+00:00` → immer mit `new Date()` lesen.
- Stundensatz Standard **100 € = 10000 Cent**; Minuten je Arbeitsblatt = `worksheetMinutes(ws)` (inkl. Unterstützung), **je Arbeitsblatt auf 15 Minuten aufgerundet**.
- **Abo-Zeit** (`worksheets.countsToPlan === true`) und Kommentare (`isComment === true`) werden nie projektbezogen berechnet.
- Grenzen (Rechnung und AB): 1–20 Positionen bzw. Zeilen (Minuszeilen zählen mit; Ausnahme: eine Schlussrechnung darf bis 40 Zeilen haben – AB-Positionen ≤ 20, Zusatzpositionen ≤ 10, Abzüge ≤ 10), Beschreibung 1–200 Zeichen, Menge ganzzahlig 1–999, Einzelpreis 1–10.000.000 Cent, Summe ≤ 99.999.999 Cent, Zahlungsziel ganzzahlig 0–60 Tage (Standard 14), Leistungszeitraum ≤ 140 Zeichen.
- AB-Nummer `AB-JJJJ-NNNN` (4 Ziffern, je Kalenderjahr ab 0001, Berliner Jahr des Sendens).
- Bestätigungs-Token: 32 Zufallsbytes base64url (43 Zeichen), gespeichert **nur** als SHA-256 (hex), gültig **60 Tage** ab dem letzten Versand.
- **Keine IP-Adressen** speichern (Ratenbegrenzung nur im Arbeitsspeicher).
- Schalter `AUFTRAEGE_ENABLED` = `off` (Vorgabe) | `pilot` (nur Kunden, deren E-Mail in `AUFTRAEGE_PILOT_EMAILS` steht; `*` = alle) | `on`. Er betrifft nur AB-Funktionen und die Rechnungsarten `anzahlung`/`schluss`; Projektkonto und die Arten `frei`, `festbetrag`, `aufwand` laufen immer.
- Kein Chrome zur Laufzeit; PDFs der AB über **pdfkit**.
- Mail-Kennungen: **K31** Auftragsbestätigung, **K32** Auftrag bestätigt (Kunde), **T22** Auftrag bestätigt (Team).
- Tests: `node --test`; nur Nachbauten aus `test/support/` (`startAppwriteMock`, `startFakeStripe`, `useTestEnv`, `mailSink`, `startTestApp`), nie echtes Appwrite, Stripe oder SMTP. Neue Appwrite-Felder nur über `server/services/auftraege/schema.js` (der Appwrite-Nachbau prüft gegen das Schema).
- **Ressourcen** (Server mit wenig Arbeitsspeicher, Memory „server-speicher“): Testläufe nur gezielt und begrenzt: `cd <worktree> && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/<eigene Dateien>`. Die volle Suite (`npm test`) läuft **nur in Z1**, und schwere Läufe (volle Suite, Chrome-PDF-Druck, `npm ci`, `vite build`) laufen hinter `flock /tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-schwer.lock`. Vorher `free -m`: unter 1500 MB „available“ warten. Nie `pkill -f` (trifft die eigene Shell), Prozesse nur per PID beenden.
- Git: jeder Strang nur in seinem Worktree und Branch; Commits klein, je Task mindestens einer, Nachricht deutsch, Schlusszeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`; **kein Push** (der Nutzer pusht), nie `--force`, nie `git add -A`.
- Sicherheit der Umgebung (Memory „feedback-destruktive-skripte“): kein `rm -rf`/`rsync --delete` außerhalb des eigenen Worktrees bzw. Scratchpads; keine Live-Daten lesen oder schreiben (kein Prod-Appwrite, kein Live-Stripe, keine echten Mails); `build/` nie anfassen.
- Bestehendes Verhalten bleibt: alle vorhandenen Tests bleiben grün; die bisherigen Rechnungsfelder (`mapInvoice`) bleiben, neue kommen dazu.
## Review Focus
1. **Gleichzeitige Aufwandsrechnungen** mit denselben Arbeitsblättern (Doppelklick, zwei Admins): die zweite muss mit 409 „Arbeitsblatt bereits abgerechnet“ scheitern. Test in L2 (zwei parallele `createProjectInvoice` mit gleichen `arbeitsblattIds` unter Sperre `sperreRechnung(kundenId)` = `['auftraege-rechnung:<kundenId>']`).
2. **Alte Token nach Erinnerung, neuer Fassung, Storno oder Widerruf**: der alte Link darf nie eine AB bestätigen; die Seite erklärt den Grund. Tests in L5 (Erinnerung → alter Token 404; ersetzt/storniert/widerrufen → 410 mit Meldung).
3. **Schlussrechnung neben stornierten oder noch nicht gesendeten Rechnungen**: Stornos zählen nicht, ein offener Entwurf zur AB blockiert mit 409. Tests in L2.
4. **Unvollständige Kundendaten beim AB-Senden** (keine Kundenart, keine Anschrift, keine E-Mail, Interessent statt Kunde): klare Meldung **vor** Nummernvergabe und PDF, keine Nummer verbraucht. Tests in L3.
5. **Umlaute, €, Minuszeichen und lange Texte im PDF** (ä ö ü ß €, „−“, 200 Zeichen ohne Leerzeichen, 20 Positionen über eine Seite): lesbar, kein Glyphenausfall, Seitenumbruch mit Kopf der Tabelle. Tests in L4 (Text aus dem PDF lesen).
---
## Verträge
### V1 Collections und Attribute (`server/services/auftraege/schema.js`, L0)
Bauart wie `server/services/zeiten/schema.js` (reine Daten, kein Import von `config.js`; Helfer `str(key,size)`, `int(key)`, `bool(key)`, `dt(key)`, `index(key, ...attrs)`; alle Attribute `required: false`).
```js
export const AUFTRAEGE_COLLECTIONS = {
auftraege: {
attributes: [
str('nummer', 20), int('jahr'), int('laufnummer'), str('status', 20),
str('kundenId', 64), str('kundeJson', 2000), str('projektId', 64), str('titel', 200),
str('zeitquelleJson', 1000), str('art', 20), str('positionenJson', 20000),
int('stundensatzCents'), int('schaetzungMinuten'), int('summeCents'),
str('zahlungsplanJson', 1000), int('zahlungszielTage'),
str('auftragVom', 10), str('auftragWeg', 20), str('startAm', 10), str('fertigBis', 10),
str('texteFassung', 20), str('agbFassung', 20), str('pdfDatei', 300), str('pdfSha256', 64),
dt('gesendetAm'), str('gesendetAn', 255), str('mailStatus', 20), str('mailFehler', 500),
str('tokenHash', 64), dt('tokenGueltigBis'),
dt('bestaetigtAm'), str('bestaetigtUeber', 10), str('bestaetigtPdfSha256', 64),
bool('fruehbeginn'), str('fruehbeginnFassung', 20),
str('ersetztId', 64), str('ersetztDurchId', 64), dt('storniertAm'), dt('widerrufenAm'),
str('erstelltVon', 255), dt('erstelltAm'), dt('geaendertAm'),
],
indexes: [index('idx_kunde', 'kundenId'), index('idx_projekt', 'projektId'), index('idx_jahr', 'jahr'),
index('idx_token', 'tokenHash'), index('idx_nummer', 'nummer')], // idx_nummer NICHT unique: Entwürfe haben nummer ''
},
// Dokument-ID = Stripe-Rechnungs-ID (muss /^in_\w{1,33}$/ erfüllen; der Stripe-Nachbau vergibt in_test_<n>)
projektRechnungen: {
attributes: [
str('invoiceId', 64), str('kundenId', 64), str('projektId', 64), str('auftragId', 64), str('art', 20),
int('betragCents'), str('arbeitsblattIdsJson', 20000), str('rechnungsnummer', 64), bool('aktiv'),
str('erstelltVon', 255), dt('erstelltAm'),
],
indexes: [index('idx_kunde', 'kundenId'), index('idx_projekt', 'projektId'), index('idx_auftrag', 'auftragId')],
},
}
export const AUFTRAEGE_ZUSATZ_ATTRIBUTE = {
customers: [str('kundenart', 20)], // '' | 'unternehmen' | 'privat'
financeSettings: [str('steuernummer', 40)],
}
```
`config.collections.auftraege` (`APPWRITE_COLLECTION_AUFTRAEGE`, Vorgabe `'auftraege'`), `config.collections.projektRechnungen` (`APPWRITE_COLLECTION_PROJEKT_RECHNUNGEN`, Vorgabe `'projektRechnungen'`).
`config.auftraege = { mode, pilotEmails, uploadDir }` mit `mode` aus `AUFTRAEGE_ENABLED` (wie `config.zeiten.mode`), `pilotEmails = commaList(process.env.AUFTRAEGE_PILOT_EMAILS, [])`, `uploadDir = process.env.AUFTRAEGE_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'auftraege')`.
### V2 Konstanten (`server/services/auftraege/konstanten.js`, L0)
```js
export const RECHNUNGS_ARTEN = ['frei', 'festbetrag', 'aufwand', 'anzahlung', 'schluss']
export const AB_RECHNUNGS_ARTEN = ['anzahlung', 'schluss'] // brauchen AB und Schalter
export const AUFTRAG_ARTEN = ['festpreis', 'aufwand']
export const AUFTRAG_STATUS = ['entwurf', 'versendet', 'bestaetigt', 'abgerechnet', 'storniert', 'ersetzt', 'widerrufen']
export const VEREINBARTE_STATUS = ['versendet', 'bestaetigt', 'abgerechnet'] // zählen als „vereinbart“
export const AUFTRAG_WEGE = { muendlich: 'mündlich', telefon: 'telefonisch', email: 'per E-Mail', vor_ort: 'vor Ort', chat: 'im Chat' }
export const ZAHLUNGSPLAN_ARTEN = ['nach_abnahme', 'anzahlung', 'monatlich', 'frei']
export const KUNDENARTEN = ['unternehmen', 'privat']
export const STUNDENSATZ_STANDARD_CENTS = 10000
export const VIERTELSTUNDE = 15
export const TOKEN_GUELTIG_TAGE = 60
export const AGB_FASSUNG = '2026-09-30' // L7 darf diese eine Zeile anpassen
export const LIMITS = {
maxPositionen: 20, maxBeschreibung: 200, maxMenge: 999, maxEinzelpreisCents: 10_000_000,
maxSummeCents: 99_999_999, maxTitel: 200, maxZahlungsplanText: 500, maxZahlungszielTage: 60,
standardZahlungszielTage: 14, standardAnzahlungProzent: 50, maxZeitquellen: 10,
maxSchaetzungMinuten: 99_960, maxLeistungszeitraum: 140,
}
// withLocks sperrt jedes Listenelement einzeln: deshalb je Sperre GENAU EIN zusammengesetzter Schlüssel
export const SPERRE_NUMMER = ['auftraege-nummer']
export const sperreRechnung = (kundenId) => [`auftraege-rechnung:${kundenId}`] // Aufwand, Anzahlung, Schluss je Kunde
export const sperreAuftrag = (id) => [`auftraege-ab:${id}`] // jede Statusänderung einer AB (L2, L3, L5)
export const SCHALTER_AUS_MELDUNG = 'Auftragsbestätigungen sind für diesen Kunden noch nicht freigeschaltet.'
```
Fehlerklasse (`server/services/auftraege/fehler.js`, L0): `export class AuftragError extends Error { constructor(status, message) }` (Eigenschaft `status`) und `export function sendeFehler(res, was, err)`: `AuftragError` und `InvoiceError` (aus `stripeFinance.js`) → `res.status(err.status).json({ error: err.message })`; sonst Protokoll ohne Meldung (wie `logFailure` in `finanzen.js`) und `500 { error: 'Interner Fehler.' }`.
Statusübergänge der AB (nur diese):
`entwurf → versendet` (L3 senden) · `versendet → bestaetigt` (L5) · `versendet|bestaetigt → abgerechnet` (L2, Schlussrechnung finalisiert) · `abgerechnet → bestaetigt|versendet` (L2, Storno der Schlussrechnung; Ziel `bestaetigt`, wenn `bestaetigtAm` gesetzt) · `versendet|bestaetigt → storniert` (L3) · `versendet|bestaetigt|abgerechnet → ersetzt` (L3, beim Senden der neuen Fassung) · `versendet|bestaetigt → widerrufen` (L5, nur Kundenart `privat`). Bestätigen ist auch bei `abgerechnet` ohne `bestaetigtAm` möglich (Status bleibt `abgerechnet`).
### V3 Datenformen
```js
// Auftrag (intern; auftragStore.docToAuftrag)
{
id, nummer /* '' im Entwurf */, jahr /* number|null */, laufnummer /* number|null */, status,
kundenId, kunde /* KundeSnapshot|null, beim Senden eingefroren */, projektId /* '' ohne Projekt */, titel,
zeitquelle /* string[] Ticket-IDs (workorders.$id) */, art /* 'festpreis'|'aufwand' */,
positionen /* AbPosition[]; aufwand: [] */, stundensatzCents /* aufwand, sonst null */,
schaetzungMinuten /* aufwand optional, sonst null */, summeCents /* festpreis, sonst null */,
zahlungsplan /* { art, prozent: number|null, text: string } */, zahlungszielTage,
auftragVom /* 'YYYY-MM-DD' */, auftragWeg /* Schlüssel aus AUFTRAG_WEGE */, startAm, fertigBis /* 'YYYY-MM-DD'|'' */,
ersetztId, ersetztDurchId, texteFassung, agbFassung, pdfDatei, pdfSha256,
gesendetAm /* ISO|null */, gesendetAn, mailStatus /* ''|'gesendet'|'fehler' */, mailFehler,
tokenHash, tokenGueltigBis /* ISO|null */,
bestaetigtAm /* ISO|null */, bestaetigtUeber /* ''|'link'|'portal' */, bestaetigtPdfSha256,
fruehbeginn /* boolean */, fruehbeginnFassung, storniertAm, widerrufenAm /* ISO|null */,
erstelltVon, erstelltAm, geaendertAm,
}
// KundeSnapshot (auftragSicht.kundeSnapshot(customerDoc))
{ name, firma, strasse, plz, ort, land, email, kundenart /* ''|'unternehmen'|'privat' */, kundennummer /* customers.code || '' */ }
// AbPosition
{ beschreibung, menge, einzelpreisCents }
// ProjektRechnung (projektRechnungenStore)
{ invoiceId, kundenId, projektId, auftragId, art /* 'festbetrag'|'aufwand'|'anzahlung'|'schluss' */, betragCents,
arbeitsblattIds /* string[] */, rechnungsnummer, aktiv /* boolean */, erstelltVon, erstelltAm }
// Arbeitsblatt (arbeitsblaetter.js; normiert)
{ id, ticketId /* worksheets.workorderId */, woid, datum /* 'YYYY-MM-DD' */, minuten /* auf 15 aufgerundet */,
text /* kundentauglich, ≤ 200 */, aboZeit /* countsToPlan === true */, kommentar /* isComment === true */ }
// Position (Rechnungszeile; positionen.js)
{ schluessel /* z. B. '2026-10-06', 'KW-2026-41', 'ab-1', 'zusatz-1', 'abzug-in_123' */, beschreibung,
menge, einzelpreisCents, betragCents /* menge × einzelpreis; Minuszeile: menge 1, beide negativ */ }
// Projektkonto (projektkonto.js)
{ projektId, name, projektTyp, tickets /* [{ id, woid }] */, zeitquelle /* string[] */,
vereinbart: { festpreisCents /* number|null */, aufwand /* { stundensatzCents, schaetzungMinuten }|null */ },
berechnetCents, bezahltCents, offenCents /* number|null */,
zeit: { minuten, aboMinuten, offeneMinuten, satzCents, wertCents, offenerWertCents } }
// AuftragTeam (auftragSicht.auftragFuerTeam): Auftrag ohne tokenHash, pdfDatei, bestaetigtPdfSha256, plus
{ pdfVorhanden /* boolean */, fruehesterBeginn /* 'YYYY-MM-DD'|null */, zahlungsplanText, aufwandText /* string|null */ }
// AuftragOeffentlich (auftragSicht.oeffentlicheAnsicht) – nur für Token- und Kundenrouten
{ nummer, datum /* Berliner Tag von gesendetAm */, titel, art,
positionen /* [{ beschreibung, menge, einzelpreisCents, summeCents }] */, summeCents, stundensatzCents,
schaetzungMinuten, aufwandText, zahlungsplanText, startAm, fertigBis, kundenart, status, bestaetigtAm,
fruehbeginn, fruehesterBeginn, agbUrl /* '/rechtliches/agb-<auftrag.agbFassung || AGB_FASSUNG>.pdf' */,
texte: { bestaetigung /* BESTAETIGUNG_TEXT */, fruehbeginn /* FRUEHBEGINN_TEXT oder null bei unternehmen */,
kleinunternehmer /* AB_TEXTE.kleinunternehmer */ } }
// Kundenrouten ergänzen id und kannBestaetigen = (status === 'versendet' || (status === 'abgerechnet' && !bestaetigtAm)) && nicht Kundenansicht
```
Regeln, die mehrere Stränge brauchen (alle in L0, `auftragSicht.js`):
- `fruehesterBeginn(auftrag)`: nur bei `kunde.kundenart === 'privat'`, `!fruehbeginn` und gesetztem `gesendetAm`: Tag nach `withdrawalDeadline(new Date(gesendetAm)).lastDay` (aus `server/services/deadlines.js`); sonst `null`.
- `zahlungsplanText(auftrag)` und `aufwandText(auftrag)`: aus den Vorlagen in `texte.js` (V7), Beträge mit `euroDe`, bei `anzahlung` Anzahlung = `Math.round(summeCents * prozent / 100)`, Rest = `summeCents − Anzahlung`.
### V4 Module und Funktionen
**L0 – Fundament**
- `schalter.js`: `auftraegeAktiv(kunde) → boolean` (`kunde`: Kunden-Dokument oder `{ email }`; `on` → true, `pilot` → E-Mail klein geschrieben in `config.auftraege.pilotEmails` oder Liste enthält `*`, sonst false).
- `auftragStore.js`: `docToAuftrag(doc)`, `auftragToDoc(auftrag)` (nur Felder aus V1, JSON-Felder serialisiert), `getAuftrag(id) → Auftrag|null`, `listAuftraege({ kundenId, projektId, jahr } = {}) → Auftrag[]` (neueste zuerst), `createAuftrag(daten) → Auftrag` (setzt `erstelltAm`, `geaendertAm`, Status `entwurf`), `updateAuftrag(id, patch) → Auftrag` (setzt `geaendertAm`), `deleteAuftrag(id)`, `findAuftragByTokenHash(hash) → Auftrag|null`, `findAuftragByNummer(nummer) → Auftrag|null`.
- `projektRechnungenStore.js`: `getProjektRechnung(invoiceId) → ProjektRechnung|null`, `listProjektRechnungen({ kundenId, projektId, auftragId, nurAktiv = true } = {})`, `createProjektRechnung(invoiceId, daten) → ProjektRechnung`, `updateProjektRechnung(invoiceId, patch) → ProjektRechnung`, `umhaengen(auftragIdAlt, auftragIdNeu) → number` (Anzahl umgehängter Dokumente).
- `auftragSicht.js`: `kundeSnapshot(customerDoc)`, `fruehesterBeginn(auftrag)`, `zahlungsplanText(auftrag)`, `aufwandText(auftrag)`, `auftragFuerTeam(auftrag)`, `oeffentlicheAnsicht(auftrag)`.
- `arbeitsblaetter.js`: `ladeArbeitsblaetter(ticketIds) → Arbeitsblatt[]` (sortiert nach `datum`, dann `id`; Datum wie `parseSheetDate` in `hourLedger.js`: `TT.MM.JJJJ`, sonst ISO, sonst `$createdAt`, als Berliner Tag; Text gefiltert wie `customerWorkEntries` in `customerProjectView.js`), `ladeArbeitsblaetterNachIds(ids) → Arbeitsblatt[]`, `abrechenbar(blatt) → boolean` (`!aboZeit && !kommentar && minuten > 0`), `abgerechneteArbeitsblaetter(kundenId) → Map<worksheetId, { invoiceId, rechnungsnummer }>` (aus aktiven `projektRechnungen`), `offeneArbeitsblaetter(ticketIds, kundenId) → Arbeitsblatt[]` (abrechenbar und nicht abgerechnet), `zeitquelleFuerProjekt(projekt, auftraege) → string[]` (Vereinigung der `zeitquelle` aller ABs mit Status aus `VEREINBARTE_STATUS`, sonst `[projekt.ticketId]` ohne Leereinträge).
- `ablage.js`: `speicherePdf(kundenId, auftragId, buffer) → { pdfDatei: '<kundenId>/<auftragId>.pdf', pdfSha256 }` (unter `config.auftraege.uploadDir`, Verzeichnis 0700, Datei 0600, überschreibt nie: vorhandene Datei → Fehler), `lesePdf(pdfDatei) → Buffer` (nur `^[A-Za-z0-9_-]{1,64}/[A-Za-z0-9_-]{1,64}\.pdf$`, sonst `AuftragError(404, 'PDF nicht gefunden.')`).
- `settings.js` (Finanzen): Schlüssel `steuernummer` (Vorgabe `''`, getrimmt, höchstens 40 Zeichen, erlaubt Buchstaben, Ziffern, Leerzeichen, `/`, `-` und `.` (auch eine USt-IdNr. „DE…“); `''` löscht; sonst `SettingsError(400, 'Steuernummer ungültig.')`).
**L1 – Projektkonto**: `projektkonto.js`: `projektkontenFuerKunde(kundenId) → { auftraegeAktiv, kunde: { id, name, kundenart }, konten: Projektkonto[], ohneProjekt: { berechnetCents, bezahltCents, anzahl }, stripeOk }`; `projektkontoDetail(projektId) → { konto: Projektkonto, auftraege: AuftragTeam[], rechnungen: RechnungKurz[], offeneArbeitsblaetter: Arbeitsblatt[], stripeOk }` mit `RechnungKurz = { id, number, status, createdDate, totalCents, paidCents, art, auftragId }`. Rechenregeln: Spec Abschnitt 4; `berechnetCents`/`bezahltCents` nur aus finalisierten Rechnungen (Status `offen`, `ueberfaellig`, `bezahlt`, `uneinbringlich` laut `listInvoices()`) aktiver `projektRechnungen`; `offenCents = festpreisCents === null ? null : max(0, festpreisCents − Σ total der finalisierten Rechnungen mit auftragId einer Festpreis-AB)`. Scheitert Stripe: Beträge 0, `stripeOk: false`.
**L2 – Rechnungen**: `positionen.js` (rein): `festePositionen(items) → Position[]` (Eingabe `{ description, quantity, unitAmountCents }` wie heute), `aufwandPositionen(blaetter, { satzCents, zeilentexte = {}, maxZeilen = 20 }) → { positionen, minuten, von, bis }` (eine Zeile je Tag, Text „06.10.2026: <Texte des Tages, mit '; ' verbunden, gekürzt> – 2,25 Std. à 100,00 €“, ohne Text „Arbeitszeit“; zu viele Zeilen → älteste Tage zu ISO-Kalenderwochen „KW 41/2026: …“, Schlüssel `KW-2026-41`; `zeilentexte[schluessel]` ersetzt den Text vor dem Gedankenstrich), `anzahlungPosition(auftrag, { prozent, betragCents, bezeichnung = 'Anzahlung' }, bereitsBerechnetCents) → Position`, `schlussPositionen(auftrag, fruehere, zusatz) → Position[]` (`fruehere: [{ invoiceId, rechnungsnummer, datum, betragCents, art }]`; Minuszeile „abzüglich Anzahlung, Rechnung <Nr> vom <TT.MM.JJJJ>“ bzw. „abzüglich Teilrechnung …“), `stundenText(minuten) → '2,25'`, `leistungszeitraumText(von, bis) → '06.10.2026' | '01.10.–20.10.2026' | '28.12.2026–05.01.2027'`.
`rechnungsbau.js`: `baueRechnung(eingabe) → RechnungsEntwurf` mit `eingabe = { customerId, art = 'frei', projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum }` und `RechnungsEntwurf = { kunde, art, projekt /* { id, name, ticketId }|null */, auftrag /* Auftrag|null */, positionen, summeCents, leistungszeitraum, customFields /* [{ name, value }] ≤ 4 */, metadata, projektRechnung /* Daten für createProjektRechnung ohne invoiceId|null */ }`. Zusatzfelder: `Projekt` (Name, auf 140 gekürzt; nicht bei `frei`), `Auftrag` („AB-2026-0002 vom 06.10.2026“, nur mit AB), `Leistungszeitraum` (wenn nicht leer). Metadaten zusätzlich zu den heutigen: `projektId`, `projektName`, `auftragId`, `auftragNummer`, `art`.
`stripeFinance.js`: `createProjectInvoice({ customerId, art = 'frei', projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden, idempotencyKey, createdBy })` (Positionen über `baueRechnung`; positive Zeile → `quantity` + `unit_amount_decimal`, Minuszeile → `amount`; `custom_fields`; `rendering: { pdf: { page_size: 'a4' } }` für **alle** Rechnungen; `createProjektRechnung` nach dem Entwurf bei `art !== 'frei'`; Aufwand, Anzahlung und Schluss unter `withLocks(sperreRechnung(customerId))`), neue Funktion `rechnungsVorschau(eingabe) → { positionen, summeCents, leistungszeitraum, customFields }` (ohne Stripe-Schreibzugriff), `mapInvoice` liefert zusätzlich `projektId`, `projektName`, `art` (`metadata.art`, sonst `'abo'` bei Abo-Rechnungen bzw. `'frei'`), `auftragId`, `auftragNummer`; Finalisieren (`senden` oder `sendInvoiceNow`) setzt `projektRechnungen.rechnungsnummer` und bei `schluss` die AB auf `abgerechnet`; `voidInvoice`/`deleteDraft` setzen `aktiv: false` und nehmen `abgerechnet` zurück.
**L3 – AB-Kern**: `nummern.js`: `formatNummer(jahr, laufnummer) → 'AB-2026-0001'`, `vergibNummer(jetzt = new Date()) → { nummer, jahr, laufnummer }` (größte `laufnummer` des Berliner Jahres + 1; Aufrufer hält `SPERRE_NUMMER`). `auftraege.js`: `pruefeEingabe(eingabe, { teilweise = false }) → bereinigte Felder` (AuftragError 400 mit Feldmeldung), `legeEntwurfAn(eingabe, { von })`, `aendereEntwurf(id, eingabe)`, `loescheEntwurf(id)`, `vorschauPdf(id, { deps }) → Buffer`, `sendeAuftrag(id, { von, deps }) → { auftrag, mailFehler: boolean }`, `erinnereAuftrag(id, { deps }) → { auftrag, mailFehler }`, `neueFassung(id, { von }) → Auftrag` (neuer Entwurf mit `ersetztId`), `storniereAuftrag(id) → Auftrag`. `deps = { erzeugeAuftragPdf, neuerToken, sendeAuftragMail }`; Vorgabe per **dynamischem Import** von `./pdf.js` (L4), `./token.js` (L5), `./mails.js` (L5) beim Aufruf, damit L3 ohne L4/L5 testbar ist (Tests übergeben Attrappen). Reihenfolge beim Senden: Eingabe, Kunde (fester Kunde, E-Mail, Anschrift, Kundenart) prüfen → Sperre `SPERRE_NUMMER` → Nummer → PDF erzeugen → `speicherePdf` → Token → Dokument speichern (Status `versendet`) → Sperre frei → Mail → `mailStatus`. Hat die AB `ersetztId`: alte AB `ersetzt` (+ `ersetztDurchId`), `umhaengen(alt, neu)`.
**L4 – AB-PDF**: `pdf.js`: `auftragPdfDaten(auftrag, { settings, vorschau = false, jetzt, ersetzt }) → AuftragPdfDaten`, `erzeugeAuftragPdf(auftrag, { settings, vorschau = false, jetzt, ersetzt }) → Promise<Buffer>` (`ersetzt = { nummer, datum }` optional, sonst über `getAuftrag(auftrag.ersetztId)`; `AB_TEXTE.termine` bekommt `privat`). `auftrag.kunde` ist immer gesetzt (L3 übergibt bei der Vorschau `kundeSnapshot(liveKunde)` und `nummer: 'Entwurf'`). `settings = { steuernummer, bankDetailsText }`. Inhalt laut Spec Abschnitt 2 „Inhalt des PDFs“, Texte aus `texte.js`. Schrift: statische Figtree-Schnitte (Regular, SemiBold) als Dateien unter `server/assets/fonts/` (OFL, Lizenzdatei dazu); Logo aus `public/` (vorhandene Datei, die die Mailvorlagen nutzen).
**L5 – Bestätigung und Mails**: `token.js`: `neuerToken(jetzt = new Date()) → { token, hash, gueltigBis /* Date */ }`, `hashToken(token) → string|null` (null bei falschem Format). `bestaetigung.js`: `bestaetigeAuftrag(auftrag, { ueber, fruehbeginn, jetzt = new Date() }) → Auftrag` (Regeln V2; idempotent; `fruehbeginn` nur bei `privat`; schickt danach `sendeBestaetigtMails`, Fehler nur ins Protokoll). `mails.js`: `sendeAuftragMail(auftrag, { pdf, token, erinnerung = false }) → Promise<void>` (wirft bei Fehler), `sendeBestaetigtMails(auftrag) → Promise<void>`. Widerruf in `server/routes/abo.js`: `order = 'projekt'` mit Feld `abNummer`.
**L6 – Kundenportal**: nur Oberfläche und `customerInvoices.js` (Feld `projekt: raw.metadata?.projektName || ''`).
**L7 – Rechtstexte**: Wortlaute in `texte.js`, neue Rechtstext-Dateien, Datenschutz, Druckskripte (Spec Abschnitt 8) und das Freigabe-Dokument `/root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md`.
### V5 HTTP-Endpunkte
Antwortformat bei Fehlern immer `{ error: '<Meldung>' }`. Admin-Routen: Router mit `requireAppwriteStaff` (wie `finanzen.js`); Team-Routen: unter `/api/team` (dort gilt `requireStaffJwt`); Kundenrouten: `requireSession` und eigener Kunde; Token-Routen: ohne Anmeldung, Ratenbegrenzung im Arbeitsspeicher je IP (GET 120/h, POST 10/h → 429 `{ error: 'Zu viele Anfragen. Bitte später erneut versuchen.' }`).
| Strang | Methode, Pfad | Eingabe | Antwort (200) | Fehler |
|---|---|---|---|---|
| L1 | GET `/api/admin/finanzen/projektkonten?kunde=<id>` | – | `{ auftraegeAktiv, kunde, konten, ohneProjekt, stripeOk }` | 400 ohne Kunde, 404 Kunde unbekannt |
| L1 | GET `/api/admin/finanzen/projektkonten/:projektId` | – | `{ konto, auftraege, rechnungen, offeneArbeitsblaetter, stripeOk }` | 404 |
| L1 | GET `/api/team/arbeitsblaetter/abgerechnet?ticketId=<id>` | – | `{ abgerechnet: { <worksheetId>: '<Rechnungsnummer>' \| 'Entwurf' } }` (unbekanntes Ticket: `{ abgerechnet: {} }`) | 400 ohne ticketId |
| L2 | POST `/api/admin/finanzen/rechnungen` | `{ customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung: { prozent, betragCents, bezeichnung }, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden, idempotencyKey }` | `{ rechnung }` (wie heute, `mapInvoice` erweitert) | 400 Eingabe, 404, 409 (Arbeitsblatt bereits abgerechnet, offener Entwurf zur AB, Nichts mehr offen, Adresse), 502 |
| L2 | POST `/api/admin/finanzen/rechnungen/vorschau` | wie oben ohne `senden`, `idempotencyKey`, `daysUntilDue`, `memo` | `{ positionen, summeCents, leistungszeitraum, customFields }` | wie oben |
| L2 | GET `/api/admin/finanzen/rechnungen` | wie heute | Einträge zusätzlich mit `projektId`, `projektName`, `art`, `auftragId`, `auftragNummer` | – |
| L3 | GET `/api/admin/finanzen/auftraege?kunde=<id>` | – | `{ auftraege: AuftragTeam[] }` | 400, 404 Schalter aus |
| L3 | POST `/api/admin/finanzen/auftraege` | `{ kundenId, projektId, titel, kundenart, art, positionen, stundensatzCents, schaetzungMinuten, zahlungsplan, zahlungszielTage, auftragVom, auftragWeg, startAm, fertigBis, zeitquelle }` (`kundenart` setzt `customers.kundenart`, wenn dort leer) | `{ auftrag: AuftragTeam }` | 400, 404 |
| L3 | GET / PATCH / DELETE `/api/admin/finanzen/auftraege/:id` | PATCH wie POST (teilweise) | `{ auftrag }` bzw. `{ ok: true }` | 404, 409 kein Entwurf |
| L3 | GET `/api/admin/finanzen/auftraege/:id/pdf` | – | `application/pdf` (gesendet: gespeicherte Datei; Entwurf: Vorschau mit „Entwurf“) | 404, 409 Entwurf unvollständig |
| L3 | POST `/api/admin/finanzen/auftraege/:id/projekt` | `{ projektId }` | `{ auftrag }` (setzt projektId und, wenn leer, zeitquelle = [Ticket des Projekts]; PDF unverändert) | 400, 404, 409 (hat schon ein Projekt; Status storniert/ersetzt/widerrufen) |
| L3 | POST `/api/admin/finanzen/auftraege/:id/senden` | – | `{ auftrag, mailFehler }` | 400 Kundenart/E-Mail/Interessent, 409 Adresse, kein Entwurf, Entwurf unvollständig; 500 PDF |
| L3 | POST `…/auftraege/:id/erinnern` | – | `{ auftrag, mailFehler }` | 409 nicht `versendet` |
| L3 | POST `…/auftraege/:id/neue-fassung` | – | `{ auftrag }` (neuer Entwurf) | 409 |
| L3 | POST `…/auftraege/:id/stornieren` | – | `{ auftrag }` | 409 |
| L5 | GET `/api/auftrag/:token` | – | `{ auftrag: AuftragOeffentlich }` | 404 `'Dieser Link ist ungültig oder abgelaufen.'`; 410 `'Diese Auftragsbestätigung wurde ersetzt.'` / `'… wurde storniert.'` / `'… wurde widerrufen.'`; 429 |
| L5 | GET `/api/auftrag/:token/pdf` | – | `application/pdf` | wie oben |
| L5 | POST `/api/auftrag/:token/bestaetigen` | `{ bestaetigt: true, fruehbeginn: boolean }` | `{ auftrag: AuftragOeffentlich }` | 400 „Bitte bestätigen Sie den Auftrag mit dem Häkchen.“, 404/410, 429 |
| L5 | GET `/api/auftraege` (Kunde) | – | `{ aktiv, auftraege: (AuftragOeffentlich & { id, kannBestaetigen })[] }` (keine Entwürfe) | 401 |
| L5 | GET `/api/auftraege/:id/pdf` (Kunde) | – | `application/pdf` | 404 fremd/unbekannt |
| L5 | POST `/api/auftraege/:id/bestaetigen` (Kunde) | `{ bestaetigt: true, fruehbeginn }` | `{ auftrag }` | 400 (Text wie Token-Route), 404, 410 (Meldungen wie Token-Route); in der Kundenansicht sperrt der Wächter (kein Eintrag in `KUNDENANSICHT_ALLOWED`) |
| L5 | POST `/api/abo/public/withdraw` | zusätzlich `order: 'projekt'`, `abNummer` (optional; ohne/ungültig nur Mails mit Vermerk „ungeprüft“) | wie heute | wie heute; gilt unabhängig vom Schalter |
| L0 | GET/PATCH `/api/admin/finanzen/einstellungen` | zusätzlich `steuernummer` | wie heute | 400 |
Ist der Schalter für den Kunden aus: Admin-AB-Routen 404 mit `SCHALTER_AUS_MELDUNG`; Rechnungsarten `anzahlung`/`schluss` 400 mit derselben Meldung; Token- und Kundenrouten antworten wie bei unbekannter AB (404) bzw. `GET /api/auftraege` mit `{ aktiv: false, auftraege: [] }`.
Einbindung (L0): in `server/index.js` **vor** `app.use('/api/admin/finanzen', adminFinanzenRoutes)`: `app.use('/api/admin/finanzen/projektkonten', adminProjektkontenRoutes)`, `app.use('/api/admin/finanzen/auftraege', adminAuftraegeRoutes)`; außerdem `app.use('/api/auftrag', publicAuftragRoutes)` und `app.use('/api/auftraege', kundeAuftraegeRoutes)`; in `server/routes/team/index.js` nach `requireStaffJwt`: `router.use('/arbeitsblaetter', arbeitsblaetterTeamRoutes)`.
### V6 Mails (L5)
Vorlagen in `server/mail-templates/` mit Kopfkommentar wie die vorhandenen (`renderMail`):
- `K31 Auftragsbestaetigung.html` – Betreff „Ihre Auftragsbestätigung {{nummer}} – {{titel}}“; Platzhalter `name, nummer, titel, datum, betrag, link, gueltig_bis`; Blöcke `erinnerung, privat, festpreis, aufwand, kundenmail, teamkopie` (`kundenmail` = Gegenblock zu `teamkopie`, weil die Vorlagen nur positive Blöcke kennen; die Teamkopie geht als eine Mail ohne Anhänge über `sendAdminNotice` nach der Kundenmail). Anhänge: AB-PDF (`<nummer>.pdf`) und AGB-PDF (`public/rechtliches/agb-<AGB_FASSUNG>.pdf`, Dateiname `WEBklar-AGB-<AGB_FASSUNG>.pdf`). **Kein** BCC an das Team (`bccTeam: false`), weil die Mail den Bestätigungslink trägt; das Team bekommt eine Kopie mit Block `teamkopie` an `CHAT_NOTIFY_EMAILS` (statt Knopf: „Kopie für das Team – der Bestätigungslink steht nur in der Mail an den Kunden.“), auch bei Erinnerungen. Anhang-Fassung: `auftrag.agbFassung` (Rückfall `AGB_FASSUNG`); fehlt die Datei, wirft der Versand (→ `mailStatus: 'fehler'`).
- `K32 Auftrag bestaetigt.html` – Betreff „Ihre Bestätigung zu {{nummer}} ist eingegangen“; Platzhalter `name, nummer, titel, zeitpunkt, fruehester_beginn, fruehbeginn_text`; Blöcke `fruehbeginn` (gibt `FRUEHBEGINN_TEXT` wörtlich wieder; kein Satz „Ihr Widerrufsrecht bleibt unberührt“), `ohne_fruehbeginn`.
- `T22 Auftrag bestaetigt.html` – Betreff „[Aufträge] Auftrag {{nummer}} bestätigt: {{kunde}}“; Platzhalter `kunde, nummer, titel, zeitpunkt, ueber, woms_link, fruehester_beginn`; Blöcke `privat_fruehbeginn, privat_ohne_fruehbeginn`. `woms_link = <womsBaseUrl>/customers/<kundenId>?tab=invoices`.
- Link der K31: `<config.publicBaseUrl>/auftrag.html#t=<token>` (Fragment: landet nicht in Server- und Proxy-Protokollen; die Seite liest `#t=` und als Rückfall `?t=`).
- `K04 Eingang Widerruf.html` und `T09 Widerruf eingegangen.html` bekommen je einen Block `projekt` (Vertragsart „Projektauftrag (Auftragsbestätigung <Nr>)“; bei Unternehmen der Hinweis „kein gesetzliches Widerrufsrecht“ im K04-Block).
### V7 Texte (`server/services/auftraege/texte.js`; L0 legt an, L7 formuliert)
Exportnamen und Formen (verbindlich):
```js
export const TEXTE_FASSUNG = '2026-10-06'
export const ABSENDER = { name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler',
telefon: '0176 23726355', email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein' }
export const AB_TEXTE = {
titel: 'Auftragsbestätigung',
einleitung: ({ datum, weg }) => string, // „Vielen Dank für Ihren Auftrag vom 05.10.2026 (telefonisch). Wir bestätigen ihn wie folgt:“
aufwand: ({ satz, schaetzung /* '10' Stunden oder null */ }) => string,
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
zahlungsplan: {
nach_abnahme: ({ ziel }) => string, anzahlung: ({ prozent, anzahlung, rest, ziel }) => string,
monatlich: ({ ziel }) => string, frei: ({ text, ziel }) => string,
},
termine: ({ start /* 'TT.MM.JJJJ'|null */, fertig /* 'TT.MM.JJJJ'|null */, privat /* boolean */ }) => string|null, // privat: „Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.“
mitwirkung: string, // Verweis auf AGB Nr. 7 und 8
agb: ({ fassung /* 'TT.MM.JJJJ' */ }) => string, // „Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom …, die dieser Bestätigung beiliegen.“
ersetzt: ({ nummer, datum }) => string, // „Diese Auftragsbestätigung ersetzt AB-… vom ….“
gruss: string,
}
export const BESTAETIGUNG_TEXT = 'Ich bestätige den Auftrag zu diesen Bedingungen.'
export const FRUEHBEGINN_FASSUNG = '2026-10-06'
export const FRUEHBEGINN_TEXT = string
export const WIDERRUFSBELEHRUNG_PROJEKT = { titel: 'Widerrufsbelehrung', abschnitte: [{ ueberschrift: string, absaetze: string[] }] }
export const WIDERRUFSFORMULAR = { titel: 'Muster-Widerrufsformular', einleitung: string, zeilen: string[] }
```
### V8 WOMS-Schnittstellen (W0/W1/W2)
- `src/lib/financeApi.js` (W0 ergänzt): `getProjektkonten(kundeId)`, `getProjektkonto(projektId)`, `previewInvoice(body)`, `createInvoice(body)` (besteht, Körper erweitert), `listAuftraege(kundeId)`, `getAuftrag(id)`, `createAuftrag(body)`, `patchAuftrag(id, body)`, `deleteAuftrag(id)`, `sendAuftrag(id)`, `erinnereAuftrag(id)`, `neueFassungAuftrag(id)`, `storniereAuftrag(id)`, `getAuftragPdf(id) → Blob`.
- `src/lib/teamApi.js` (W0 ergänzt): `getAbgerechnet(ticketId) → { abgerechnet }`.
- `src/components/auftraege/AuftraegeUndRechnungen.jsx` (W0): Inhalt des Kundenreiters `invoices` (Beschriftung „Aufträge und Rechnungen“), Props `{ customer, onShowOverview }`; lädt `getProjektkonten` und (wenn `auftraegeAktiv`) `listAuftraege`; zeigt `ProjektkontoListe`, `AuftragListe` (nur mit Schalter), Rechnungsliste (bestehende `InvoiceTable`) und öffnet `RechnungDialog` bzw. `AuftragDialog`.
- `ProjektkontoListe.jsx` (W1): Props `{ konten, ohneProjekt, auftraegeAktiv, stripeOk, onRechnung(projektId|null), onAuftrag(projektId|null) }`; die Kindkomponenten rendern ihre `Card` selbst.
- `RechnungDialog.jsx` (W1): Props `{ open, onClose, onCreated(rechnung, info /* unverändert weiterreichen: senden, notice, tone, finalizedFilter */), kunde /* Kundendokument oder null = Kundenwahl */, projektId /* undefined = keine Vorauswahl, null = „Ohne Projekt“ */, auftraegeAktiv /* nur Startwert; danach gilt getProjektkonten */, onShowOverview /* optional */ }`; lädt `getProjektkonten` selbst; sitzt in `InvoiceTable.jsx` (dort ersetzt W1 das Modal) – `FinancePage.jsx` bleibt unverändert; ersetzt `InvoiceCreateModal` überall (auch `FinancePage`).
- `AuftragListe.jsx` (W2): Props `{ auftraege, konten, onChanged(), onBearbeiten(auftrag), onShowOverview }` (W0 reicht `konten` und `onShowOverview` durch); Aktion „Projekt zuordnen“ für ABs ohne Projekt (`ordneAuftragProjektZu(id, projektId)` in financeApi.js).
- `AuftragDialog.jsx` (W2): Props `{ open, onClose, onSaved(auftrag), kunde, projektId /* oder null */, auftrag /* Entwurf zum Bearbeiten oder null */, konten /* Projektkonten für die Projektwahl */, onShowOverview }`; Hinweise (nicht blockierend): fehlende Steuernummer im Schritt 4; bei Weg „vor Ort“ und Kundenart privat „zusätzlich ausdrucken und übergeben“ (§ 312f Abs. 1 BGB). PDF öffnen: `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`.
- Reine Ansichtslogik mit Tests: `src/lib/projektkontoView.js` und `src/lib/rechnungDialogView.js` (W1), `src/lib/auftragView.js` (W2).
- Je Strang eigene CSS-Datei: `src/components/auftraege/projektkonto.css`, `rechnung-dialog.css` (W1), `auftrag.css` (W2), Gestaltung nach den Regeln des Redesigns (Komponenten aus `src/components/ui`, `Dialog`, `financeUi.js`).
- Weitere Änderungen: W1 `InvoiceTable.jsx` (Spalten Projekt, Art; Modal → `RechnungDialog`; „weitere Tickets“ liest W1 direkt aus Appwrite und schickt `arbeitsblattIds`), `WorksheetList.jsx` (Abzeichen), `WorksheetTimesModal.jsx` (Warnung); W2 `CustomerDetailPage.jsx` (Feld Kundenart in der Übersicht), `SettingsCard.jsx` (Steuernummer); W0 `src/lib/customerTabs.js` (Beschriftung).
---
## Integration, Kontrolle, Übergabe
Plan-Datei `2026-10-06-auftraege-12-integration-kontrolle.md`:
- **Z1 Portal**: Merge in der Reihenfolge L1, L2, L3, L4, L7 (ohne seinen letzten Commit, den Datenschutz-Abschnitt: `git merge --no-ff auftraege/l7-rechtstexte~1`; der Datenschutz-Commit kommt erst beim AB-Livegang nach der Freigabe), L5, L6 nach `feature/auftraege` (`git merge --no-ff`), nach jedem Merge die Tests des Strangs; dann Durchlauf-Test `test/auftraegeAblauf.test.js` über `startTestApp` (Festbetrag → Projektkonto; Aufwand → abgerechnet → Storno → frei; AB → Senden → Token-Bestätigung → Anzahlung → Schlussrechnung mit Minuszeile → `abgerechnet` → Storno → zurück; Privatperson mit/ohne Frühbeginn; Kundenansicht gesperrt; Schalter aus → 404); volle Suite unter `flock`; Node-20-Lauf der `test/auftraege*.test.js` (`docker run --rm --network host -m 1200m -v <worktree>:<worktree>:ro node:20-alpine`).
- **Z2 WOMS**: Merge W1, W2; `npm test`; `vite build`; Bildschirmfotos (Desktop 1440 px, Handy 390 px) aller neuen Ansichten mit nachgebildeten Antworten.
- **Z3 Kontrolle**: fünf Prüfer parallel (sonnet) – Geld und Rechenlogik; Sicherheit und Datenschutz (Token, öffentliche Routen, Rechte, Pfade, IDOR, Kundenansicht); Recht und Texte (Spec, § 34a UStDV, § 312f BGB, Widerruf, Frühbeginn, Datenschutz); Oberfläche (Bildschirmfotos); Spec-Abdeckung (jede Zeile der Spec → Code/Test). Jeder Befund wird von einem zweiten Agenten (haiku) gegengeprüft; nur bestätigte Critical/Important werden korrigiert (sonnet), danach betroffene Tests und Z1-Durchlauf erneut.
- **Übergabe**: `/root/reports/auftraege-rechnungen-2026-10-06/uebergabe.md` mit Push-Befehlen (Portal `feature/auftraege:main`, WOMS erst nach dem Redesign), Setup-Befehl `docker exec -w /app webklar-kundenbereich-server node scripts/auftraege-setup.mjs`, Compose-Zeilen für `AUFTRAEGE_ENABLED`/`AUFTRAEGE_PILOT_EMAILS` (Vorgabe aus), Steuernummer (Stripe-Dashboard + WOMS-Einstellung), Rechtsrunde (Freigabe-Dokument), Live-Prüfungen.
## Livegang (Kurzfassung, Details in der Übergabe)
1. Nutzer: Steuernummer im Stripe-Dashboard hinterlegen.
2. Nutzer: Portal `feature/auftraege` → `main` pushen; sofort danach Setup-Skript; Container-Log prüfen. Rechnungsteil (Projektkonto, Festbetrag, Aufwand, A4, Zusatzfelder) ist live, AB-Teil aus.
3. Nutzer: WOMS pushen, sobald das Redesign auf `test` ist.
4. Rechtsrunde: Nutzer gibt die Texte frei; Claude lädt sie in WOMS → Rechtliches hoch (kurzlebiger Schlüssel).
5. `AUFTRAEGE_ENABLED=pilot` mit Testkunde, Probe-AB an den Testkunden, dann `on`.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,973 @@
# WOMS-Basis – Umsetzungsplan (Strang W0)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Das WOMS bekommt die gemeinsame Grundlage für „Aufträge und Rechnungen“: alle neuen Portal-Aufrufe (V8) im API-Client, den Kundenreiter „Aufträge und Rechnungen“ mit dem Container `AuftraegeUndRechnungen` (Ladezustände, Dialogzustand, bestehende Rechnungsliste, Adresswarnung) und vier gültige Platzhalter, die W1 und W2 jeweils durch ihre echte Komponente ersetzen.
**Architecture:** Der API-Client bleibt dünn: Pfade und Methoden stehen im reinen Modul `src/lib/auftraegeEndpoints.js` (unter Node mit Fetch-Attrappe testbar, weil `financeApi.js` Appwrite lädt), und `financeRequest.js` lernt Blob-Antworten (`as: 'blob'`) für das PDF. Die Ansichtslogik des Reiters (Ladezustand, frische Kundenart, Meldungen) steht getestet in `src/lib/auftraegeAnsicht.js`; der Container verdrahtet sie mit `useFinanceResource`, den vier Platzhaltern und der bestehenden `InvoiceTable` und ersetzt `CustomerInvoices` im Reiter. Die Bausteine des weißen Designs werden nur benutzt, nie verändert.
**Tech Stack:** React 18, Vite 5, react-router-dom 6, reines CSS (keine neue Datei), Tests mit `node --test` und dem esbuild-Lader `tests/unit/load.mjs` (`node:test`, `assert/strict`), Appwrite Web SDK 13 (nur in `financeApi.js` und `teamApi.js`, nie in getesteten Modulen), Node 24 auf dem Host.
**Spec:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/specs/2026-10-06-auftraege-und-rechnungen-design.md` (Abschnitt 1 „Oberfläche in WOMS“, dazu 4 „Projektkonto“ und 6 „Schnittstellen“).
**Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-auftraege/docs/superpowers/plans/2026-10-06-auftraege-00-uebersicht.md` (verbindlich: V5 Endpunkte, V8 WOMS-Schnittstellen und der Abschnitt „Nachträge nach der Planrunde“; dazu V3 Datenformen). Die „Vertragsfragen“ am Ende dieser Datei sind dort entschieden; weicht diese Datei von der Übersicht ab, gilt die Übersicht.
Jeder Implementierer liest die Spec (Abschnitt 1), die Übersicht (V5, V8) und diese Datei.
---
## Global Constraints
Alle Punkte der Übersicht gelten (Sprache, Git ohne Push und ohne `git add -A`, Ressourcen und `flock`, Sicherheit der Umgebung, `build/` nie anfassen, vorhandene Tests bleiben grün). Für W0 kommt hinzu:
- **Startbedingung (hart):** Der Strang beginnt erst, wenn die Redesign-Pakete P4 (Kunden), P6 (Kunden-Unterseiten) und P7 (Finanzen) in `feature/design-weiss` zusammengeführt und als `design-p4`, `design-p6`, `design-p7` getaggt sind. Task 1 prüft das. Fehlt etwas, gilt **STOPP**: nichts anlegen, dem Orchestrator melden (er fragt den Nutzer: warten oder auf `test` bauen und eine Mitnahme-Notiz in `/root/reports/ticketsystem-design-2026-10-04/strang/mitnehmen.md` hinterlegen).
- **Anker statt Zeilennummern:** Dieser Plan nennt keine Zeilennummern. Maßgeblich sind Namen und Ankertexte im zusammengeführten Stand; Task 1 Step 5 prüft sie gegen den Stand beim Start.
- **Stand von `test`:** `origin/test` ist dem Redesign voraus (Öffnungszeiten: `zeitenView.js`, zusätzliche Zeilen in `financeRequest.js` und `teamApi.js`). Änderungen an diesen Dateien hängen sich an Ankertexte an und lassen fremde Zeilen unberührt.
- **Dateien:** W0 ändert nur die Dateien der Tabelle „Dateien“. Alle anderen Redesign-Dateien (`InvoiceTable.jsx`, `AddressWarning.jsx`, `InvoiceCreateModal.jsx`, `FinancePage.jsx`, `global.css`, …) bleiben unberührt; sie gehören W1 und W2.
- **Regeln des Redesigns für neue Dateien:** nur Bausteine aus `src/components/ui` und vorhandene Klassen (`msg`, `loading`, `link`, `fin-notice`, `fin-load-error`); keine Icon-Importe (`react-icons`, `@tabler`), kein `style={{…}}`, keine Farbliterale, keine KOMPAT-Klassen (`ui-btn`, `form-control`, `form-group`, `form-label`, `ui-card`, `badge-…`, `overlay-content`, `overlay-close`, `spinner`, `kpi`), keine neue CSS-Datei. P10 räumt KOMPAT und die Icon-Pakete weg; Neues darf davon nichts brauchen. Kommentare und Texte deutsch wie in den Nachbardateien.
- **Tests:** `node:test`, `assert/strict`, `loadModule` aus `tests/unit/load.mjs`. `financeApi.js`, `teamApi.js` und alle Komponenten laden Appwrite und sind unter Node nicht ladbar; ihre Logik steht in reinen `src/lib`-Modulen. Neue Tests liegen in neuen Dateien (Ausnahme: `customerTabs.test.mjs`).
- **Befehle mit Speichergrenzen** (Server mit wenig Arbeitsspeicher). `WT` ist `/home/webklar/apps/ticket.webklar.com/dev-auftraege`, `L` ist `/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad`. Jede Shell-Zeile steht für sich (Umgebungsvariablen bleiben nicht erhalten), deshalb stehen die Pfade ausgeschrieben in den Steps.
- Einzeltest: `cd $WT && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/<datei>.test.mjs`
- Volle Suite, `npm ci` und Build laufen **nur** hinter `flock $L/auftraege-schwer.lock`, vorher `free -m` (unter 1500 MB „available“: warten), und unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0` (Arbeitsweise des Redesigns). Läuft das Redesign noch schwere Läufe, zusätzlich dessen Sperre `/root/reports/ticketsystem-design-2026-10-04/sperren/schwer.lock` (erst unsere, dann deren).
- Build: `node node_modules/vite/bin/vite.js build --outDir $L/auftraege-w0-dist --emptyOutDir` mit `NODE_OPTIONS=--max-old-space-size=1200` (nie `npm run build`, nie nach `dist/` im Worktree).
- Bash-Timeout 600000 ms; dauert ein Lauf länger, mit `run_in_background` starten. Nie `pkill -f`.
- **Git:** Worktree `/home/webklar/apps/ticket.webklar.com/dev-auftraege`, Branch `feature/auftraege`. Im Repo `build` (Branch `test`) entsteht nur der Worktree-Eintrag aus Task 1, sonst wird dort nichts angefasst. Commits einzeln mit Dateinamen, deutsche Nachricht, Schlusszeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` (zweites `-m`). Kein Push, nie `--force`.
- **Suche:** In dieser Shell ist `grep` eine Funktion, die Dateien still auslässt. Die Steps benutzen `rg` (13.0.0, ohne Lookarounds); notfalls `command grep -a`.
## Dateien
| Datei | Aktion | Aufgabe | Task |
|---|---|---|---|
| `src/lib/financeRequest.js` | ändern | `request(path, { method, body, as })`: `as: 'blob'` liefert bei Erfolg den rohen Inhalt (PDF) | 2 |
| `tests/unit/financeRequestBlob.test.mjs` | neu | Tests dazu (Fetch-Attrappe) | 2 |
| `src/lib/auftraegeEndpoints.js` | neu | rein: `createAuftraegeEndpoints(request)` (14 Funktionen nach V5, darunter `ordneAuftragProjektZu`) und `abgerechnetPfad(ticketId)` | 3 |
| `tests/unit/auftraegeEndpoints.test.mjs` | neu | Pfade, Methoden, Körper, Kodierung, PDF-Blob | 3 |
| `src/lib/financeApi.js` | ändern | exportiert die 14 Funktionen (Verdrahtung mit Appwrite-JWT) | 3 |
| `src/lib/teamApi.js` | ändern | `getAbgerechnet(ticketId)` | 3 |
| `src/lib/customerTabs.js`, `tests/unit/customerTabs.test.mjs` | ändern | Beschriftung „Aufträge und Rechnungen“ | 4 |
| `src/lib/auftraegeAnsicht.js` | neu | rein: Laden, Ladezustand, frische Kundenart, Meldungen | 5, 6 |
| `tests/unit/auftraegeAnsicht.test.mjs` | neu | Tests dazu | 5, 6 |
| `src/components/auftraege/ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `AuftragListe.jsx`, `AuftragDialog.jsx` | neu | Platzhalter mit den Props aus V8 (samt `konten` und `onShowOverview`), rendern `null`; W1 bzw. W2 ersetzen die Dateien | 7 |
| `src/components/auftraege/AuftraegeUndRechnungen.jsx` | neu | Container des Reiters; reicht `konten` und `onShowOverview` an Liste und Dialoge durch | 7 |
| `src/pages/CustomerDetailPage.jsx` | ändern | Reiter `invoices` zeigt den Container | 8 |
| `src/components/finance/CustomerInvoices.jsx` | löschen | ersetzt durch den Container (keine Nutzer mehr) | 8 |
## Review Focus
Fälle, die die Spec nicht ausdrücklich nennt und die im Betrieb am ehesten auftreten, wahrscheinlichster zuerst:
1. **Schalter `AUFTRAEGE_ENABLED` aus (Vorgabe beim Livegang):** `auftraegeAktiv` ist `false` oder fehlt. Erwartet: kein Abruf der Aufträge (er würde 404 liefern), kein Aufträge-Abschnitt, keine Fehlermeldung; Projekte und Rechnungen laufen wie sonst. Test: Task 5, „Schalter aus“ und „unvollständige Antworten“.
2. **Projektkonten-Abruf scheitert** (Setup-Skript nicht gelaufen, Portal-Fehler 500, Portal ohne den Endpunkt): Erwartet: Der Abschnitt „Projekte“ zeigt die Meldung mit „Erneut versuchen“, die Rechnungsliste und ihr Knopf „Rechnung schreiben“ bleiben bedienbar, Aufträge werden nicht abgerufen. Test: Task 5, „Projektkonten scheitern“ und „vor der ersten Antwort“.
3. **Aufträge-Abruf scheitert allein** (Schalter an, Collection fehlt): Erwartet: Projekte bleiben sichtbar, der Aufträge-Abschnitt zeigt seine eigene Meldung. Test: Task 5, „Aufträge scheitern allein“.
4. **Neuladen scheitert, nachdem Daten da sind** (Portal antwortet nach dem Senden einer Rechnung kurz nicht): Erwartet: Die alten Zeilen bleiben stehen, darüber steht eine Warnung mit „Erneut versuchen“, nichts springt auf „Wird geladen …“. Test: Task 5, „Neuladen scheitert“.
5. **Kundenart eben gespeichert, die Kundenseite hält noch den alten Datensatz:** Erwartet: Die Dialoge bekommen den frischen Wert des Portals, der AB-Dialog fragt nicht erneut. Test: Task 6, `kundeMitKundenart`.
Weitere Randfälle haben eigene Tests in den Tasks: PDF-Abruf mit Fehlerantwort statt Blob und Kennungen mit Sonderzeichen (Task 2 und 3).
---
### Task 1: Startbedingung prüfen und Worktree anlegen
**Files:** keine Codeänderung. Legt `/home/webklar/apps/ticket.webklar.com/dev-auftraege` (Branch `feature/auftraege`) an.
**Interfaces:**
- Consumes: Tags `design-p4`, `design-p6`, `design-p7` (und ggf. `design-p8` bis `design-p10`) im WOMS-Repo, erreichbar über `/home/webklar/apps/ticket.webklar.com/build`.
- Produces: Worktree mit **echtem** `node_modules` (W1 und W2 verlinken es per `ln -s ../dev-auftraege/node_modules node_modules`), den Basis-Stand `B` (Tag-Name) und die Ausgangs-Testzahl `N0`. Beides gehört in die Abschlussmeldung (Task 9).
- [ ] **Step 1: Redesign-Tags und Basis bestimmen**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/build
for t in design-p4 design-p6 design-p7; do git rev-parse -q --verify "refs/tags/$t^{commit}" >/dev/null && echo "da $t" || echo "FEHLT $t"; done
for t in design-p10 design-p9 design-p8 design-p7; do git rev-parse -q --verify "refs/tags/$t^{commit}" >/dev/null && { echo "BASIS=$t"; break; }; done
```
Expected: dreimal `da …` und eine Zeile `BASIS=design-p…`. Das ist der höchste vorhandene Tag und damit `B`; nennt der Orchestrator ausdrücklich einen anderen Stand, gilt dieser. Fehlt einer der drei Tags: **STOPP**.
- [ ] **Step 2: Inhalt prüfen (Tags allein genügen nicht)**
Run (mit `B` aus Step 1):
```bash
cd /home/webklar/apps/ticket.webklar.com/build
B=<B aus Step 1>
git merge-base --is-ancestor design-p7 "$B" && echo "design-p7 liegt im Basis-Stand"
git cat-file -e "$B:src/lib/customerList.js" && echo "P4 ok"
git cat-file -e "$B:tests/unit/creditFormat.test.mjs" && echo "P6 ok"
git cat-file -e "$B:src/lib/financeUi.js" && echo "P7 ok"
git merge-base --is-ancestor origin/test "$B" && echo "Basis enthält origin/test" || echo "HINWEIS: origin/test ist weiter als die Basis (nur melden)"
```
Expected: vier Bestätigungen (`design-p7 …`, `P4 ok`, `P6 ok`, `P7 ok`); die letzte Zeile ist nur ein Hinweis. Fehlt eine der vier: **STOPP** mit Meldung „Startbedingung nicht erfüllt: <was>“ samt Ausgabe von `git tag -l 'design-p*' 'strang-p*'` und `git log --oneline -3 design/kunden design/finanzen`.
- [ ] **Step 3: Worktree anlegen**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com
test ! -e dev-auftraege && ! git -C build rev-parse -q --verify refs/heads/feature/auftraege >/dev/null && git -C build worktree add ../dev-auftraege -b feature/auftraege <B> && git -C dev-auftraege log --oneline -1
```
Expected: Der Worktree entsteht, die letzte Zeile ist der Commit der Basis. Existiert Ordner oder Branch schon: **STOPP** (ein früherer Versuch; Orchestrator fragen).
- [ ] **Step 4: `node_modules` wie im Redesign (echtes `npm ci`, kein Symlink)**
Run:
```bash
free -m | head -2
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && flock /tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 nice -n 10 npm ci --no-audit --no-fund && git status --short
```
Expected: Exit 0 und keine Zeile von `git status` (`node_modules` steht in `.gitignore`).
- [ ] **Step 5: Anker gegen den Basis-Stand prüfen**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
rg -n "CustomerInvoices" src/pages/CustomerDetailPage.jsx
rg -n "label: 'Rechnungen'" src/lib/customerTabs.js
rg -n "return async function request" src/lib/financeRequest.js
rg -n "getCustomerAboStatus" src/lib/teamApi.js
rg -n "export default function (CustomerInvoices|InvoiceTable|AddressWarning)" src/components/finance
rg -n "export function (useFinanceResource|useScrollIntoView)" src/hooks
rg -n "export function (customerInvoicesGate|invoiceDoneMessage)" src/lib/financeView.js
rg -n "export function noticeProps" src/lib/financeUi.js
rg -n "export \{ default as (Card|Button|EmptyState) " src/components/ui/index.js
rg -n "^\.fin-load-error|^\.fin-notice" src/styles
```
Expected: jeder Befehl mindestens ein Treffer (`CustomerDetailPage.jsx` zwei: Import und Verwendung, `export function (useFinanceResource|useScrollIntoView)` zwei). Fehlt ein Anker, weil das Redesign etwas umbenannt hat: **STOPP** und melden. Einzige Ausnahme: fehlen `.fin-load-error` und `.fin-notice` im CSS, benutzt Task 7 nur `msg …` ohne diese Zusatzklassen.
- [ ] **Step 6: Ausgangslage der Tests**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
free -m | head -2
flock $L/auftraege-schwer.lock env NODE_OPTIONS=--max-old-space-size=1024 timeout 900 nice -n 10 npm test > $L/auftraege-w0-test.log 2>&1; echo "exit $?"
rg "^(ℹ|#) (tests|pass|fail) " $L/auftraege-w0-test.log
```
Expected: `exit 0`, `fail 0`. Die Zahl hinter `pass` ist `N0` (notieren; Soll am Ende: `N0 + 21`). Schlägt schon die Basis fehl: **STOPP** und melden (Basis kaputt).
Kein Commit.
### Task 2: `financeRequest` liest Dateien (Blob)
**Files:**
- Modify: `src/lib/financeRequest.js`
- Test: `tests/unit/financeRequestBlob.test.mjs` (neu)
**Interfaces:**
- Consumes: `createFinanceRequest({ baseUrl, createJWT, fetchImpl })` (besteht).
- Produces: `request(path, { method = 'GET', body, as })`. Mit `as: 'blob'` liefert eine erfolgreiche Antwort (`res.ok`) `await res.blob()`. Fehlerantworten kommen unverändert als JSON-Meldung des Portals (Fehlertexte, Status, JWT-Wiederholung bei 401). Ohne `as` oder mit jedem anderen Wert bleibt alles wie bisher. Das nutzt `getAuftragPdf` (Task 3).
- [ ] **Step 1: Failing Test schreiben**
`tests/unit/financeRequestBlob.test.mjs`:
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { createFinanceRequest, PORTAL_DOWN_TEXT } = await loadModule('src/lib/financeRequest.js')
const BASE = 'https://portal.example.invalid/api/admin/finanzen'
/** Antwort wie fetch: { ok, status, json(), blob() }; ohne `json` ist der Körper kein JSON (json() wirft). */
const reply = (status, { json, blob } = {}) => ({
ok: status >= 200 && status < 300,
status,
json: async () => {
if (json === undefined) throw new SyntaxError('Unexpected token % in JSON')
return json
},
blob: async () => blob,
})
function client({ responses = [], createJWT } = {}) {
const calls = []
const queue = [...responses]
const api = createFinanceRequest({
baseUrl: BASE,
createJWT: createJWT || (async () => ({ jwt: 'jwt-1' })),
fetchImpl: async (url, init) => {
calls.push({ url, init })
return queue.shift() || reply(200, { json: {} })
},
})
return { api, calls }
}
const fails = async (promise) => promise.then(() => assert.fail('hätte scheitern müssen'), (err) => err)
const pdf = () => new Blob(['%PDF-1.7 Probe'], { type: 'application/pdf' })
test('as blob: Erfolg liefert den rohen Inhalt als Blob, mit Bearer-JWT, ohne JSON zu lesen', async () => {
const { api, calls } = client({
responses: [{ ...reply(200, { blob: pdf() }), json: async () => assert.fail('json() darf bei as blob nicht gelesen werden') }],
})
const result = await api('/auftraege/a1/pdf', { as: 'blob' })
assert.ok(result instanceof Blob)
assert.equal(result.type, 'application/pdf')
assert.equal(await result.text(), '%PDF-1.7 Probe')
assert.equal(calls[0].url, `${BASE}/auftraege/a1/pdf`)
assert.equal(calls[0].init.method, 'GET')
assert.equal(calls[0].init.headers.Authorization, 'Bearer jwt-1')
})
test('as blob: Fehler kommen weiter als Meldung des Portals mit Status (kein kaputtes PDF)', async () => {
const { api } = client({ responses: [reply(404, { json: { error: 'PDF nicht gefunden.' } })] })
const err = await fails(api('/auftraege/a1/pdf', { as: 'blob' }))
assert.equal(err.message, 'PDF nicht gefunden.')
assert.equal(err.status, 404)
// Proxy-Fehlerseite ohne JSON: dieselbe Meldung wie bei JSON-Anfragen
const { api: api2 } = client({ responses: [reply(502)] })
assert.equal((await fails(api2('/auftraege/a1/pdf', { as: 'blob' }))).message, PORTAL_DOWN_TEXT)
})
test('as blob: bei 401 einmal mit frischem JWT wiederholt, dann kommt der Blob', async () => {
let n = 0
const datei = pdf()
const { api, calls } = client({
createJWT: async () => ({ jwt: `jwt-${++n}` }),
responses: [reply(401, { json: { error: 'abgelaufen' } }), reply(200, { blob: datei })],
})
assert.equal(await api('/auftraege/a1/pdf', { as: 'blob' }), datei)
assert.deepEqual(calls.map((c) => c.init.headers.Authorization), ['Bearer jwt-1', 'Bearer jwt-2'])
})
test('ohne as bleibt es JSON (der Blob wird nicht gelesen), ein unbekannter Wert verhält sich ebenso', async () => {
const { api } = client({
responses: [{ ...reply(200, { json: { ok: 1 } }), blob: async () => assert.fail('blob() darf ohne as nicht gelesen werden') }],
})
assert.deepEqual(await api('/x'), { ok: 1 })
const { api: api2 } = client({ responses: [reply(200, { json: { ok: 2 } })] })
assert.deepEqual(await api2('/x', { as: 'text' }), { ok: 2 })
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/financeRequestBlob.test.mjs`
Expected: FAIL in zwei Tests („Erfolg liefert den rohen Inhalt als Blob“ und „bei 401 …“); `request` ignoriert `as` und liefert `{}`. Die anderen beiden bestehen schon.
- [ ] **Step 3: `as: 'blob'` in `createFinanceRequest` einbauen**
In `src/lib/financeRequest.js`: Die innere Funktion bekommt die Signatur `request(path, { method = 'GET', body, as } = {})`. Direkt nach dem `fetchWithRetry`-Aufruf und vor dem Lesen des JSON kommt `if (as === 'blob' && res.ok) return res.blob()` (mit einem Kommentar: Dateien wie das PDF der Auftragsbestätigung kommen roh, Fehler weiter als JSON). Header, JWT-Cache, Wiederholung bei 401 und alle Fehlertexte bleiben, ebenso die fremden Zeilen (`code`, `fehler`) im Fehlerzweig. Den Kommentarblock über `createFinanceRequest` um einen Satz zu `as: 'blob'` ergänzen.
- [ ] **Step 4: Tests laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/financeRequestBlob.test.mjs tests/unit/financeRequest.test.mjs`
Expected: PASS, in der neuen Datei `pass 4`, `fail 0`; die bestehende Datei bleibt grün.
- [ ] **Step 5: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/lib/financeRequest.js tests/unit/financeRequestBlob.test.mjs
git commit -m "Finanzen-Anfrage: Dateien lesen (as: 'blob'), z. B. das PDF einer Auftragsbestätigung" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 3: API-Schicht – Pfade rein, `financeApi.js` und `teamApi.js` verdrahtet
**Files:**
- Create: `src/lib/auftraegeEndpoints.js`
- Test: `tests/unit/auftraegeEndpoints.test.mjs` (neu)
- Modify: `src/lib/financeApi.js`, `src/lib/teamApi.js`
**Interfaces:**
- Consumes: `request(path, { method, body, as })` aus `createFinanceRequest` (Task 2).
- Produces:
- `createAuftraegeEndpoints(request) → { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf }` (14 Funktionen) und `abgerechnetPfad(ticketId) → string` (reine Datei ohne Importe).
- Aus `src/lib/financeApi.js` exportiert (V8; Basis `/api/admin/finanzen`, Antwortformen nach V5):
- `getProjektkonten(kundeId) → { auftraegeAktiv, kunde, konten, ohneProjekt, stripeOk }`
- `getProjektkonto(projektId) → { konto, auftraege, rechnungen, offeneArbeitsblaetter, stripeOk }`
- `previewInvoice(body) → { positionen, summeCents, leistungszeitraum, customFields }`
- `listAuftraege(kundeId) → { auftraege }`
- `getAuftrag(id)`, `createAuftrag(body)`, `patchAuftrag(id, body) → { auftrag }`; `deleteAuftrag(id) → { ok: true }`
- `sendAuftrag(id)`, `erinnereAuftrag(id) → { auftrag, mailFehler }`; `neueFassungAuftrag(id)`, `storniereAuftrag(id) → { auftrag }`
- `ordneAuftragProjektZu(id, projektId) → { auftrag }` (Nachtrag der Planrunde, V5 Zeile L3: ordnet einer AB ohne Projekt ein Projekt zu; 409, wenn sie schon eines hat oder `storniert`, `ersetzt` oder `widerrufen` ist; W2 braucht es für „Projekt zuordnen“)
- `getAuftragPdf(id) → Promise<Blob>`
- Aus `src/lib/teamApi.js`: `getAbgerechnet(ticketId) → { abgerechnet: { <worksheetId>: '<Rechnungsnummer>' | 'Entwurf' } }` (V5; für alle Mitarbeiter, ohne Beträge).
- `createInvoice(body)` besteht und reicht den erweiterten Körper unverändert durch; nur der Kommentar wird nachgezogen.
- [ ] **Step 1: Failing Test schreiben**
`tests/unit/auftraegeEndpoints.test.mjs`:
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { createAuftraegeEndpoints, abgerechnetPfad } = await loadModule('src/lib/auftraegeEndpoints.js')
const { createFinanceRequest } = await loadModule('src/lib/financeRequest.js')
const BASE = 'https://portal.example.invalid/api/admin/finanzen'
// Echte Anfrage-Schicht mit Fetch-Attrappe: geprüft wird, was das Portal erreicht (URL, Methode, Kopf, JSON-Körper)
function setup(answer = () => ({ ok: true, status: 200, json: async () => ({ ok: true }) })) {
const calls = []
const request = createFinanceRequest({
baseUrl: BASE,
createJWT: async () => ({ jwt: 'jwt-1' }),
fetchImpl: async (url, init) => {
calls.push({ url, method: init.method, body: init.body, auth: init.headers.Authorization })
return answer()
},
})
return { api: createAuftraegeEndpoints(request), calls }
}
// [Funktion, Argumente, Methode, Pfad ab BASE, Körper]
const FAELLE = [
['getProjektkonten', ['k1'], 'GET', '/projektkonten?kunde=k1', undefined],
['getProjektkonto', ['p1'], 'GET', '/projektkonten/p1', undefined],
['previewInvoice', [{ customerId: 'k1', art: 'aufwand', projektId: 'p1' }], 'POST', '/rechnungen/vorschau', { customerId: 'k1', art: 'aufwand', projektId: 'p1' }],
['listAuftraege', ['k1'], 'GET', '/auftraege?kunde=k1', undefined],
['getAuftrag', ['a1'], 'GET', '/auftraege/a1', undefined],
['createAuftrag', [{ kundenId: 'k1', titel: 'Website' }], 'POST', '/auftraege', { kundenId: 'k1', titel: 'Website' }],
['patchAuftrag', ['a1', { titel: 'Neu' }], 'PATCH', '/auftraege/a1', { titel: 'Neu' }],
['deleteAuftrag', ['a1'], 'DELETE', '/auftraege/a1', undefined],
['sendAuftrag', ['a1'], 'POST', '/auftraege/a1/senden', undefined],
['erinnereAuftrag', ['a1'], 'POST', '/auftraege/a1/erinnern', undefined],
['neueFassungAuftrag', ['a1'], 'POST', '/auftraege/a1/neue-fassung', undefined],
['storniereAuftrag', ['a1'], 'POST', '/auftraege/a1/stornieren', undefined],
['ordneAuftragProjektZu', ['a1', 'p1'], 'POST', '/auftraege/a1/projekt', { projektId: 'p1' }],
]
test('alle 14 Funktionen sind da (V8 samt „Projekt zuordnen“ aus V5)', () => {
const { api } = setup()
assert.deepEqual(Object.keys(api).sort(), [
'createAuftrag', 'deleteAuftrag', 'erinnereAuftrag', 'getAuftrag', 'getAuftragPdf', 'getProjektkonten', 'getProjektkonto',
'listAuftraege', 'neueFassungAuftrag', 'ordneAuftragProjektZu', 'patchAuftrag', 'previewInvoice', 'sendAuftrag', 'storniereAuftrag',
])
})
test('Pfad, Methode, Kopf und Körper jeder Funktion (die Vorschau trägt keinen Schlüssel dazu)', async () => {
for (const [name, args, method, path, body] of FAELLE) {
const { api, calls } = setup()
await api[name](...args)
assert.equal(calls.length, 1, name)
assert.equal(calls[0].url, `${BASE}${path}`, name)
assert.equal(calls[0].method, method, name)
assert.equal(calls[0].auth, 'Bearer jwt-1', name)
assert.equal(calls[0].body, body === undefined ? undefined : JSON.stringify(body), name)
}
})
test('Kennungen werden URL-kodiert, auch im Pfad der abgerechneten Arbeitsblätter', async () => {
const { api, calls } = setup()
await api.getProjektkonten('k 1&x=2')
await api.getAuftrag('a/b?c')
await api.storniereAuftrag('a#1')
await api.ordneAuftragProjektZu('a b', 'p/1')
assert.deepEqual(calls.map((c) => c.url.slice(BASE.length)), [
'/projektkonten?kunde=k%201%26x%3D2',
'/auftraege/a%2Fb%3Fc',
'/auftraege/a%231/stornieren',
'/auftraege/a%20b/projekt',
])
assert.equal(calls[3].body, JSON.stringify({ projektId: 'p/1' })) // die Projektkennung steht im Körper, nicht im Pfad
assert.equal(abgerechnetPfad('100131'), '/arbeitsblaetter/abgerechnet?ticketId=100131')
assert.equal(abgerechnetPfad('t 1/2'), '/arbeitsblaetter/abgerechnet?ticketId=t%201%2F2')
})
test('getAuftragPdf liefert das PDF als Blob; eine Fehlerantwort kommt als Meldung des Portals', async () => {
const datei = new Blob(['%PDF-1.7'], { type: 'application/pdf' })
const ok = setup(() => ({ ok: true, status: 200, json: async () => assert.fail('kein JSON lesen'), blob: async () => datei }))
assert.equal(await ok.api.getAuftragPdf('a1'), datei)
assert.equal(ok.calls[0].url, `${BASE}/auftraege/a1/pdf`)
assert.equal(ok.calls[0].method, 'GET')
const fehler = setup(() => ({ ok: false, status: 404, json: async () => ({ error: 'PDF nicht gefunden.' }), blob: async () => assert.fail('kein Blob lesen') }))
await assert.rejects(fehler.api.getAuftragPdf('a1'), (e) => e.message === 'PDF nicht gefunden.' && e.status === 404)
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeEndpoints.test.mjs`
Expected: FAIL, `src/lib/auftraegeEndpoints.js` fehlt.
- [ ] **Step 3: `src/lib/auftraegeEndpoints.js` implementieren**
Reine Datei ohne Importe, deutscher Kopfkommentar im Stil von `financeRequest.js`. `createAuftraegeEndpoints(request)` gibt ein Objekt mit genau den 14 Funktionen zurück. Jede ruft `request` mit Pfad relativ zu `/api/admin/finanzen`; jede Kennung läuft durch `encodeURIComponent`; Aufrufe ohne Körper übergeben keinen Körper. Tabelle:
| Funktion | Methode | Pfad | Körper |
|---|---|---|---|
| `getProjektkonten(kundeId)` | GET | `/projektkonten?kunde=<kundeId>` | – |
| `getProjektkonto(projektId)` | GET | `/projektkonten/<projektId>` | – |
| `previewInvoice(body)` | POST | `/rechnungen/vorschau` | `body` unverändert |
| `listAuftraege(kundeId)` | GET | `/auftraege?kunde=<kundeId>` | – |
| `getAuftrag(id)` | GET | `/auftraege/<id>` | – |
| `createAuftrag(body)` | POST | `/auftraege` | `body` |
| `patchAuftrag(id, body)` | PATCH | `/auftraege/<id>` | `body` |
| `deleteAuftrag(id)` | DELETE | `/auftraege/<id>` | – |
| `sendAuftrag(id)` | POST | `/auftraege/<id>/senden` | – |
| `erinnereAuftrag(id)` | POST | `/auftraege/<id>/erinnern` | – |
| `neueFassungAuftrag(id)` | POST | `/auftraege/<id>/neue-fassung` | – |
| `storniereAuftrag(id)` | POST | `/auftraege/<id>/stornieren` | – |
| `ordneAuftragProjektZu(id, projektId)` | POST | `/auftraege/<id>/projekt` | `{ projektId }` |
| `getAuftragPdf(id)` | GET | `/auftraege/<id>/pdf` | – (Option `as: 'blob'`) |
`abgerechnetPfad(ticketId)` liefert `/arbeitsblaetter/abgerechnet?ticketId=<kodiert>` (Basis `/api/team`, siehe Step 4).
- [ ] **Step 4: Verdrahtung in `financeApi.js` und `teamApi.js`**
- `src/lib/financeApi.js`: `import { createAuftraegeEndpoints } from './auftraegeEndpoints'` zu den Importen. Hinter dem Block „Rechnungen“ ein Abschnitt mit Kommentar „Projektkonten, Aufträge und Rechnungsvorschau (Pfade und Methoden: auftraegeEndpoints.js, getestet)“ und `export const { getProjektkonten, getProjektkonto, previewInvoice, listAuftraege, getAuftrag, createAuftrag, patchAuftrag, deleteAuftrag, sendAuftrag, erinnereAuftrag, neueFassungAuftrag, storniereAuftrag, ordneAuftragProjektZu, getAuftragPdf } = createAuftraegeEndpoints(request)` (die vorhandene Konstante `request` wird übergeben). Der Kommentar über `createInvoice` nennt den erweiterten Körper `{ customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum, daysUntilDue, memo, senden }`; die Funktion selbst bleibt unverändert.
- `src/lib/teamApi.js`: `import { abgerechnetPfad } from './auftraegeEndpoints'` zu den Importen; vor dem Block „Abo-Status“ ein Abschnitt „Abgerechnete Arbeitsblätter (alle Mitarbeiter, ohne Beträge)“ mit `export const getAbgerechnet = (ticketId) => request(abgerechnetPfad(ticketId))` und einem Kommentar zur Antwortform.
- [ ] **Step 5: Tests und Syntaxprüfung**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeEndpoints.test.mjs
node_modules/.bin/esbuild src/lib/financeApi.js src/lib/teamApi.js --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
```
Expected: `pass 4`, `fail 0`; esbuild ohne Ausgabe und Exit 0 (nur Syntax, die Dateien lassen sich unter Node nicht bündeln).
- [ ] **Step 6: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/lib/auftraegeEndpoints.js tests/unit/auftraegeEndpoints.test.mjs src/lib/financeApi.js src/lib/teamApi.js
git commit -m "Finanzen-API: Projektkonten, Auftragsbestätigungen (samt Projekt zuordnen), Rechnungsvorschau und abgerechnete Arbeitsblätter (Pfade rein und getestet)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: Reiter-Beschriftung „Aufträge und Rechnungen“
**Files:**
- Modify: `src/lib/customerTabs.js`, `tests/unit/customerTabs.test.mjs`
**Interfaces:**
- Consumes: `customerTabs(isAdmin)` und `resolveCustomerTab(tab, isAdmin)` (bestehen).
- Produces: `customerTabs(true)[2]` ist `{ id: 'invoices', label: 'Aufträge und Rechnungen' }`. Die `id` bleibt `invoices` (Mails des Portals und Aufgaben der Finanzen-Seite verlinken `?tab=invoices`), der Reiter bleibt nur für Admins.
- [ ] **Step 1: Test anpassen (zuerst)**
In `tests/unit/customerTabs.test.mjs` die Zeile `{ id: 'invoices', label: 'Rechnungen' },` zu `{ id: 'invoices', label: 'Aufträge und Rechnungen' },` ändern und den Titel des ersten Tests zu `'customerTabs: volle Reiterfolge der Spec, „Aufträge und Rechnungen“ nur für Admins'`. Alles andere bleibt (es pinnt unter anderem, dass `customerTabs(false)` keinen Reiter `invoices` enthält und `resolveCustomerTab('invoices', false)` auf `overview` fällt).
- [ ] **Step 2: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/customerTabs.test.mjs`
Expected: FAIL (die Beschriftung lautet noch „Rechnungen“).
- [ ] **Step 3: Beschriftung ändern**
In `src/lib/customerTabs.js` das Label des Eintrags `invoices` auf `'Aufträge und Rechnungen'` setzen und die beiden Kommentare, die „Rechnungen“ als Reiternamen nennen (Kopfkommentar der Datei und Doc-Kommentar von `customerTabs`), auf „Aufträge und Rechnungen“ nachziehen. `adminOnly` und alle anderen Reiter bleiben.
- [ ] **Step 4: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/customerTabs.test.mjs`
Expected: PASS, `fail 0`.
- [ ] **Step 5: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/lib/customerTabs.js tests/unit/customerTabs.test.mjs
git commit -m "Kundenreiter „Rechnungen“ heißt jetzt „Aufträge und Rechnungen“ (id bleibt invoices)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Ansichtslogik – Laden und Ladezustand
**Files:**
- Create: `src/lib/auftraegeAnsicht.js`
- Test: `tests/unit/auftraegeAnsicht.test.mjs` (neu)
**Interfaces:**
- Consumes: nichts (reine Datei; die API-Funktionen kommen als Argument).
- Produces, alle aus `src/lib/auftraegeAnsicht.js`:
- `KONTEN_FEHLER = 'Die Projektübersicht konnte nicht geladen werden.'`, `AUFTRAEGE_FEHLER = 'Die Aufträge konnten nicht geladen werden.'`
- `ladeAuftraegeAnsicht(kundeId, { getProjektkonten, listAuftraege }) → Promise<{ konten, auftraege, auftraegeFehler }>`: `konten` ist die Antwort von `getProjektkonten`, `auftraege` die Liste oder `null`, `auftraegeFehler` Text oder `''`.
- `ansichtsZustand({ data, error } = {}) → AnsichtsZustand` (`data` und `error` kommen aus `useFinanceResource`):
```
{ projekteStatus: 'laden' | 'fehler' | 'bereit', fehler: string, nachladeFehler: string,
auftraegeAktiv: boolean, auftraegeStatus: 'aus' | 'bereit' | 'fehler', auftraegeFehler: string,
konten: Projektkonto[], ohneProjekt: { berechnetCents, bezahltCents, anzahl }, stripeOk: boolean,
kunde: { id, name, kundenart } | null, auftraege: AuftragTeam[] }
```
- [ ] **Step 1: Failing Test schreiben**
`tests/unit/auftraegeAnsicht.test.mjs`:
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { KONTEN_FEHLER, AUFTRAEGE_FEHLER, ladeAuftraegeAnsicht, ansichtsZustand } = await loadModule('src/lib/auftraegeAnsicht.js')
// Antwort von getProjektkonten (V5) mit den Feldern, die die Ansicht braucht
const konten = (extra = {}) => ({
auftraegeAktiv: false,
kunde: { id: 'k1', name: 'Anelia Wolf', kundenart: 'privat' },
konten: [{ projektId: 'p1', name: 'Website' }],
ohneProjekt: { berechnetCents: 5000, bezahltCents: 0, anzahl: 1 },
stripeOk: true,
...extra,
})
const OHNE_PROJEKT_LEER = { berechnetCents: 0, bezahltCents: 0, anzahl: 0 }
// Attrappe für getProjektkonten und listAuftraege: gibt Werte zurück, wirft Fehler und merkt sich die Aufrufe
function fakeApi({ projektkonten, auftraege } = {}) {
const calls = []
const antwort = (wert) => {
if (wert instanceof Error) throw wert
return wert
}
return {
calls,
getProjektkonten: async (id) => { calls.push(['konten', id]); return antwort(projektkonten) },
listAuftraege: async (id) => { calls.push(['auftraege', id]); return antwort(auftraege) },
}
}
test('Schalter aus: die Aufträge werden gar nicht erst abgerufen', async () => {
const api = fakeApi({ projektkonten: konten({ auftraegeAktiv: false }), auftraege: new Error('404 darf nie ankommen') })
const result = await ladeAuftraegeAnsicht('k1', api)
assert.deepEqual(api.calls, [['konten', 'k1']])
assert.equal(result.konten.auftraegeAktiv, false)
assert.equal(result.auftraege, null)
assert.equal(result.auftraegeFehler, '')
// ein fehlendes Feld gilt wie „aus“, nur true schaltet an; eine Antwort ohne Objekt wird zu {}
const fehlt = fakeApi({ projektkonten: { konten: [] } })
assert.equal((await ladeAuftraegeAnsicht('k1', fehlt)).auftraege, null)
assert.deepEqual(fehlt.calls, [['konten', 'k1']])
const nichts = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: null }))
assert.deepEqual(nichts, { konten: {}, auftraege: null, auftraegeFehler: '' })
})
test('Schalter an: erst die Projektkonten, dann die Aufträge des Kunden', async () => {
const liste = [{ id: 'a1', nummer: 'AB-2026-0001', status: 'versendet' }]
const api = fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: { auftraege: liste } })
const result = await ladeAuftraegeAnsicht('k1', api)
assert.deepEqual(api.calls, [['konten', 'k1'], ['auftraege', 'k1']])
assert.deepEqual(result.auftraege, liste)
assert.equal(result.auftraegeFehler, '')
// eine Antwort ohne Liste gibt eine leere Liste statt eines Absturzes
const leer = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: {} }))
assert.deepEqual(leer.auftraege, [])
})
test('Aufträge scheitern allein: die Projektkonten bleiben, der Fehler steht eigens da', async () => {
const api = fakeApi({
projektkonten: konten({ auftraegeAktiv: true }),
auftraege: Object.assign(new Error('Interner Fehler.'), { status: 500 }),
})
const result = await ladeAuftraegeAnsicht('k1', api)
assert.equal(result.konten.konten.length, 1)
assert.equal(result.auftraege, null)
assert.equal(result.auftraegeFehler, 'Interner Fehler.')
const ohneText = await ladeAuftraegeAnsicht('k1', fakeApi({ projektkonten: konten({ auftraegeAktiv: true }), auftraege: new Error('') }))
assert.equal(ohneText.auftraegeFehler, AUFTRAEGE_FEHLER)
assert.equal(AUFTRAEGE_FEHLER, 'Die Aufträge konnten nicht geladen werden.')
})
test('Projektkonten scheitern: der Fehler geht weiter, die Aufträge werden nicht abgerufen', async () => {
const fehler = Object.assign(new Error('Interner Fehler.'), { status: 500 })
const api = fakeApi({ projektkonten: fehler, auftraege: { auftraege: [] } })
await assert.rejects(ladeAuftraegeAnsicht('k1', api), (e) => e === fehler)
assert.deepEqual(api.calls, [['konten', 'k1']])
})
test('ansichtsZustand: vor der ersten Antwort „laden“, nach einem Fehler „fehler“ mit der Meldung', () => {
const laden = ansichtsZustand({ data: null, error: '' })
assert.equal(laden.projekteStatus, 'laden')
assert.equal(laden.auftraegeStatus, 'aus') // der Schalter ist noch unbekannt: kein Abschnitt, kein Flackern
assert.deepEqual(laden.konten, [])
assert.equal(ansichtsZustand().projekteStatus, 'laden')
const fehler = ansichtsZustand({ data: null, error: KONTEN_FEHLER })
assert.equal(fehler.projekteStatus, 'fehler')
assert.equal(fehler.fehler, KONTEN_FEHLER)
assert.equal(fehler.auftraegeStatus, 'aus') // ohne Projektkonten kein Schalter, also auch keine Aufträge
assert.equal(KONTEN_FEHLER, 'Die Projektübersicht konnte nicht geladen werden.')
})
test('ansichtsZustand: Schalter aus – Projekte bereit, kein Aufträge-Abschnitt', () => {
const z = ansichtsZustand({ data: { konten: konten({ auftraegeAktiv: false }), auftraege: null, auftraegeFehler: '' }, error: '' })
assert.equal(z.projekteStatus, 'bereit')
assert.equal(z.auftraegeAktiv, false)
assert.equal(z.auftraegeStatus, 'aus')
assert.equal(z.konten.length, 1)
assert.deepEqual(z.ohneProjekt, { berechnetCents: 5000, bezahltCents: 0, anzahl: 1 })
assert.equal(z.stripeOk, true)
assert.deepEqual(z.kunde, { id: 'k1', name: 'Anelia Wolf', kundenart: 'privat' })
assert.equal(z.nachladeFehler, '')
})
test('ansichtsZustand: Schalter an – Aufträge bereit oder mit eigenem Fehler, die Projekte bleiben', () => {
const liste = [{ id: 'a1' }]
const bereit = ansichtsZustand({ data: { konten: konten({ auftraegeAktiv: true }), auftraege: liste, auftraegeFehler: '' }, error: '' })
assert.equal(bereit.auftraegeAktiv, true)
assert.equal(bereit.auftraegeStatus, 'bereit')
assert.deepEqual(bereit.auftraege, liste)
const kaputt = ansichtsZustand({
data: { konten: konten({ auftraegeAktiv: true }), auftraege: null, auftraegeFehler: 'Interner Fehler.' },
error: '',
})
assert.equal(kaputt.projekteStatus, 'bereit')
assert.equal(kaputt.auftraegeStatus, 'fehler')
assert.equal(kaputt.auftraegeFehler, 'Interner Fehler.')
assert.deepEqual(kaputt.auftraege, [])
})
test('ansichtsZustand: Neuladen scheitert – die alten Daten bleiben, die Meldung steht daneben', () => {
const data = { konten: konten({ auftraegeAktiv: true }), auftraege: [{ id: 'a1' }], auftraegeFehler: '' }
const z = ansichtsZustand({ data, error: 'Das Kundenportal ist gerade nicht erreichbar.' })
assert.equal(z.projekteStatus, 'bereit')
assert.equal(z.fehler, '')
assert.equal(z.nachladeFehler, 'Das Kundenportal ist gerade nicht erreichbar.')
assert.equal(z.konten.length, 1)
assert.equal(z.auftraegeStatus, 'bereit')
assert.equal(z.auftraege.length, 1)
})
test('ansichtsZustand: unvollständige Antworten brechen die Ansicht nicht', () => {
for (const data of [{}, { konten: null }, { konten: {} }, { konten: { konten: 'x', ohneProjekt: 5, kunde: 'k' } }]) {
const z = ansichtsZustand({ data, error: '' })
assert.equal(z.projekteStatus, 'bereit')
assert.deepEqual(z.konten, [])
assert.deepEqual(z.ohneProjekt, OHNE_PROJEKT_LEER)
assert.equal(z.stripeOk, true)
assert.equal(z.kunde, null)
assert.equal(z.auftraegeAktiv, false)
assert.equal(z.auftraegeStatus, 'aus')
}
assert.equal(ansichtsZustand({ data: { konten: konten({ stripeOk: false }) }, error: '' }).stripeOk, false)
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
Expected: FAIL, `src/lib/auftraegeAnsicht.js` fehlt.
- [ ] **Step 3: `src/lib/auftraegeAnsicht.js` implementieren**
Reine Datei (kein React, kein Appwrite, noch keine Importe), deutscher Kopfkommentar „Ansichtslogik des Reiters Aufträge und Rechnungen“. Regeln:
- Die zwei Fehlertexte werden exportiert (Wortlaut siehe Interfaces).
- `ladeAuftraegeAnsicht`: ruft zuerst `getProjektkonten(kundeId)`; ein Fehler dort wird nicht abgefangen. Ist die Antwort kein Objekt, gilt `{}`. Nur wenn `konten.auftraegeAktiv === true`, folgt `listAuftraege(kundeId)`: Erfolg gibt `antwort.auftraege`, wenn das ein Array ist, sonst `[]`; ein Fehler gibt `auftraege: null` und `auftraegeFehler: err.message`, bei leerer Meldung `AUFTRAEGE_FEHLER`. Sonst `auftraege: null`, `auftraegeFehler: ''`. Rückgabe immer `{ konten, auftraege, auftraegeFehler }`.
- `ansichtsZustand({ data, error } = {})`:
- Ohne `data`: `projekteStatus` ist `'fehler'` (mit `fehler = error`), wenn `error` nicht leer ist, sonst `'laden'`; `auftraegeStatus` ist dann `'aus'`; `konten` `[]`, `ohneProjekt` eine Kopie von `{ berechnetCents: 0, bezahltCents: 0, anzahl: 0 }`, `stripeOk` true, `kunde` null, `auftraege` `[]`.
- Mit `data`: `projekteStatus: 'bereit'`, `fehler: ''`, `nachladeFehler: error || ''`. Gelesen wird aus `data.konten` (nur wenn Objekt, sonst `{}`): `auftraegeAktiv = (data.konten.auftraegeAktiv === true)`, `konten` nur wenn Array, `ohneProjekt` nur wenn Objekt (sonst die Null-Kopie), `stripeOk = (data.konten.stripeOk !== false)`, `kunde` nur wenn Objekt (sonst `null`).
- `auftraegeStatus`: `'aus'` ohne Schalter, `'fehler'` bei Schalter und nicht leerem `data.auftraegeFehler`, sonst `'bereit'`. `auftraegeFehler` nur im Fall `'fehler'`. `auftraege` ist `data.auftraege`, wenn Schalter an und Array, sonst `[]`.
- [ ] **Step 4: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
Expected: PASS, `pass 9`, `fail 0`.
- [ ] **Step 5: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/lib/auftraegeAnsicht.js tests/unit/auftraegeAnsicht.test.mjs
git commit -m "Aufträge und Rechnungen: Laden und Ladezustand des Reiters als reine Funktionen (mit Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Ansichtslogik – Kundenart und Meldungen
**Files:**
- Modify: `src/lib/auftraegeAnsicht.js`, `tests/unit/auftraegeAnsicht.test.mjs`
**Interfaces:**
- Consumes: `invoiceDoneMessage(actionKey, rechnung)` aus `src/lib/financeView.js` (liefert für `'create-send'` „Rechnung <Nr> wurde an <E-Mail> gesendet.“ und für `'create-draft'` „Der Entwurf wurde gespeichert.“).
- Produces, aus `src/lib/auftraegeAnsicht.js`:
- `kundeMitKundenart(customer, kundeInfo) → customer'`: Ist `kundeInfo.kundenart` eines von `'unternehmen'`, `'privat'`, kommt `{ ...customer, kundenart: <dieser Wert> }` zurück (das Dokument selbst bleibt unverändert). In allen anderen Fällen (`kundeInfo` fehlt, kein Objekt, Wert leer, unbekannt oder keine Zeichenkette) die **unveränderte Referenz** `customer`; `null` bleibt `null`.
- `erstelltMeldung(rechnung, info) → { tone, text }`: wie `InvoiceTable.handleCreated`: mit `info.notice` (Zeichenkette) `{ tone: info.tone || 'ok', text: info.notice }`, sonst `{ tone: 'ok', text: invoiceDoneMessage(info.senden ? 'create-send' : 'create-draft', rechnung) }`. `info` darf fehlen oder `null` sein.
- `auftragMeldung(auftrag) → { tone, text }` für `onSaved(auftrag)`.
- [ ] **Step 1: Failing Tests ergänzen**
In `tests/unit/auftraegeAnsicht.test.mjs` die `loadModule`-Zeile um `kundeMitKundenart, erstelltMeldung, auftragMeldung` erweitern und anhängen:
```js
test('kundeMitKundenart: ein gültiger Wert des Portals gewinnt gegen das veraltete Kundendokument', () => {
const doc = { $id: 'k1', name: 'Anelia Wolf', kundenart: '' }
assert.deepEqual(kundeMitKundenart(doc, { id: 'k1', kundenart: 'privat' }), { ...doc, kundenart: 'privat' })
assert.equal(doc.kundenart, '') // das Dokument der Seite wird nicht verändert
assert.equal(kundeMitKundenart({ ...doc, kundenart: 'unternehmen' }, { kundenart: 'privat' }).kundenart, 'privat')
})
test('kundeMitKundenart: ohne gültigen Wert vom Portal bleibt das Dokument (dieselbe Referenz)', () => {
const doc = { $id: 'k1', kundenart: 'unternehmen' }
for (const info of [null, undefined, {}, { kundenart: '' }, { kundenart: 'firma' }, { kundenart: 5 }, 'privat']) {
assert.equal(kundeMitKundenart(doc, info), doc)
}
assert.equal(kundeMitKundenart(null, { kundenart: 'privat' }), null)
})
test('erstelltMeldung: gesendet, Entwurf und die Meldung nach festgeschriebener, nicht gesendeter Rechnung', () => {
assert.deepEqual(
erstelltMeldung({ number: 'WEBKLAR-0009', customerEmail: 'a@b.de' }, { senden: true }),
{ tone: 'ok', text: 'Rechnung WEBKLAR-0009 wurde an a@b.de gesendet.' },
)
assert.deepEqual(erstelltMeldung({ id: 'in_1' }, { senden: false }), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
assert.deepEqual(erstelltMeldung(null, undefined), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
assert.deepEqual(erstelltMeldung(null, null), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
const text = 'Rechnung WEBKLAR-0010 wurde festgeschrieben, aber nicht versendet. Bitte in der Liste erneut senden.'
assert.deepEqual(erstelltMeldung(null, { senden: true, notice: text, tone: 'warn', finalizedFilter: 'offen' }), { tone: 'warn', text })
assert.deepEqual(erstelltMeldung(null, { senden: true, notice: text }), { tone: 'ok', text })
})
test('auftragMeldung: Entwurf, gesendet und gesendet ohne zugestellte Mail', () => {
assert.deepEqual(auftragMeldung({ status: 'entwurf', nummer: '' }), { tone: 'ok', text: 'Der Entwurf wurde gespeichert.' })
assert.deepEqual(
auftragMeldung({ status: 'versendet', nummer: 'AB-2026-0001', gesendetAn: 'k@x.de', mailStatus: 'gesendet' }),
{ tone: 'ok', text: 'Auftragsbestätigung AB-2026-0001 wurde an k@x.de gesendet.' },
)
assert.deepEqual(
auftragMeldung({ status: 'versendet', nummer: 'AB-2026-0001', gesendetAn: 'k@x.de', mailStatus: 'fehler' }),
{ tone: 'warn', text: 'Auftragsbestätigung AB-2026-0001 ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.' },
)
assert.deepEqual(auftragMeldung(null), { tone: 'ok', text: 'Die Auftragsbestätigung wurde gespeichert.' })
})
```
- [ ] **Step 2: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
Expected: FAIL in den vier neuen Tests (`kundeMitKundenart is not a function` und Verwandte); die neun aus Task 5 bestehen weiter.
- [ ] **Step 3: Die drei Funktionen implementieren**
In `src/lib/auftraegeAnsicht.js` `import { invoiceDoneMessage } from './financeView'` ergänzen. Die erlaubten Kundenarten sind zwei Konstanten `'unternehmen'` und `'privat'` (Wertebereich von `customers.kundenart` nach V1; bewusst lokal, `auftragView.js` gehört W2). `auftragMeldung` prüft der Reihe nach, mit `a = auftrag` (Objekt, sonst `{}`) und `name = a.nummer ? 'Auftragsbestätigung <nummer>' : 'Die Auftragsbestätigung'`:
1. `a.status === 'entwurf'` → `{ tone: 'ok', text: 'Der Entwurf wurde gespeichert.' }`
2. `a.mailStatus === 'fehler'` → `{ tone: 'warn', text: '<name> ist versendet, die Mail an den Kunden wurde aber nicht zugestellt. Bitte in der Liste erneut senden.' }`
3. `a.nummer` → `{ tone: 'ok', text: '<name> wurde[ an <gesendetAn>] gesendet.' }` (der Teil „an …“ nur bei nicht leerem `gesendetAn`)
4. sonst → `{ tone: 'ok', text: 'Die Auftragsbestätigung wurde gespeichert.' }`
- [ ] **Step 4: Test laufen lassen**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && NODE_OPTIONS=--max-old-space-size=1024 timeout 600 nice -n 10 node --test tests/unit/auftraegeAnsicht.test.mjs`
Expected: PASS, `pass 13`, `fail 0`.
- [ ] **Step 5: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/lib/auftraegeAnsicht.js tests/unit/auftraegeAnsicht.test.mjs
git commit -m "Aufträge und Rechnungen: frische Kundenart und Rückmeldungen nach Rechnung und Auftrag (mit Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: Platzhalter und Container
**Files:**
- Create: `src/components/auftraege/ProjektkontoListe.jsx`, `RechnungDialog.jsx`, `AuftragListe.jsx`, `AuftragDialog.jsx`, `AuftraegeUndRechnungen.jsx`
**Interfaces:**
- Consumes: `Card`, `Button`, `EmptyState` (`../ui`), `AddressWarning` und `InvoiceTable` (`../finance/…`, Default-Exporte), `useFinanceResource` und `useScrollIntoView` (`../../hooks/…`, benannte Exporte), `getProjektkonten` und `listAuftraege` (`../../lib/financeApi`, Task 3), `customerInvoicesGate` (`../../lib/financeView`), `noticeProps` (`../../lib/financeUi`), alles aus `../../lib/auftraegeAnsicht` (Task 5 und 6).
- Produces (Default-Exporte, Dateiname = Komponentenname, Props nach V8):
- `AuftraegeUndRechnungen({ customer, onShowOverview })`
- `ProjektkontoListe({ konten, ohneProjekt, auftraegeAktiv, stripeOk, onRechnung, onAuftrag })`
- `RechnungDialog({ open, onClose, onCreated, kunde, projektId, auftraegeAktiv, onShowOverview })`
- `AuftragListe({ auftraege, konten, onChanged, onBearbeiten, onShowOverview })`
- `AuftragDialog({ open, onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview })`
Die Props entsprechen V8 nach den Nachträgen der Planrunde: Der Container reicht `konten` und `onShowOverview` an `AuftragListe` und `AuftragDialog` durch und `onShowOverview` (optional) auch an `RechnungDialog`.
- [ ] **Step 1: Vier Platzhalter schreiben**
Jede Datei ist `export default function <Dateiname>({ … })` mit der Prop-Signatur aus den Interfaces (destrukturiert, auch wenn nichts davon benutzt wird; sie dokumentiert den Vertrag) und gibt `null` zurück (kein React-Import nötig), mit einem Kommentarkopf in genau diesem Sinn (Platzhalter von W0, Strang W1 bzw. W2 ersetzt die Datei vollständig, Dateiname, Default-Export und Props bleiben):
- `ProjektkontoListe.jsx` (W1): `projektId` in `onRechnung(projektId)` und `onAuftrag(projektId)` ist die Kennung des Projekts, `null` die Zeile „Ohne Projekt“. Die fertige Komponente rendert ihren Abschnitt selbst: `<Card title="Projekte">` (ein `<section>`; den Abstand zu den Nachbarn liefert `.page section + section`).
- `RechnungDialog.jsx` (W1): `kunde` ist das Kundendokument oder `null` (Kundenwahl im Dialog); `projektId`: `undefined` = keine Vorauswahl, `null` = „Ohne Projekt“, Text = dieses Projekt; `auftraegeAktiv` ist nur der Startwert (danach gilt `getProjektkonten`, das der Dialog selbst lädt); `onShowOverview` ist optional („Zur Übersicht“ in der Adresswarnung); `onCreated(rechnung, info)`: `info` unverändert weiterreichen wie im bisherigen `InvoiceCreateModal` (`{ senden }`, bei festgeschriebener, nicht versendeter Rechnung `{ senden: true, notice, tone: 'warn', finalizedFilter }`), danach ruft der Dialog `onClose()`. Der Dialog sitzt zusätzlich in `InvoiceTable.jsx` (dort ersetzt W1 das Modal), `FinancePage.jsx` bleibt unverändert.
- `AuftragListe.jsx` (W2): `auftraege` sind `AuftragTeam[]` (V3); `konten` liefern den Projektnamen der Zeile und die Auswahl für „Projekt zuordnen“ (Aktion für ABs ohne Projekt, ruft `ordneAuftragProjektZu(id, projektId)` aus `financeApi.js`); `onShowOverview` ist der Weg „Zur Übersicht“ aus Meldungen. Die fertige Komponente rendert `<Card title="Aufträge">` selbst und ruft nach jeder Aktion `onChanged()`; „Entwurf bearbeiten“ ruft `onBearbeiten(auftrag)`, nach „Neue Fassung“ ruft sie `onBearbeiten(neuerEntwurf)`.
- `AuftragDialog.jsx` (W2): `kunde` kommt mit aktueller `kundenart` (der Container gleicht sie über `kundeMitKundenart` ab); `projektId` ist das vorausgewählte Projekt oder `null` (freier Titel); `auftrag` der Entwurf zum Bearbeiten oder `null`; `konten` die Projektkonten für die Projektwahl; `onShowOverview` der Weg „Zur Übersicht“; nach Speichern oder Senden ruft der Dialog `onSaved(auftrag)` (`AuftragTeam`, mit `mailStatus`) und danach `onClose()`. PDF-Vorschau über `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`.
- [ ] **Step 2: Container `AuftraegeUndRechnungen.jsx` schreiben**
Aufbau und Regeln (ersetzt `CustomerInvoices`; deutscher Kopfkommentar zu Aufgabe und Reihenfolge):
- **Außen** `export default function AuftraegeUndRechnungen({ customer, onShowOverview })`: Ist `customerInvoicesGate(customer).ok` falsch, kommt wie bisher `EmptyState` mit `gate.title`, `gate.hint` und (nur mit `onShowOverview`) `<Button variant="primary" onClick={onShowOverview}>Daten erfassen</Button>`; es werden keine Abrufe gestartet. Sonst rendert sie die innere Komponente `AuftraegeInhalt` (die Hooks stehen erst dort).
- **Innen** `AuftraegeInhalt({ customer, onShowOverview })`:
- `const res = useFinanceResource(() => ladeAuftraegeAnsicht(customer.$id, { getProjektkonten, listAuftraege }), KONTEN_FEHLER)`; `const ansicht = ansichtsZustand(res)`; `const reload = res.reload`.
- Zustand: `meldung` (`{ tone, text }` oder `null`), `rechnungsDialog` (`{ projektId }` oder `null`), `auftragsDialog` (`{ projektId, auftrag }` oder `null`), `listeToken` (Zahl, Start 0), dazu `meldungRef` mit `useScrollIntoView(meldungRef, meldung)`.
- `const kunde = kundeMitKundenart(customer, ansicht.kunde)` (geht an beide Dialoge).
- Öffnen eines Dialogs setzt zuerst `meldung` auf `null`.
- `RechnungDialog.onCreated(rechnung, info)`: Dialogzustand `null`, `setMeldung(erstelltMeldung(rechnung, info))`, `setListeToken((n) => n + 1)`, `reload()`. `AuftragDialog.onSaved(auftrag)`: Dialogzustand `null`, `setMeldung(auftragMeldung(auftrag))`, `reload()`. `onClose` setzt den jeweiligen Zustand auf `null` (der Dialog ruft `onCreated` und danach `onClose`; die Handler sind in jeder Reihenfolge und mehrfach unschädlich).
- **Reihenfolge der Ausgabe** (ein Fragment; die Abschnitte sind `<section>`, alles andere steht davor, damit `.page section + section` überall den Abstand liefert):
| Nr. | Element | Bedingung | Umsetzung |
|---|---|---|---|
| a | Adresswarnung | immer | `<AddressWarning customer={customer} onShowOverview={onShowOverview} variant="page" />` |
| b | Rückmeldung | `meldung` | `<p ref={meldungRef} {...noticeProps(meldung.tone, 'fin-notice')}>{meldung.text}</p>` |
| c | Nachladefehler | `projekteStatus === 'bereit'` und `nachladeFehler` | `<p {...noticeProps('warn', 'fin-notice')}>Die Übersicht konnte nicht neu geladen werden: {nachladeFehler}{' '}<button type="button" className="link" onClick={reload}>Erneut versuchen</button></p>` |
| d | Projekte | `laden` | `<Card title="Projekte"><div className="loading" role="status">Projekte werden geladen …</div></Card>` |
| | | `fehler` | `<Card title="Projekte"><div className="fin-load-error"><p className="msg warn" role="alert">{fehler}</p><Button onClick={reload}>Erneut versuchen</Button></div></Card>` |
| | | `bereit` | `<ProjektkontoListe konten ohneProjekt auftraegeAktiv stripeOk onRechnung onAuftrag />` mit den Werten aus `ansicht`; `onRechnung(projektId)` öffnet den Rechnungsdialog, `onAuftrag(projektId)` den Auftragsdialog mit `auftrag: null` |
| e | Aufträge | `auftraegeStatus === 'bereit'` | `<AuftragListe auftraege={ansicht.auftraege} konten={ansicht.konten} onChanged={reload} onBearbeiten={(auftrag) => … Auftragsdialog mit projektId: auftrag.projektId \|\| null und dem Entwurf} onShowOverview={onShowOverview} />` |
| | | `'fehler'` | `<Card title="Aufträge">` mit demselben `fin-load-error`-Block wie bei Projekten, Text `ansicht.auftraegeFehler` |
| | | `'aus'` | nichts |
| f | Rechnungen | immer | `<InvoiceTable customer={customer} onShowOverview={onShowOverview} refreshToken={listeToken} onChanged={reload} />` (nur diese bisherigen Props) |
| g | Dialoge | Zustand gesetzt | `<RechnungDialog open … kunde={kunde} projektId={rechnungsDialog.projektId} auftraegeAktiv={ansicht.auftraegeAktiv} onShowOverview={onShowOverview} />` bzw. `<AuftragDialog open … kunde={kunde} projektId={…} auftrag={…} konten={ansicht.konten} onShowOverview={onShowOverview} />`, nur gemountet, solange der Zustand gesetzt ist (frischer Zustand bei jedem Öffnen) |
- Regeln dazu: Fehlt `.fin-load-error` oder `.fin-notice` im CSS (Task 1 Step 5), entfallen diese beiden Zusatzklassen. Die Dialoge stehen als Geschwister hinter den Abschnitten, **nie** innerhalb von `InvoiceTable` oder `.fin-body` (dessen `container-type` würde `position: fixed` verbiegen). `InvoiceTable` behält seinen eigenen Knopf „Rechnung schreiben“; der Container fasst ihn nicht an. Kein `style`, keine neue CSS-Datei, keine Icons.
- [ ] **Step 3: Syntaxprüfung**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
node_modules/.bin/esbuild src/components/auftraege/*.jsx --loader:.jsx=jsx --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
```
Expected: keine Ausgabe, Exit 0 (reine Syntaxprüfung; den Import-Graphen prüft der Build in Task 9).
- [ ] **Step 4: Struktur gegen die Tabelle prüfen (Review Focus 1 bis 3)**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
rg -n "<InvoiceTable|<AddressWarning|<ProjektkontoListe|<AuftragListe|<RechnungDialog|<AuftragDialog|useFinanceResource\(|ansichtsZustand\(|kundeMitKundenart\(" src/components/auftraege/AuftraegeUndRechnungen.jsx
rg -n "konten=\{ansicht\.konten\}|onShowOverview=\{onShowOverview\}" src/components/auftraege/AuftraegeUndRechnungen.jsx
```
Expected: im ersten Befehl mindestens ein Treffer je Muster. Im zweiten mindestens dreimal `konten={ansicht.konten}` (`ProjektkontoListe`, `AuftragListe`, `AuftragDialog`) und mindestens fünfmal `onShowOverview={onShowOverview}` (`AddressWarning`, `InvoiceTable`, `RechnungDialog`, `AuftragListe`, `AuftragDialog`). Beim Lesen der Datei gegenprüfen: `<InvoiceTable` und `<AddressWarning` stehen **nicht** in einem Ausdruck, der von `projekteStatus` oder `auftraegeStatus` abhängt (die Rechnungsliste bleibt bei jedem Ausfall der Projektkonten und Aufträge bedienbar); `<AuftragListe` steht nur im Fall `auftraegeStatus === 'bereit'`; es gibt keinen Aufruf von `listAuftraege` außerhalb von `ladeAuftraegeAnsicht`.
- [ ] **Step 5: Hygiene**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && rg -n "react-icons|@tabler|style=\{\{|ui-btn|form-control|form-group|form-label|ui-card|badge-|overlay-content|overlay-close|spinner|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(|console\.log" src/components/auftraege src/lib/auftraegeAnsicht.js src/lib/auftraegeEndpoints.js`
Expected: keine Treffer.
- [ ] **Step 6: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/components/auftraege/ProjektkontoListe.jsx src/components/auftraege/RechnungDialog.jsx src/components/auftraege/AuftragListe.jsx src/components/auftraege/AuftragDialog.jsx src/components/auftraege/AuftraegeUndRechnungen.jsx
git commit -m "Aufträge und Rechnungen: Container des Kundenreiters und vier Platzhalter für W1 und W2" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Einbindung im Kundenreiter, `CustomerInvoices` entfällt
**Files:**
- Modify: `src/pages/CustomerDetailPage.jsx`
- Delete: `src/components/finance/CustomerInvoices.jsx`
**Interfaces:**
- Consumes: `AuftraegeUndRechnungen({ customer, onShowOverview })` (Task 7).
- Produces: Der Reiter `invoices` der Kundenseite zeigt den Container. Die Adresswarnung (`AddressWarning variant="page"`) und die Sperre „Noch ein Lead“ usw. stecken jetzt im Container und bleiben sichtbar. `AddressWarning.jsx` selbst bleibt unverändert (Nutzer: der Container und der Rechnungsdialog); `CustomerInvoices.jsx` hat danach keinen Nutzer mehr und wird gelöscht.
- [ ] **Step 1: Seite umstellen**
In `src/pages/CustomerDetailPage.jsx`:
- Den Import `import CustomerInvoices from '../components/finance/CustomerInvoices'` ersetzen durch `import AuftraegeUndRechnungen from '../components/auftraege/AuftraegeUndRechnungen'`.
- Die Verwendung `{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}` ersetzen durch `{activeTab === 'invoices' && <AuftraegeUndRechnungen key={customer.$id} customer={customer} onShowOverview={() => setTab('overview')} />}` (der `key` sorgt dafür, dass ein anderer Kunde nie den Zustand eines früheren erbt).
- Sonst bleibt die Datei unverändert (laut Übersicht ändert W0 nur Import und Reiterzeile; das Feld Kundenart in der Übersicht gehört W2).
- [ ] **Step 2: Alte Datei entfernen**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
rg -n "CustomerInvoices" src tests
git rm src/components/finance/CustomerInvoices.jsx
rg -n "CustomerInvoices" src tests
```
Expected: Vor dem Löschen Treffer nur in `CustomerInvoices.jsx` selbst (und ggf. in Kommentaren); danach keine Treffer. Bleibt ein Import oder eine Verwendung stehen, wird sie umgestellt, bevor gelöscht wird.
- [ ] **Step 3: Syntax und Anker**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
node_modules/.bin/esbuild src/pages/CustomerDetailPage.jsx --loader:.jsx=jsx --outdir=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad/auftraege-w0-syntax --log-level=warning
rg -n "AuftraegeUndRechnungen" src/pages/CustomerDetailPage.jsx
```
Expected: esbuild ohne Ausgabe; zwei Treffer (Import und Verwendung).
- [ ] **Step 4: Commit**
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
git add src/pages/CustomerDetailPage.jsx
git commit -m "Kundenseite: Reiter „Aufträge und Rechnungen“ zeigt den neuen Container, CustomerInvoices entfällt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
(Die mit `git rm` vorgemerkte Löschung gehört in denselben Commit.)
### Task 9: Selbstprüfung
**Files:** keine Änderung, außer Korrekturen aus den Prüfungen.
**Interfaces:**
- Consumes: alle Ergebnisse der Tasks 1 bis 8.
- Produces: die Abschlussmeldung an den Orchestrator (Step 6). Von diesem Stand legt der Orchestrator die Worktrees von W1 und W2 an.
- [ ] **Step 1: Volle Suite (hinter dem `flock`)**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
free -m | head -2
flock $L/auftraege-schwer.lock env NODE_OPTIONS=--max-old-space-size=1024 timeout 900 nice -n 10 npm test > $L/auftraege-w0-test.log 2>&1; echo "exit $?"
rg "^(ℹ|#) (tests|pass|fail) " $L/auftraege-w0-test.log
```
Expected: `exit 0`, `fail 0`, `pass` = `N0 + 21` (4 Blob, 4 Endpunkte, 13 Ansichtslogik; `customerTabs` ändert nur eine Erwartung).
- [ ] **Step 2: Build (hinter dem `flock`, mit Speichergrenze)**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
L=/tmp/claude-0/-root/dc97e1da-8b3d-40c0-9943-f8c37f910877/scratchpad
free -m | head -2
flock $L/auftraege-schwer.lock systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 nice -n 10 node node_modules/vite/bin/vite.js build --outDir $L/auftraege-w0-dist --emptyOutDir > $L/auftraege-w0-build.log 2>&1; echo "exit $?"
tail -8 $L/auftraege-w0-build.log
```
Expected: `exit 0`, die letzten Zeilen nennen die gebauten Dateien und „built in …“, keine Fehlermeldung zu fehlenden Exporten oder Importen. Schlägt der Build fehl, wird die Ursache in der Datei des zuständigen Tasks korrigiert (neuer Commit „Korrektur: …“), danach Step 1 und 2 wiederholt.
- [ ] **Step 3: Vertragsliste abhaken (V8)**
Run:
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
for n in getProjektkonten getProjektkonto previewInvoice listAuftraege getAuftrag createAuftrag patchAuftrag deleteAuftrag sendAuftrag erinnereAuftrag neueFassungAuftrag storniereAuftrag ordneAuftragProjektZu getAuftragPdf; do rg -q "\b$n\b" src/lib/financeApi.js && echo "ok $n" || echo "FEHLT $n"; done
rg -n "export const createInvoice|export const getAbgerechnet" src/lib/financeApi.js src/lib/teamApi.js
for f in AuftraegeUndRechnungen ProjektkontoListe RechnungDialog AuftragListe AuftragDialog; do rg -c "export default function $f\b" src/components/auftraege/$f.jsx; done
rg -n "label: 'Aufträge und Rechnungen'" src/lib/customerTabs.js
rg -n "AuftraegeUndRechnungen" src/pages/CustomerDetailPage.jsx
```
Expected: 14-mal `ok …`, `createInvoice` und `getAbgerechnet` gefunden, fünfmal `1`, die Beschriftung und die Einbindung gefunden. Abgehakt wird außerdem (aus den Tests):
- [ ] `getAuftragPdf` liefert einen Blob (Task 3, Test „getAuftragPdf …“); `ordneAuftragProjektZu` sendet `POST …/auftraege/<id>/projekt` mit `{ projektId }` (Task 3, Test „Pfad, Methode, Kopf und Körper …“).
- [ ] Der Container hat die Props `{ customer, onShowOverview }`, die Platzhalter die Props aus V8 samt `konten` und `onShowOverview`.
- [ ] Der Container reicht `konten` und `onShowOverview` an `AuftragListe` und `AuftragDialog` durch, `onShowOverview` auch an `RechnungDialog` (Task 7, Step 4).
- [ ] `AddressWarning variant="page"` steht im Container (Task 7, Zeile a) und `CustomerInvoices` ist weg (Task 8).
- [ ] Der Reiter bleibt nur für Admins (Test `customerTabs`).
- [ ] **Step 4: Hygiene der neuen Dateien**
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-auftraege && rg -n "react-icons|@tabler|style=\{\{|ui-btn|form-control|form-group|form-label|ui-card|badge-|overlay-content|overlay-close|spinner|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(|console\.log" src/components/auftraege src/lib/auftraegeAnsicht.js src/lib/auftraegeEndpoints.js`
Expected: keine Treffer.
- [ ] **Step 5: Dateiliste und Git-Zustand**
Run (mit `B` aus Task 1):
```bash
cd /home/webklar/apps/ticket.webklar.com/dev-auftraege
B=<B aus Task 1>
git diff --name-status "$B"..HEAD
git log --oneline "$B"..HEAD
git status --short
```
Expected: genau diese 17 Pfade (Reihenfolge egal): neu `src/components/auftraege/{AuftraegeUndRechnungen,ProjektkontoListe,RechnungDialog,AuftragListe,AuftragDialog}.jsx`, `src/lib/auftraegeAnsicht.js`, `src/lib/auftraegeEndpoints.js`, `tests/unit/{auftraegeAnsicht,auftraegeEndpoints,financeRequestBlob}.test.mjs`; geändert `src/lib/{customerTabs,financeApi,financeRequest,teamApi}.js`, `src/pages/CustomerDetailPage.jsx`, `tests/unit/customerTabs.test.mjs`; gelöscht `src/components/finance/CustomerInvoices.jsx`. Sieben Commits (plus eventuelle „Korrektur“-Commits), `git status --short` leer. Nichts gepusht.
- [ ] **Step 6: Abschlussmeldung an den Orchestrator**
Die Antwort nennt: Basis `B` und `N0`, letzten Commit (`git rev-parse --short HEAD`), Testzahl und Build-Ergebnis, die 17 Pfade, Abweichungen von diesem Plan samt Grund, und den Hinweis: „`node_modules` im Worktree ist echt und kann von W1 und W2 verlinkt werden.“ Dazu der Hinweis, dass der Container V8 samt Nachträgen der Planrunde umsetzt (Kindkomponenten rendern ihre `Card` selbst; `projektId` `undefined` = keine Vorauswahl, `null` = „Ohne Projekt“; `info` von `onCreated` unverändert; `konten` und `onShowOverview` durchgereicht).
---
## Vertragsfragen
Alle Fragen aus der Erstfassung dieses Plans sind **entschieden (siehe Übersicht, Nachträge nach der Planrunde, V5 und V8)**; die Übersicht gilt. Die Liste hält fest, was entschieden wurde und wo dieser Plan es umsetzt.
1. **Wer öffnet „Rechnung schreiben“ im Kundenreiter?** Entschieden (siehe Übersicht, Nachträge): `RechnungDialog` sitzt in `InvoiceTable.jsx` (dort ersetzt W1 das Modal), `FinancePage.jsx` bleibt unverändert. Umsetzung: Der Container öffnet `RechnungDialog` nur für die Projektzeilen und nutzt `InvoiceTable` mit seinen bisherigen Props (`customer`, `onShowOverview`, `refreshToken`, `onChanged`); der Dialog lädt Projektliste und Schalter selbst über `getProjektkonten` (`auftraegeAktiv` ist nur der Startwert). Es bleibt dabei: Nach dem Anlegen über eine Projektzeile behält die Liste ihren Filter, die Meldung des Containers nennt Entwurf oder Rechnung (Task 6 und 7); ein Entwurf liegt dann unter „Entwürfe“.
2. **`projektId` im `RechnungDialog`.** Entschieden (siehe Übersicht, Nachträge; V8): `undefined` = keine Vorauswahl (Kartenknopf in `InvoiceTable`), `null` = „Ohne Projekt“ (Zeile „Ohne Projekt“), Text = dieses Projekt. Umsetzung: Der Container reicht die Kennung aus `onRechnung(projektId)` unverändert durch (Task 7, Zeile g).
3. **`onCreated(rechnung, info)`.** Entschieden (siehe Übersicht, Nachträge; V8): `info` unverändert weiterreichen (`senden`, `notice`, `tone`, `finalizedFilter`). Umsetzung: `erstelltMeldung` wertet `notice` und `tone` aus (Task 6).
4. **Abschnittsüberschriften.** Entschieden (siehe Übersicht, Nachträge; V8): Die Kindkomponenten rendern ihre `Card` samt Titel selbst. Umsetzung: Kommentare der Platzhalter und Container (Zustände „laden“ und „fehler“ mit denselben Titeln, Task 7).
5. **PDF öffnen (W2).** Entschieden (siehe Übersicht, Nachträge; V8): `openInNewTab(async () => URL.createObjectURL(await getAuftragPdf(id)))`. Umsetzung: `getAuftragPdf` liefert den Blob (Task 2 und 3); das Öffnen gehört W2 (Hinweis im Kommentar von `AuftragDialog.jsx`).
6. **Testbefehl.** Entschieden (siehe Übersicht, Nachträge; Tech Stack): Die WOMS-Tests laufen mit `npm test` (`tests/unit/*.test.mjs`). Umsetzung: Task 1 Step 6 und Task 9 Step 1.
7. **Form von `abgerechnet`.** Entschieden (siehe Übersicht, Nachträge; V5): `{ abgerechnet: { <worksheetId>: '<Rechnungsnummer>' | 'Entwurf' } }`, für ein unbekanntes Ticket `{ abgerechnet: {} }`. W0 folgt V5 (Task 3); Spec Abschnitt 6 nennt die kürzere Form. Das Abzeichen für `'Entwurf'` legt W1 fest.
8. **Dateizuständigkeit WOMS.** Entschieden (siehe Übersicht, Nachträge, Absatz „WOMS“): W0 besitzt API-Schicht (`financeApi.js`, `financeRequest.js`, `teamApi.js`, `auftraegeEndpoints.js`), `auftraegeAnsicht.js`, `customerTabs.js`, den Container samt vier Platzhaltern, die Reiterzeile in `CustomerDetailPage.jsx` und die Löschung von `CustomerInvoices.jsx`; W1 die Projektliste, den Rechnungsdialog, `InvoiceTable.jsx`, `WorksheetList.jsx`, `WorksheetTimesModal.jsx` und die Löschung von `InvoiceCreateModal.jsx`; W2 die Auftragsliste, den Auftragsdialog, das Feld Kundenart in `CustomerDetailPage.jsx` und `SettingsCard.jsx`. W2 startet nach W0, also gibt es in `CustomerDetailPage.jsx` keinen Konflikt.
9. **Basis und Push-Reihenfolge.** Entschieden (siehe Übersicht, Nachträge): `feature/auftraege` (WOMS) enthält alle Redesign-Commits bis zum Basis-Tag `B`. Der Nutzer pusht erst, wenn das Redesign auf `test` liegt; vorher wird der Zweig auf `origin/test` aktualisiert (Z2). Die Übergabe nennt `B`; Task 1 Step 2 meldet, wenn `origin/test` weiter ist als `B`.
10. **Prüfwerkzeug des Redesigns.** Entschieden (siehe Übersicht, Nachträge): Das Redesign-Werkzeug wird nicht geändert; Bildschirmfotos entstehen mit eigenen Skripten im Scratchpad (W1, W2, Z2). W0 macht keine Bildschirmfotos und fasst das Werkzeug nicht an.
11. **Kundenart, Schreibweg.** Entschieden (siehe Übersicht, Nachträge; V5): Die AB-Dialoge (W2) schicken `kundenart` mit `POST` und `PATCH …/auftraege`, das Portal setzt sie am Kunden, wenn dort leer; die Übersicht (W2) schreibt das Feld bei Änderung direkt nach Appwrite. W0 gleicht in beiden Fällen über `kunde.kundenart` aus der Antwort von `getProjektkonten` ab (`kundeMitKundenart`, Task 6 und 7).
12. **`onShowOverview` im `RechnungDialog`.** Entschieden (siehe Übersicht, Nachträge; V8): optionale Prop des Dialogs. Umsetzung: Der Container reicht sie durch (Task 7, Zeile g).
Mit der Planrunde kamen zwei Festlegungen für W0 dazu, die dieser Plan umsetzt:
13. **`ordneAuftragProjektZu(id, projektId)`.** V5 (Zeile L3) hat die Route `POST …/auftraege/:id/projekt` mit `{ projektId }`; W2 braucht sie für „Projekt zuordnen“ (Spec: „später zuordenbar“). Umsetzung: Task 3 (Pfad in `auftraegeEndpoints.js`, Test, Export in `financeApi.js`).
14. **`konten` und `onShowOverview` für `AuftragListe` und `AuftragDialog`.** V8 (Nachträge): Der Container reicht beides durch (Projektname und Auswahl „Projekt zuordnen“ in der Liste, „Zur Übersicht“ in Meldungen). Umsetzung: Task 7 (Props der Platzhalter, Zeilen e und g, Step 4).
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,493 @@
# Buchungskalender (Funktion `unterkunft`) Implementation Plan
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home.
**Architecture:** Eigenes Modul `server/services/unterkunft/` nach dem Muster von Webshop (`shop`) und Terminplaner (`termine`). Ein serieller Kern (Strang 0) legt Schema, Konfiguration, reine Logik, Statusmaschine und **alle** Verdrahtungen in gemeinsam genutzten Dateien an; danach laufen vier Stränge parallel in eigenen Worktrees und berühren nur noch ihre eigenen Dateien (Ausnahmen sind je Task genannt). Abschluss: Zusammenführen, UI-Durchlauf, Review.
**Tech Stack:** Node 20/24 ESM, Express 4, Appwrite (node-appwrite, Server-Key), Stripe Connect (stripe ^19), nodemailer, Vanilla-ES-Module im Frontend, `node:test`, Playwright-core für UI.
**Spec:** `docs/superpowers/specs/2026-10-07-buchungskalender-design.md` (Abschnitte 1–16 + Nachträge N1–N6 in Abschnitt 17; N1–N6 gehen vor).
**Code-Fakten:** `/tmp/claude-0/-root/b360f353-cb78-442e-ada0-d24927a23169/scratchpad/plan/fakten-portal.md` und `fakten-woms.md` (vor jedem Task die genannten Muster dort nachschlagen; bei Abweichung zählt der Code).
## Global Constraints
- **Sicherheit (in JEDEN Agenten-Prompt kopieren):** Kein `rm -rf`, `rsync --delete`, `git reset --hard`, `git clean`, `git checkout -- .`, `git push`, kein `--force`; nichts außerhalb des eigenen Worktrees und des Scratchpads schreiben; keine destruktiven Proben; nie `/dev/null` o. ä. löschen oder ersetzen; nie Produktions-Container, `.env`, compose-Dateien oder Live-Datenbanken anfassen.
- **Speicher:** Server hat wenig RAM. Jeder Node-Testlauf: `flock /tmp/claude-0/uk-test.lock nice timeout 600 node --max-old-space-size=1024 --test <datei>`; nie `npm test` (alles parallel); UI-Läufe vorher `free -m`, nur wenn „available“ > 1500 MB.
- **node_modules:** Worktrees haben keins → `ln -s ../dev-termine/node_modules node_modules` (Portal), untracked lassen, **nie** `git add -A`, immer Dateien einzeln stagen. Keine neuen Abhängigkeiten.
- **Commits:** Autor `Kenso Grimm <kenso@webklar.com>`, Nachricht deutsch, endet mit `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Kein Push (macht der Inhaber).
- Funktionsschlüssel `unterkunft`; UI-Name „Buchungskalender“; Portal-Hash `#buchungen`; Fassung Rechtstexte `2026-10-07`.
- Schalter `UNTERKUNFT_ENABLED` (`off`|`pilot`|`on`), `UNTERKUNFT_PILOT_EMAILS` (Komma, `*` = alle).
- Stripe-Produkt `unterkunftSetup`: 19900 Cent, Lookup-Key `webklar_unterkunft_einrichtung_19900`, `priceLabel '199 €'`, `setupEstimate 'meist innerhalb von 1–2 Wochen'`.
- Collections: `ukUnterkuenfte`, `ukPreise`, `ukSperren`, `ukAnfragen`, `ukRechnungen`, `ukEinstellungen` (Dokument-ID = `featureId`). Kein Feld heißt `data`. JSON-Felder als Text.
- Datumswerte im Modul sind Kalendertage `YYYY-MM-DD` (Berlin); `bis` = Abreisetag, exklusiv. Nie mit lokaler Zeitzone rechnen; „heute“ über `termine/zeit.js` `heute()`.
- Geld immer ganze Cent (Integer), Bruttobeträge.
- Mail-IDs: G16–G22, K31–K34, T22 (vor Anlage gegen `server/mail-templates/` prüfen).
- Gast-Mails: Sie-Form, Absender `config.smtp.from` mit Anzeigename = Firma des Vermieters, Reply-To = E-Mail des Vermieters. K-Mails: Du-Form.
- Öffentliche Antworten enthalten nie Kunden-, Projekt-, Feature-, Anfrage-IDs (außer der Anfrage-ID im Buchungslink), keine E-Mails außer Anbieterdaten.
- Sperren nur über `withLocks([key], fn)` aus `server/services/keyedLock.js`; Schlüssel `unterkunft:<unterkunftId>` (Belegung/Status) und `uk-rechnungsnr:<featureId>`.
- Statuswerte exakt: `neu`, `freigegeben`, `gebucht`, `bezahlt`, `abgelehnt`, `abgelaufen`, `storniert`; `zahlart` ∈ `online|vorort`; `bezahltVia` ∈ `stripe|vorort`.
## Review Focus
1. **Sommerzeit/Datumsgrenzen:** Aufenthalt 2026-10-24 → 2026-10-26 (Zeitumstellung 25.10.) = genau 2 Nächte, Preis für 2 Nächte; Anreise heute (Berlin) ist erlaubt, gestern nicht. → Tests in Task 2 und Task 3.
2. **Gleichzeitige Anfragen für dieselben Nächte:** Zwei Anfragen überlappen; Freigabe der ersten klappt, Freigabe der zweiten scheitert mit `409 zeitraum_belegt`, ohne Mail; zwei gleichzeitige `bucheVerbindlich` derselben Anfrage ergeben genau eine Rechnung. → Tests in Task 4.
3. **iCal-Formate aus der Praxis:** Booking.com (`DTSTART;VALUE=DATE`), Airbnb, `DATE-TIME` mit `Z` oder `TZID`, gefaltete Zeilen, `STATUS:CANCELLED`, fehlendes `DTEND`, HTML-Fehlerseite statt ICS (→ Fehler, alter Stand bleibt). → Tests in Task 9.
4. **Buchungslink erneut geöffnet / doppelt geklickt:** nach Buchung zeigt die Seite den Status statt erneut „buchen“; Doppelklick erzeugt keine zweite Rechnung; abgelaufener Link → klare Meldung, keine Buchung; falsches Token → 404 ohne Unterschied zu unbekannter ID. → Tests in Task 7.
5. **Unsinnige Posten des Vermieters:** leere Postenliste, Gesamtsumme ≤ 0, Text leer/über 200 Zeichen, mehr als 30 Posten, ust ≠ 0 bei Kleinunternehmer → 400 mit Feldfehler, nichts gespeichert. → Tests in Task 2 und Task 8.
---
## Ablauf der Stränge
```
Strang 0 (seriell): Task 1 → 2 → 3 → 4 → 5 Branch feature/unterkunft (dev-unterkunft)
danach parallel, je eigener Worktree von feature/unterkunft:
Strang 1 Server : Task 6 → 7 → 8 → 9 → 10 → 11 feature/uk-server (dev-uk-server)
Strang 2 Oberfl. : Task 12 → 13 → 14 → 15 feature/uk-ui (dev-uk-ui) + WOMS feature/unterkunft
Strang 3 Widget : Task 16 → 17 → 18 feature/uk-widget (dev-uk-widget) + Donau-Repo
Strang 4 Mail/PDF : Task 19 → 20 → 21 feature/uk-mail (dev-uk-mail)
Abschluss (seriell): Task 22 (Merge + UI-Durchlauf) → Task 23 (Gesamtreview)
```
Stränge 2 und 3 bauen gegen die API-Verträge aus Task 5 (Routen + Antwortformen sind dort festgelegt) und testen mit Attrappen; echte Daten erst in Task 22. Worktree je Strang: `git -C /home/webklar/apps/kundenbereich.webklar.com/build worktree add -b feature/uk-<x> ../dev-uk-<x> feature/unterkunft` (Upstream danach mit `git branch --unset-upstream` lösen).
---
## Strang 0: Kern
### Task 1: Konfiguration, Schalter, Schema, Setup-Skript
**Files:**
- Modify: `server/config.js` (Env-Lesen neben `SHOP_ENABLED` ~Z.32; Produkt neben `shopSetup` ~Z.131 und Export ~Z.653; `collections` ~Z.612-636; neuer Block `unterkunft` neben `shop` ~Z.884)
- Create: `server/services/unterkunft/schalter.js`, `server/services/unterkunft/schema.js`, `scripts/unterkunft-setup.mjs`
- Modify: `test/support/schema.js` (`UNTERKUNFT_COLLECTIONS` in `schemaKeys`)
- Test: `test/unterkunftSchalter.test.js`, `test/unterkunftSetup.test.js`
**Interfaces:**
- Produces:
- `config.unterkunft = { mode, pilotEmails, fassung:'2026-10-07', formMinSekunden:3, minAntwortMs:300, icalIntervallMin:15, icalMaxBytes:1_000_000, icalTimeoutMs:10_000, icalFehlerStunden:6, horizontMonate:18, maxPosten:30, postenTextMax:200, nachrichtMax:2000, anfragenJeSiteStunde:30, ipHashTage:30, unverbindlichMonate:6, neuVerfallTage:60, anonymisierenJahre:3, rechnungJahre:8, loeschTageNachAbschalten:90 }` (Werte bei jedem Zugriff aus Env, Muster `config.shop`).
- `config.unterkunftSetup = { key:'unterkunftSetup', name:'Buchungskalender (Einrichtung)', amountCents:19900, lookupKey:'webklar_unterkunft_einrichtung_19900', oneTime:true, priceNickname }`.
- `config.collections.{ukUnterkuenfte,ukPreise,ukSperren,ukAnfragen,ukRechnungen,ukEinstellungen}` mit Env-Override `APPWRITE_COLLECTION_UK_*`.
- `schalter.js`: `unterkunftModus(): 'off'|'pilot'|'on'`, `unterkunftFuerKunde(email): boolean`.
- `schema.js`: `export const UNTERKUNFT_COLLECTIONS` (reine Daten, Muster `shop/schema.js`, Helfer `str/int/bool/dt/index`).
- `scripts/unterkunft-setup.mjs`: `export async function main({ check=false, client, log })`.
- [ ] **Step 1: Schalter-Test schreiben** (Muster `test/shopSchalter.test.js`): `off` bei leer/unbekannt; `pilot` → nur `UNTERKUNFT_PILOT_EMAILS` (case-insensitiv), `*` = alle; `on` → alle.
- [ ] **Step 2: Setup-Test schreiben** (Muster `test/shopSetup.test.js`): `--check` gegen leeren Mock meldet alle 6 Collections als FEHLT und schreibt nichts; ohne `check` legt es alle an, zweiter Lauf legt nichts an; jede Collection hat die Attribute aus Spec §3 + N2, u. a.:
- `ukUnterkuenfte`: featureId, customerId, projectId, name(120), beschreibung(500), maxPersonen int, mindestNaechte int, checkIn(5), checkOut(5), sort int, aktiv bool, icalImporte(4000), icalExportToken(64); Index featureId.
- `ukPreise`: unterkunftId, featureId, staffeln(2000), reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart(20), ustUebernachtung, ustReinigung, fristTage, stornobedingungen(5000); Index unterkunftId.
- `ukSperren`: unterkunftId, featureId, von(10), bis(10), quelle(60), notiz(200); Indizes unterkunftId, (unterkunftId,quelle).
- `ukAnfragen`: featureId, customerId, projectId, unterkunftId, nummer(12), von, bis, personen int, status(12), gast(3000), rechnungsadresse(1500), posten(8000), summen(1000), nachrichtVermieter(2000), fristAt dt, tokenHash(64), zahlart(8), bezahltVia(8), bezahltAt dt, stripeSessionId(100), stripePaymentIntentId(100), erstattetCent int, stornoGrund(500), stornobedingungenText(5000), rechtstexteFassung(20), ipHash(16), freigegebenAt/gebuchtAt/abgelehntAt/abgelaufenAt/storniertAt dt, rechnungId(36), anonymisiertAt dt; Indizes featureId, unterkunftId, (unterkunftId,status), status.
- `ukRechnungen`: featureId, anfrageId, art(8), nummer(24), datum(10), bruttoCent int, inhalt(20000); Indizes featureId, anfrageId, (featureId,nummer).
- `ukEinstellungen`: featureId, firma(150), inhaber(150), strasse(150), plz(10), ort(100), email(200), telefon(50), steuernummer(30), ustId(20), iban(34), bic(11), bank(100), rechnungsPraefix(8), rechnungZaehler(100), akzentfarbe(7), stripeAccountId(40), stripeStatus(4000), onlineZahlung bool, rechnungAuftragAt dt, anfrage(2000), anfrageStatus(16), anfrageAt dt, anfrageEntschiedenAt dt, anfrageEntschiedenVon(120), anfrageBegruendung(1000), icalFehlerMailAt dt.
- [ ] **Step 3: Tests laufen lassen, FAIL erwartet** (`flock … --test test/unterkunftSchalter.test.js test/unterkunftSetup.test.js`).
- [ ] **Step 4: Implementieren** (config-Block, Produkt, Collections, schalter.js, schema.js, Setup-Skript als Kopie des Shop-Musters mit `createAppwriteClient()` aus `scripts/analyse-setup.mjs`), `test/support/schema.js` ergänzen.
- [ ] **Step 5: Tests PASS**; zusätzlich `test/shopSetup.test.js` und `test/shopSchalter.test.js` weiter grün.
- [ ] **Step 6: Commit** `Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript`.
### Task 2: Preisrechnung
**Files:**
- Create: `server/services/unterkunft/datum.js`, `server/services/unterkunft/preis.js`
- Test: `test/unterkunftPreis.test.js`
**Interfaces:**
- Produces:
- `datum.js`: `istTag(s): boolean` (`/^\d{4}-\d{2}-\d{2}$/` + gültig), `naechte(von, bis): number` (UTC-Datumsarithmetik auf Tagesstrings), `plusTage(tag, n): string`, `tageVon(von, bis): string[]` (belegte Nächte, `bis` exklusiv), `formatTag(tag): '12.11.2026'`.
- `preis.js`:
- `berechnePreis({ preise, von, bis, personen }) → { naechte, posten:[{text, menge, einzelCent, ustProzent}], summen }`
- `summiere(posten, steuerart) → { bruttoCent, nettoCent, ust:[{ satz, nettoCent, steuerCent }] }` (bei `kleinunternehmer` `ust: []`, netto = brutto)
- `pruefePosten(posten, steuerart) → { posten } | { fehler: { posten: string } }`
- `formatEuro(cent) → '1.234,50 €'`
- `preiseVollstaendig(preise) → boolean`
- [ ] **Step 1: Failing tests** mit `PREISE = { staffeln:[{abNaechten:1,preisProNachtCent:6500},{abNaechten:7,preisProNachtCent:5500},{abNaechten:28,preisProNachtCent:4500}], reinigungCent:6000, personenInklusive:2, aufpreisPersonNachtCent:1000, direktrabattProzent:10, steuerart:'regel', ustUebernachtung:7, ustReinigung:7 }`:
- `naechte('2026-10-24','2026-10-26') === 2`; `naechte('2026-12-30','2027-01-02') === 3`.
- 2026-11-02→2026-11-09, 2 Personen: Posten `[Übernachtung 7×5500, Endreinigung 1×6000, Direktbuchungsrabatt 10 % 1×-3850]`, `bruttoCent 40650`, `ust [{satz:7, nettoCent:37991, steuerCent:2659}]`.
- 30 Nächte, 3 Personen: Übernachtung 30×4500, „Zusatzperson“ menge 30 × 1000, Reinigung 6000, Rabatt -16500 → `bruttoCent 184500`.
- 3 Nächte, 2 Personen: `bruttoCent 23550`.
- Unterschiedliche Sätze (ustReinigung 19): zwei `ust`-Einträge, Summe netto+steuer = brutto.
- `kleinunternehmer`: `ust: []`, `nettoCent === bruttoCent`.
- Rabatt 0 / Reinigung 0 → kein Posten dafür.
- `pruefePosten`: `[]` → Fehler; Summe 0 → Fehler; Text '' oder 201 Zeichen → Fehler; 31 Posten → Fehler; `ustProzent:7` bei `kleinunternehmer` → Fehler; `ustProzent` außerhalb {0,7,19} → Fehler; `menge` kein positiver Integer → Fehler; gültige Liste → `{posten}` mit getrimmten Texten.
- `formatEuro(123450) === '1.234,50 €'`, `formatEuro(-3850) === '−38,50 €'` (U+2212).
- [ ] **Step 2: FAIL** prüfen.
- [ ] **Step 3: Implementieren.** Staffel = höchste `abNaechten` ≤ `naechte` für alle Nächte (U11). Rabatt-Basis = Übernachtung + Zusatzpersonen, `-Math.round(basis*p/100)`, ust = `ustUebernachtung` (U12). USt je Satz: `netto = Math.round(bruttoSatz*100/(100+satz))`, `steuer = bruttoSatz - netto`. Texte: `Übernachtung, ${n} Nächte`, `Zusatzperson${k>1?'en':''} (${k}), ${n} Nächte`, `Endreinigung`, `Direktbuchungsrabatt ${p} %`.
- [ ] **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Preisrechnung und Datumshilfen`.
### Task 3: Speicher und Belegung
**Files:**
- Create: `server/services/unterkunft/speicher.js`, `server/services/unterkunft/belegung.js`
- Test: `test/unterkunftBelegung.test.js`
**Interfaces:**
- Consumes: `datum.js` (Task 2), `appwriteAdmin.js` (`listAllDocuments`, `getDocument`, `createDocument`, `updateDocument`, `deleteDocument`, `Query`).
- Produces:
- `speicher.js` (alle JSON-Felder geparst/serialisiert; Rückgaben sind „flache“ Objekte mit `$id`):
`ladeEinstellungen(featureId) → obj|null`, `speichereEinstellungen(featureId, felder) → obj` (legt an, falls fehlt),
`listeUnterkuenfte(featureId, { nurAktive=false }) → obj[]` (nach `sort`), `ladeUnterkunft(id) → obj|null`, `speichereUnterkunft(featureId, felder, id?) → obj`,
`ladePreise(unterkunftId) → obj|null`, `speicherePreise(unterkunftId, featureId, felder) → obj`,
`listeSperren(unterkunftId) → obj[]`, `legeSperreAn(...)`, `loescheSperre(id)`,
`ladeAnfrage(id) → obj|null`, `legeAnfrageDokAn(felder) → obj`, `aktualisiereAnfrage(id, felder) → obj`, `listeAnfragen(featureId, { status?: string[], unterkunftId? }) → obj[]`.
- `belegung.js`:
- `AKTIVE_STATUS = ['gebucht','bezahlt']`
- `belegteBereiche(unterkunftId, von, bis, { ohneAnfrage, now }) → [{ von, bis, quelle:'manuell'|'ical'|'reserviert'|'gebucht' }]` (Sperren + Anfragen `gebucht|bezahlt` + `freigegeben` mit `fristAt > now`; Ausschnitt auf [von,bis))
- `istFrei(unterkunftId, von, bis, { ohneAnfrage, now }) → boolean`
- `ueberschneidet(a, b) → boolean` (rein, Halboffen-Intervalle)
- [ ] **Step 1: Failing tests** (Appwrite-Mock mit `ukSperren`, `ukAnfragen`):
- `ueberschneidet({von:'2026-11-01',bis:'2026-11-05'},{von:'2026-11-05',bis:'2026-11-07'}) === false` (Abreise = Anreise ok); `…'2026-11-04'…` → true.
- Sperre manuell 11-10..11-12 → `istFrei(u,'2026-11-11','2026-11-13')` false; `'2026-11-12','2026-11-14'` true.
- Anfrage `freigegeben` mit `fristAt` in Zukunft blockiert; mit `fristAt` in Vergangenheit nicht; `neu`, `abgelehnt`, `abgelaufen`, `storniert` blockieren nicht; `gebucht`/`bezahlt` blockieren.
- `ohneAnfrage: id` ignoriert genau diese Anfrage.
- `belegteBereiche` liefert die Quelle `ical` für `quelle:'ical:abc'`.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren** (Abfragen per `Query.equal('unterkunftId', id)` + Status-Filter, `listAllDocuments`).
- [ ] **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Speicher und Belegung`.
### Task 4: Statusmaschine, Token, Rechnungsnummern
**Files:**
- Create: `server/services/unterkunft/fehler.js`, `token.js`, `rechnungen.js`, `ablauf.js`
- Create (Stubs, die die Stränge ersetzen): `server/services/unterkunft/mails.js`, `server/services/unterkunft/zahlung.js`
- Test: `test/unterkunftAblauf.test.js`, `test/unterkunftRechnungen.test.js`
**Interfaces:**
- Consumes: Tasks 1–3.
- Produces:
- `fehler.js`: `class UkFehler extends Error { constructor(status, code, felder = {}) }` — Codes u. a. `zeitraum_belegt` (409), `status_ungueltig` (409), `frist_abgelaufen` (410), `nicht_gefunden` (404), `ungueltig` (400), `mindestnaechte` (400), `zu_viele_personen` (400), `online_aus` (409).
- `token.js`: `neuesToken() → { token, hash }` (32 Byte base64url, sha256 hex), `tokenPasst(hash, token) → boolean` (timingSafeEqual), `formToken(siteKey, now)`, `pruefeFormToken(siteKey, token, now) → 'ok'|'zu_schnell'|'abgelaufen'|'ungueltig'` (Kopie `shop/token.js`, Label `'uk-form'`, Min `config.unterkunft.formMinSekunden`), `ipHashVon(ip)` (Präfix `uk-ip:`).
- `rechnungen.js`:
- `naechsteNummer(featureId, now) → 'RE-2026-0001'` (Sperre `uk-rechnungsnr:<featureId>`, Zähler `{jahr,n}` in `ukEinstellungen.rechnungZaehler`, Jahreswechsel → n=1; Präfix aus Einstellungen, Standard `RE`)
- `stelleAus(ctx, anfrage, { art:'rechnung'|'storno', bezugNummer?, now }) → rechnungDoc` (`inhalt` = `{ aussteller:{firma,inhaber,strasse,plz,ort,email,telefon,steuernummer,ustId,iban,bic,bank}, empfaenger: anfrage.rechnungsadresse, unterkunft:{name}, von, bis, naechte, personen, posten (bei storno: einzelCent negiert), summen, steuerart, zahlart, bezugNummer, anfrageNummer }`)
- `listeRechnungen(featureId) → doc[]`, `ladeRechnung(id) → doc|null`
- `mails.js` (Stub, Task 20 ersetzt den Inhalt, Signaturen bleiben): `anGast(art:'G16'|'G17'|'G18'|'G19'|'G20'|'G21'|'G22', ctx, anfrage, extra={}) → Promise<void>`, `anVermieter(template:'K31'|'K32'|'K33', ctx, values={}) → Promise<void>`; Stub protokolliert Aufrufe in `export const _aufrufe = []`.
- `zahlung.js` (Stub, Task 7 ersetzt): `erstatte(ctx, anfrage, betragCent, { now }) → Promise<{ erstattetCent }>`; Stub wirft `UkFehler(409,'online_aus')`, wenn `ctx.einstellungen.stripeAccountId` fehlt, sonst `{erstattetCent: betragCent}`.
- `ablauf.js` (jede Änderung unter `withLocks(['unterkunft:'+anfrage.unterkunftId])`, Status vor Schreiben frisch lesen):
- `ctx` = `{ featureId, customerId, projectId, siteKey, einstellungen }` (Task 5 liefert ihn)
- `legeAnfrageAn(ctx, { unterkunftId, von, bis, personen, gast:{name,firma,email,telefon,nachricht} }, { now, ipHash }) → anfrage` → prüft Tage, `von ≥ heute`, `bis ≤ heute+horizontMonate`, `mindestNaechte`, `maxPersonen`, `istFrei`; `nummer` = `A-` + 4 Zeichen `[A-Z2-9]`; Mails G16 + K31.
- `gibFrei(ctx, anfrageId, { posten?, fristTage?, nachricht?, now }) → { anfrage, token }` → nur aus `neu`; Posten = `posten` (über `pruefePosten`) oder `berechnePreis`; `istFrei(…, {ohneAnfrage})`; `fristAt = now + fristTage*24h`; `tokenHash`; `stornobedingungenText` + `rechtstexteFassung` als Schnappschuss; `ipHash` = ''; Mail G17 (`extra.token`).
- `lehneAb(ctx, anfrageId, { text, now }) → anfrage` → nur aus `neu`; `ipHash` = ''; Mail G18.
- `bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse, zahlart, now }) → { anfrage, rechnung }` → Token prüfen (sonst `nicht_gefunden`), Status `freigegeben` (bei `gebucht|bezahlt` mit gleichem Token: bestehendes Ergebnis zurückgeben, idempotent), `fristAt > now` (sonst `frist_abgelaufen`), `zahlart==='online'` nur mit `einstellungen.onlineZahlung && stripeAccountId`, Pflichtfelder Adresse (name|firma, strasse, plz, ort, land), `istFrei(…,{ohneAnfrage})`; → `gebucht`, `stelleAus(rechnung)`, `rechnungId`, Mails G19 + K32.
- `markiereBezahlt(ctx, anfrageId, { via, paymentIntentId?, now }) → anfrage` → aus `gebucht`; idempotent bei `bezahlt`; Mail G20 nur bei `via==='stripe'`.
- `storniere(ctx, anfrageId, { grund, erstattungCent=0, now }) → { anfrage, stornoRechnung|null }` → aus `freigegeben|gebucht|bezahlt`; Erstattung nur bei `bezahltVia==='stripe'`, 0 ≤ cent ≤ brutto − bereits erstattet, über `zahlung.erstatte`; Stornorechnung nur, wenn Rechnung existiert; Mail G22.
- `laufeFristenAb(now) → { abgelaufen: number }` → alle `freigegeben` mit `fristAt ≤ now` → `abgelaufen`, Mail G21.
- `UEBERGAENGE` (Objekt Status → erlaubte Ziele), `darfUebergang(von, nach) → boolean`.
- [ ] **Step 1: Failing tests Rechnungen:** erste Nummer `RE-2026-0001`, zweite `RE-2026-0002`; 10 parallele `naechsteNummer` → 10 verschiedene, lückenlos 1..10; Jahreswechsel (now 2027-01-01T00:30+01:00 Berlin) → `RE-2027-0001`; Präfix `DH` → `DH-2026-0001`; `stelleAus(storno)` negiert alle `einzelCent` und `summen.bruttoCent`, trägt `bezugNummer`.
- [ ] **Step 2: Failing tests Ablauf** (Appwrite-Mock, `mails._aufrufe` prüfen):
- Anfrage → `neu`, Nummer-Format `/^A-[A-Z2-9]{4}$/`, `_aufrufe` enthält G16 und K31.
- Anfrage in der Vergangenheit / unter Mindestnächten / mehr Personen als max / über belegte Nacht → `UkFehler` mit passendem Code, nichts gespeichert.
- Review Focus 2: Anfragen A und B überlappen; `gibFrei(A)` ok; `gibFrei(B)` → 409 `zeitraum_belegt`, keine G17 für B.
- `gibFrei` mit Posten `[{text:'Pauschale',menge:1,einzelCent:50000,ustProzent:7}]` → summen.bruttoCent 50000.
- `bucheVerbindlich` falsches Token → 404 `nicht_gefunden`; nach `fristAt` → 410; zweimal parallel mit richtigem Token → genau ein `ukRechnungen`-Dokument, beide Aufrufe liefern dieselbe `rechnung.nummer`.
- `zahlart:'online'` ohne Stripe-Konto → 409 `online_aus`.
- `laufeFristenAb` setzt `abgelaufen` und schickt G21; Zeitraum danach frei.
- `storniere` gebuchter Anfrage → Stornorechnung mit eigener Nummer, G22; `storniere` mit `erstattungCent` > brutto → 400.
- `darfUebergang('neu','bezahlt') === false`.
- [ ] **Step 3: FAIL.**
- [ ] **Step 4: Implementieren.**
- [ ] **Step 5: PASS.**
- [ ] **Step 6: Commit** `Buchungskalender: Statusmaschine, Token und Rechnungsnummern`.
### Task 5: Verdrahtung in gemeinsamen Dateien + API-Verträge
Dieser Task berührt alle geteilten Dateien, damit die Stränge danach konfliktfrei arbeiten. Router-Dateien entstehen hier als leere `express.Router()` mit Kommentar, welcher Task sie füllt.
**Files:**
- Modify: `server/config.js` (`features.unterkunft` neben `shop` ~Z.461-500), `server/services/featureRegistry.js` (`STANDARD_HAKEN.unterkunft`), `server/services/legalDocs.js` (`SAETZE.unterkunft = { lb:{slug:'leistungsbeschreibung-unterkunft', pdf:'Leistungsbeschreibung-Buchungskalender'}, avv:{slug:'avv-unterkunft', pdf:'AVV-Buchungskalender'}, widerruf: <gemeinsam> }`, `FUNKTION_SATZ`, `aktuelleFassung`), `server/services/stripeBilling.js` (PRODUCT_METADATA `unterkunftSetup:'unterkunft-einrichtung'`), `scripts/stripe-products.mjs` (KEYS), `server/routes/abo.js` (ORDERS, FEATURE_OF_ORDER `unterkunft:'unterkunft'`, Text 'Buchungskalender (Einrichtung)'), `server/routes/funktionen.js` (Antwort `unterkunft:{verfuegbar}`), `server/index.js` (Mounts + Job-Registrierung), `server/routes/shopWebhook.js` (Weiche `wkUk` vor Z.54, `charge.refunded` nach Metadaten trennen, `account.updated` auch an Uk), `server/services/termine/outbox.js` + `server/services/termine/schema.js` (optionales Feld `anhang` 500, wird nur gespeichert/durchgereicht), `public/dashboard.html` (Section `view-buchungen` mit `<div id="buchungen-body">`), `public/app.js` (`VIEW_HASHES.buchungen`, `viewRefreshers.buchungen = () => import('/buchungen.js').then(m => m.initBuchungen(document.getElementById('buchungen-body')))`, `APP_PARENT.buchungen='funktionen'`, `FEATURE_LABELS.unterkunft='Buchungskalender'`)
- Create: `server/services/unterkunft/kontext.js`, `server/services/unterkunft/jobs.js` (Gerüst), `server/routes/publicUnterkunft.js`, `server/routes/unterkunft.js`, `server/routes/team/unterkunft.js`, `server/routes/unterkunftSeite.js`, `server/routes/unterkunftWebhook.js` (exportiert `verarbeiteUkEreignis(event, now)`, leer), `public/buchungen.js` (Platzhalter `export function initBuchungen(el){ el.textContent = 'Buchungskalender wird geladen …' }`), `docs/funktionen/unterkunft-api.md` (API-Vertrag, s. u.)
- Test: `test/unterkunftWiring.test.js` (Muster `test/shopWiring.test.js`), Anpassung `test/featureRegistry.test.js`, `test/legalDocs.test.js`, `test/termineOutbox.test.js` falls Pins brechen
**Interfaces:**
- Produces:
- `kontext.js`: `unterkunftPerSiteKey(siteKey, { now }) → ctx|null` (30-s-Cache, Feature-Dokument `featureKey:'unterkunft'`, nur state `active`; im Modus `pilot` nur, wenn die Besitzer-E-Mail in der Pilotliste steht; Modus `off` → null), `ladeFuerProjekt(customerId, projectId) → ctx|null` (beliebiger state ≠ off), `leereCache(siteKey)`, `SITE_KEY_RE`. `ctx = { featureId, customerId, projectId, siteKey, state, besitzerEmail, einstellungen, project }`.
- `jobs.js`: `minutenJob(now)`, `viertelstundenJob(now)`, `taeglicherJob(now)` mit `einzeln()`-Wrapper; bei Modus `off` → `{aus:true}`. Minutenjob ruft schon `laufeFristenAb(now)`; die anderen sind leer (Task 9/10 füllen).
- Mounts in `server/index.js`: `app.use('/api/public/unterkunft/v1', publicUnterkunftRoutes)` vor dem globalen Parser (Z.77); `app.use('/api/unterkunft', unterkunftRoutes)` (Kunden-Session); Team-Router `router.use('/unterkunft', unterkunftTeamRoutes)` (wie Shop, index.js:28); `app.use(unterkunftSeiteRouter())` vor `express.static`. Jobs: 80 s Start/60 s, 140 s Start/15 min, 210 s Start/24 h.
- Register `config.features.unterkunft` = Kopie `shop` mit: `product:'unterkunftSetup'`, `label {name:'Buchungskalender', kurz:'Buchungskalender', keine:…}`, `priceLabel '199 €'`, `setupEstimate`, `consent` (items `unterlagen`, `verantwortung` [Text Spec §9], `rechnung` [„Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG). Für Richtigkeit von Preisen und Steuerangaben bin ich verantwortlich.“], `stripe`, `beginn`, `information`), `returnHash:'buchungen'`, `returnProjekt:true`, `ticket.topic = (p) => 'Buchungskalender einbauen – ' + name`, `ticket.details` → dyn. Import `services/unterkunft/ticketDetails.js` (Task 11), `requiresWartungsAbo:false`, `ticketFreischaltung:true`.
- `STANDARD_HAKEN.unterkunft = { verfuegbar: unterkunftFuerKunde, bestellpruefung: (args) => import('./unterkunft/anfrageFunktion.js').then(m => m.anfrageOffenePunkte(args)), unterlagenBereit: () => featureLegalReady('unterkunft'), belehrung: termineBelehrung, freischaltbar: (doc) => import('./unterkunft/freischaltung.js').then(m => m.freischaltbar(doc)) }` — die zwei Module entstehen in Task 11; bis dahin existieren sie als Stubs (`anfrageOffenePunkte → ['Anfrage noch nicht freigegeben']`, `freischaltbar → false`).
- **API-Vertrag** `docs/funktionen/unterkunft-api.md` (Stränge 2 und 3 bauen dagegen; Task 6–8 erfüllen ihn). Fehlerform überall `{ fehler: code, felder?: {feld: text} }`.
- Öffentlich (`/api/public/unterkunft/v1`):
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
- `GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD` → `{ belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' }` (Quellen nicht offengelegt).
- `POST /:siteKey/anfrage` Body `{ unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url }` → `201 { nummer }`.
- `GET /:siteKey/buchung/:id?t=` → `{ status, nummer, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }`.
- `POST /:siteKey/buchung/:id?t=` Body `{ rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart }` → `{ status:'gebucht', rechnungNummer, zahlungUrl? }` (bei `online` direkt Checkout-URL).
- `POST /:siteKey/buchung/:id/zahlung?t=` → `{ url }`.
- `GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno` → PDF.
- Kunde (`/api/unterkunft`, Session, `?projectId=`): `GET /status`; `GET|PUT /einstellungen`; `GET /unterkuenfte`; `POST /unterkuenfte`; `PUT|DELETE /unterkuenfte/:id`; `GET|PUT /unterkuenfte/:id/preise`; `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{von,bis,quelle,anfrageNummer?,sperreId?,notiz?}] }`; `POST /unterkuenfte/:id/sperren`; `DELETE /sperren/:id`; `POST /unterkuenfte/:id/ical-importe`; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`; `GET /anfragen?filter=offen|gebucht|alle`; `GET /anfragen/:id` (inkl. `vorschlag` = `berechnePreis`); `POST /anfragen/:id/freigeben` `{posten,fristTage,nachricht}`; `POST /anfragen/:id/ablehnen` `{text}`; `POST /anfragen/:id/bezahlt`; `POST /anfragen/:id/stornieren` `{grund, erstattungCent}`; `POST /preisvorschau` `{unterkunftId,von,bis,personen}`; `GET /rechnungen`; `GET /rechnungen/:id.pdf` (nur Einzel-PDF, kein ZIP — Spec N7); `POST /stripe/verbinden`, `POST /stripe/vorhandenes-konto`, `POST /stripe/aktualisieren`; `POST /anfrage-funktion` `{art,einheiten,portale,nachricht}`; `POST /anfrage-funktion/zurueckziehen`.
- Team (`/api/team/unterkunft`): `GET /:customerId`; `POST /:customerId/anfrage` `{projectId, entscheidung, begruendung}`.
- [ ] **Step 1: Wiring-Test schreiben:** Registereintrag vorhanden mit Produkt `unterkunftSetup` und `returnHash 'buchungen'`; `featureHaken('unterkunft')` hat alle 5 Haken; `FUNKTION_SATZ.unterkunft === 'unterkunft'`; `GET /api/public/unterkunft/v1/aaaaaaaaaaaaaaaa` → 200 `{aktiv:false}` (Stub-Router liefert das schon); `public/app.js` enthält `buchungen` in VIEW_HASHES (Quelltext-Prüfung); `dashboard.html` enthält `id="buchungen-body"`; Shop-Webhook mit `metadata.wkUk:'1'` ruft `verarbeiteUkEreignis` statt Shop-Logik.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren** inkl. API-Vertrag-Datei (Text oben 1:1).
- [ ] **Step 4: PASS**; zusätzlich grün: `test/shopWiring.test.js`, `test/featureRegistry.test.js`, `test/legalDocs.test.js`, `test/termineOutbox.test.js`, `test/funktionenRoutes.test.js`, `test/shopWebhook*.test.js` (einzeln nacheinander).
- [ ] **Step 5: Commit** `Buchungskalender: Verdrahtung, Router-Gerüste und API-Vertrag`.
- [ ] **Step 6: Worktrees der vier Stränge anlegen** (Befehl oben), je `node_modules`-Symlink.
---
## Strang 1: Server (`dev-uk-server`)
### Task 6: Öffentliche API – Konfiguration, Belegung, Anfrage
**Files:**
- Modify: `server/routes/publicUnterkunft.js`
- Test: `test/unterkunftPublic.test.js`
**Interfaces:**
- Consumes: `kontext.unterkunftPerSiteKey`, `ablauf.legeAnfrageAn`, `belegung.belegteBereiche`, `token.formToken/pruefeFormToken/ipHashVon`, `termine/limits.js` (`zaehle`, `darfAnfrage`, `ipSchluessel`).
- Produces: die drei öffentlichen Endpunkte `GET /:siteKey`, `GET /:siteKey/belegung`, `POST /:siteKey/anfrage` laut Vertrag.
- [ ] **Step 1: Failing tests:** unbekannter/inaktiver siteKey → 200 `{aktiv:false}`; aktiver → Unterkünfte nur `aktiv`, sortiert, ohne `featureId/customerId`; Belegung über 18 Monate hinaus → 400; Belegung fasst angrenzende Bereiche zusammen; `POST anfrage` gültig → 201 `{nummer}` + Dokument `neu`; Honeypot `kontakt_url` gefüllt → 201 mit Schein-Nummer, nichts gespeichert; Form-Token `zu_schnell` → ebenso stiller Erfolg; fehlende E-Mail → 400 `felder.email`; mehr als `anfragenJeSiteStunde` je Site → 429; CORS: `OPTIONS` von erlaubter Origin (Domain des Projekts) → 204 mit Origin, fremde Origin bei POST → 403; GET mit `Access-Control-Allow-Origin: *`.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren** (Helfer `route/schreibe/fehlerMit/erlaubteHerkunft/vorabantwort` aus `publicShop.js` in eine eigene lokale Kopie; eigener 16-KB-JSON-Parser; `UkFehler` → Status/Code).
- [ ] **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: öffentliche API für Kalender und Anfrage`.
### Task 7: Buchungsseite, Stripe-Zahlung, Webhook
**Files:**
- Modify: `server/routes/publicUnterkunft.js` (Buchungs-Endpunkte), `server/routes/unterkunftWebhook.js`, `server/services/unterkunft/zahlung.js` (Stub ersetzen), `server/services/shop/connect.js` (`erstatten(..., { webklar = 'shop' })` Parameter, Standard unverändert)
- Test: `test/unterkunftBuchung.test.js`
**Interfaces:**
- Consumes: `ablauf.bucheVerbindlich/markiereBezahlt`, `connect.checkoutAnlegen/sessionHolen/erstatten`, `rechnungen.ladeRechnung`, `rechnungPdf` aus Task 19 (bis zum Merge: `GET …/rechnung.pdf` darf in diesem Strang mit `501` antworten, wenn `server/services/unterkunft/rechnungPdf.js` fehlt — dynamischer Import mit Fallback, Test prüft nur 200/501).
- Produces:
- `zahlung.js`: `checkoutFuer(ctx, anfrage, { now }) → { url }` (Session auf `ctx.einstellungen.stripeAccountId`, `mode:'payment'`, ein Posten `Aufenthalt <Unterkunft> <von>–<bis>, Rechnung <Nr>` Betrag `summen.bruttoCent`, `payment_method_types` `['card','sepa_debit']` wenn SEPA im Status aktiv sonst `['card']`, `locale:'de'`, `expires_at` +30 min, `metadata:{wkUk:'1', anfrageId}`, `payment_intent_data.metadata` gleich, `success_url`/`cancel_url` = Buchungsseite `…/unterkunft/<siteKey>/buchung/<id>?t=<token>&wk_zahlung=ok|abbruch`, `idempotencyKey:'uk-zahlung-'+anfrageId+'-'+minute`), speichert `stripeSessionId`; `schliesseAbWennBezahlt(ctx, anfrage, { now }) → anfrage` (Session holen, bei `complete`+`paid` → `markiereBezahlt(via:'stripe')`); `erstatte(...)` echt mit `webklar:'unterkunft'`.
- `unterkunftWebhook.verarbeiteUkEreignis(event, now)`: `checkout.session.completed` (paid) / `async_payment_succeeded` → `markiereBezahlt`; `charge.refunded` → `erstattetCent` nachtragen; idempotent.
- [ ] **Step 1: Failing tests** (Fake-Stripe): `GET buchung` falsches Token und unbekannte ID → identisch 404; gültig → Vertrag-Antwort, `zahlarten` ohne `online`, wenn Stripe fehlt; `POST buchung` vor Ort → `{status:'gebucht', rechnungNummer:'RE-2026-0001'}`; Review Focus 4: zweites `POST` → gleiche Antwort, keine zweite Rechnung; `GET` danach zeigt `status:'gebucht'` und `rechnungNummer`; abgelaufene Frist → 410 `frist_abgelaufen`; `online` → Antwort enthält `zahlungUrl` vom Fake-Stripe; Webhook `checkout.session.completed` mit `metadata.wkUk` → `bezahlt` + G20 im Stub-Protokoll; Buchungsseite mit `wk_zahlung=ok` schließt ohne Webhook selbst ab; Webhook zweimal → ein Übergang.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren.**
- [ ] **Step 4: PASS**; `test/shopWebhook*.test.js` und Erstattungs-Tests des Shops weiter grün.
- [ ] **Step 5: Commit** `Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook`.
### Task 8: Kunden-API (Vermieter)
**Files:**
- Modify: `server/routes/unterkunft.js`
- Create: `server/services/unterkunft/verwaltung.js` (Validierung + Speichern von Einstellungen, Unterkünften, Preisen, Sperren; Stripe-Verbinden über `shop/connect.js` `kontoAnlegen`/`onboardingLink` mit `idempotencyKey 'uk-konto-'+projectId`)
- Test: `test/unterkunftVerwaltung.test.js`
**Interfaces:**
- Consumes: Tasks 1–4, `siteFeatures.assertOwnProject`, `writeLimiter.createWriteLimiter`, `shop/connect.js`; PDF-Download mit demselben 501-Fallback wie Task 7, bis Task 19 gemergt ist.
- Produces: alle Kunden-Endpunkte laut Vertrag außer `anfrage-funktion*` (Task 11). Validierung `verwaltung.js`: `pruefeEinstellungen(eingabe) → {werte}|{fehler}`, `pruefeUnterkunft`, `pruefePreise` (Staffeln aufsteigend, erste `abNaechten:1`, max 6 Staffeln, Preise 100–100000 Cent, Rabatt 0–50, Frist 1–14, Stornobedingungen 20–5000 Zeichen), `pruefeSperre`, `pruefeIcalUrl` (nur `https:`, max 500 Zeichen). `rechnungsPraefix` nur änderbar, solange `rechnungZaehler` leer.
- [ ] **Step 1: Failing tests:** fremdes Projekt → 403; Einstellungen ohne Steuernummer und ohne USt-ID → 400 `felder.steuernummer`; Präfix nach erster Rechnung ändern → 409; Unterkunft anlegen/sortieren/deaktivieren; Preise mit Staffel `abNaechten:2` als erste → 400; Kalender liefert Sperren mit `notiz` und Anfragen mit `anfrageNummer`; Sperre über gebuchte Nacht → 409 `zeitraum_belegt`; Review Focus 5 über `POST /anfragen/:id/freigeben` (leere Posten, Summe ≤ 0, Text 201 Zeichen, 31 Posten, ust 7 bei Kleinunternehmer → je 400, Anfrage bleibt `neu`); `GET /anfragen/:id` enthält `vorschlag`; `POST /preisvorschau` = `berechnePreis`; `POST /stripe/vorhandenes-konto` übernimmt `shopConfig.stripeAccountId` desselben Projekts, ohne eins → 409; `GET /rechnungen/:id.pdf` fremde Rechnung → 404.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren.**
- [ ] **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Verwaltungs-API für Vermieter`.
### Task 9: iCal-Import und -Export
**Files:**
- Create: `server/services/unterkunft/ical.js`, `server/services/unterkunft/icalAbgleich.js`
- Modify: `server/services/unterkunft/jobs.js` (`viertelstundenJob` → `gleicheAlleAb(now)`), `server/routes/unterkunftSeite.js` (Export-Route)
- Test: `test/unterkunftIcal.test.js` (Fixtures `test/fixtures/ical/booking.ics`, `airbnb.ics`, `tzid.ics`, `gefaltet.ics`, `fehlerseite.html`)
**Interfaces:**
- Produces:
- `ical.js` (rein): `parseIcs(text, { heute, horizontTage }) → { ereignisse:[{von,bis,uid}] } | throws Error('kein_ics')`; `baueIcs({ name, ereignisse:[{uid,von,bis}] , jetzt }) → string` (CRLF, Zeilen ≤ 75 Oktette gefaltet, `PRODID:-//WEBklar//Buchungskalender//DE`, `SUMMARY:Belegt`, `DTSTART;VALUE=DATE`).
- `icalAbgleich.js`: `holeIcs(url, { fetch, now }) → string` (nur `https:`, DNS-Auflösung → Abbruch bei privaten/loopback/link-local Adressen v4+v6, Timeout `icalTimeoutMs`, Abbruch über `icalMaxBytes`, max 3 Weiterleitungen je erneut geprüft); `gleicheAb(unterkunft, importEintrag, { now, fetch }) → { ok, anzahl }|{ ok:false, fehler }`; `gleicheAlleAb(now)`; nach `icalFehlerStunden` ohne Erfolg `mails.anVermieter('K33', …)` höchstens alle 24 h (`ukEinstellungen.icalFehlerMailAt`).
- Export: `GET /unterkunft/ical/:token.ics` → `text/calendar; charset=utf-8`, `Cache-Control: no-store`; Ereignisse = Buchungen `gebucht|bezahlt`, Reservierungen `freigegeben` mit Frist, `manuell`-Sperren; nie `ical:*`; `UID` = `<anfrageId|sperreId>@project.webklar.com`; unbekanntes Token → 404.
- [ ] **Step 1: Failing tests** (Review Focus 3): Booking-Fixture → Ereignisse mit DATE-Werten; `DTSTART:20261110T140000Z` → `2026-11-10`; `TZID=Europe/Berlin:20261110T000000` → `2026-11-10`; fehlendes DTEND → 1 Nacht; `STATUS:CANCELLED` ignoriert; gefaltete `SUMMARY` stört nicht; Ereignisse vor gestern/hinter Horizont fallen weg; HTML → `kein_ics`; `holeIcs('http://…')` → abgelehnt; `https://127.0.0.1/…`, `https://10.0.0.5/`, `https://[::1]/` → abgelehnt; zu große Antwort → abgelehnt; `gleicheAb` Erfolg ersetzt alte `ical:<id>`-Sperren (neue zuerst), Fehler lässt alte stehen und setzt `fehler`; K33 erst nach 6 h, dann höchstens 1× je 24 h; Export enthält Buchung + manuelle Sperre, nicht die importierte, Zeilen mit CRLF.
- [ ] **Step 2: FAIL.**
- [ ] **Step 3: Implementieren** (DNS über `node:dns/promises` `lookup(host,{all:true})`; `fetch` injizierbar für Tests).
- [ ] **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: iCal-Import und -Export`.
### Task 10: Fristen-, Aufräum- und Löschjobs
**Files:**
- Modify: `server/services/unterkunft/jobs.js`, `server/services/retention.js` (Uk-Zweig analog Shop), `server/services/customerAdmin.js` (~Z.299-305: `disableCustomerFeatures` auch `unterkunft`, `schliesseUkVonKunde`)
- Create: `server/services/unterkunft/aufbewahrung.js`
- Test: `test/unterkunftAufbewahrung.test.js`
**Interfaces:**
- Produces: `aufbewahrung.js`: `anonymisiere(now, { deckel }) → { anfragen:n, gaeste:n }` — `ipHash` leeren nach `ipHashTage`; `neu` älter als `neuVerfallTage` → `abgelaufen` (ohne Mail); Anfragen ohne Buchung (`abgelehnt|abgelaufen`) älter als `unverbindlichMonate` → `gast`, `nachrichtVermieter` leeren, `anonymisiertAt`; gebuchte Aufenthalte 3 Jahre nach Jahresende der Abreise → `gast` leeren (Rechnungsadresse und `ukRechnungen` bleiben); `ukRechnungen` 8 Jahre nach Jahresende löschen. `loescheUkDaten({ featureId, deckel })` (90 Tage nach Abschalten, Marker in `portalFeatures.adminNote` wie Shop). `schliesseUkVonKunde(customerId)`.
- [ ] **Step 1: Failing tests:** je Frist ein Dokument knapp davor und knapp danach (now fest); Deckel begrenzt Anzahl; Rechnung bleibt nach Gast-Anonymisierung lesbar und PDF-Schnappschuss unverändert; Kundenlöschung schaltet Funktion ab.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren** (Tagesjob ruft `anonymisiere` + `loescheUkDaten` für abgeschaltete). **Step 4: PASS**; `test/retention*.test.js` weiter grün.
- [ ] **Step 5: Commit** `Buchungskalender: Aufbewahrung und Löschfristen`.
### Task 11: Funktion bestellen „auf Anfrage“, Freischaltung, Ticket
**Files:**
- Create (ersetzt Stubs aus Task 5): `server/services/unterkunft/anfrageFunktion.js`, `server/services/unterkunft/freischaltung.js`, `server/services/unterkunft/ticketDetails.js`
- Modify: `server/routes/unterkunft.js` (`anfrage-funktion*`), `server/routes/team/unterkunft.js`
- Test: `test/unterkunftAnfrageFunktion.test.js`
**Interfaces:**
- Produces:
- `anfrageFunktion.js` (Kopie-Muster `shop/anfrage.js`, Status in `ukEinstellungen.anfrage*`, Sperre `uk-anfrage:<projectId>`): `ARTEN = ['ferienwohnung','monteurzimmer','pension','sonstiges']`, `anfrageOffenePunkte({customerId,projectId}) → string[]`, `stelleAnfrage({customerId,projectId,eingabe:{art,einheiten(1–50),portale:boolean,nachricht},person,now})` → T22 ans Team, K34 an Kunden (über `sendAdminNotice`/`sendCustomerMail` direkt; Vorlagen kommen aus Task 20 — Test prüft nur den Aufruf mit Template-ID), `zieheZurueck`, `entscheideAnfrage({…, entscheidung, begruendung})`, `anfrageAnsicht(einstellungen)`.
- `freischaltung.js`: `freischaltbar(doc) → boolean` (≥ 1 aktive Unterkunft mit `preiseVollstaendig` und Stornobedingungen; Einstellungen firma, strasse, plz, ort, email und (steuernummer|ustId); fail-closed).
- `ticketDetails.js`: `ticketDetails(ctx) → string` (siteKey, Snippet `<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div><script src="https://project.webklar.com/embed/unterkunft.js" defer></script>`, Schritte aus Spec §9, Verweis `docs/funktionen/unterkunft-einbau.md`).
- Team: `GET /api/team/unterkunft/:customerId` → je Projekt `{ projectId, name, state, anfrage, stripe, unterkuenfte:n, offeneAnfragen:n }`; `POST …/anfrage`.
- [ ] **Step 1: Failing tests:** Anfrage nur bei state `off`; doppelte Anfrage → 409; Entscheidung `annehmen` → `anfrageOffenePunkte` leer; `freischaltbar` false ohne Preise, true mit vollständigen Daten, false bei Lesefehler; Ticket-Details enthalten den siteKey.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket`.
---
## Strang 2: Oberfläche (`dev-uk-ui`, WOMS-Worktree `ticket.webklar.com/dev-unterkunft`)
Alle Portal-Module nach `public/shop.js`-Muster: `public/buchungen.js` (`initBuchungen(rootEl, { projectId })`, `REITER`), Reiter-Module `public/buchungen-tab-*.js` mit `render(panel, ctx)`, DOM-Helfer aus `public/shop-dom.js` wiederverwenden (`h`, `request`, `fehlerKarte`, `infoKarte`), Stile `public/buchungen.css`. Texte deutsch, Du-Form. Tests: Quelltext-/fakeDom-Tests nach Muster `test/shop*.test.js` mit `test/support/fakeDom.js`; API mit `request`-Attrappe gemäß Vertrag.
### Task 12: Grundgerüst + Reiter „Anfragen“
**Files:**
- Modify: `public/buchungen.js` (Platzhalter ersetzen)
- Create: `public/buchungen-tab-anfragen.js`, `public/buchungen-posten.js` (Posten-Editor, reine Funktionen `summeLive(posten, steuerart)` = Spiegel von `summiere`), `public/buchungen.css`
- Test: `test/buchungenUi.test.js`
- [ ] **Step 1: Failing tests:** `REITER` = `anfragen, kalender, unterkuenfte, rechnungen, einstellungen` (Labels „Anfragen“, „Kalender“, „Unterkünfte & Preise“, „Rechnungen“, „Einstellungen“); bei state `off` statt Reitern Anfrage-/Bestellkarte (Task 14); Anfragenliste rendert Nummer, Zeitraum „12.–30.11.2026 · 18 Nächte“, Gast, Status-Pille, Betrag; Detail zeigt Vorschlag-Posten editierbar, Summe aktualisiert sich (`summeLive` = Server-Werte aus Task 2 Beispielen: 40650); „Freigeben“ sendet `{posten,fristTage,nachricht}`; 409-Antwort zeigt „Der Zeitraum ist inzwischen belegt.“; gebuchte Anfrage zeigt Rechnung-Link, „Als bezahlt (vor Ort) markieren“, „Stornieren“ (Dialog mit Grund + Erstattungsbetrag nur bei Online-Zahlung).
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Portal-Bereich und Anfragen`.
### Task 13: Reiter „Kalender“ und „Unterkünfte & Preise“
**Files:**
- Create: `public/buchungen-tab-kalender.js`, `public/buchungen-tab-unterkuenfte.js`
- Test: `test/buchungenUi.test.js` (erweitern)
- [ ] **Step 1: Failing tests:** Monatsraster beginnt Montag; Bereiche gefärbt nach `quelle` (Klassen `ist-gebucht`, `ist-reserviert`, `ist-ical`, `ist-manuell`) mit Legende; zwei Klicks auf freie Tage öffnen „Zeitraum sperren“ mit Notiz → `POST sperren`; Klick auf manuelle Sperre bietet „Sperre aufheben“; Unterkunft-Formular mit allen Feldern aus Spec §3; Preis-Editor mit Staffelzeilen (+/−), Beispielrechnung „7 Nächte, 2 Personen = 406,50 €“ über `POST /preisvorschau`; iCal-Bereich: Import hinzufügen (Name, URL), Status „zuletzt abgeglichen …“ bzw. Fehlertext, Export-Link mit „Kopieren“, Kurzanleitung Booking.com („Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen“) und Airbnb, „Link erneuern“ mit Bestätigung.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Kalender und Unterkünfte im Portal`.
### Task 14: Rechnungen, Einstellungen, Funktionen-Kachel, Anfrageformular
**Files:**
- Create: `public/buchungen-tab-rechnungen.js`, `public/buchungen-tab-einstellungen.js`, `public/buchungen-anfrage.js` (Formular der Funktion, Muster `shop-anfrage.js`)
- Modify: `public/funktionen.js` (Kachel „Buchungskalender“ nach `shopTile` ~Z.216-260, sichtbar nur bei `unterkunft.verfuegbar`; Wunschprojekt-Klick wie Z.405 auf `buchungen-body`)
- Test: `test/buchungenUi.test.js`, `test/funktionen*.test.js` falls Pins
- [ ] **Step 1: Failing tests:** Rechnungsliste mit PDF-Link `/api/unterkunft/rechnungen/<id>.pdf?projectId=`; Einstellungen: Pflichtfelder markiert, Präfix-Feld gesperrt mit Hinweis, sobald Rechnungen existieren; Stripe-Karte mit Zuständen nicht verbunden / Onboarding offen / aktiv und Knopf „Vorhandenes Webshop-Konto verwenden“ nur wenn API es meldet; Schalter „Online-Zahlung anbieten“ nur bei aktivem Konto; Knopf „Mustertext übernehmen“ füllt Stornobedingungen (Text aus `docs/funktionen/unterkunft-storno-muster.md`, Task 21 — bis dahin Konstante in `buchungen-tab-einstellungen.js`, Task 22 gleicht ab); Kachel zeigt Preis „199 €“ und Zustände wie Shop-Kachel; Anfrageformular sendet `{art,einheiten,portale,nachricht}`.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel`.
### Task 15: WOMS
**Files (WOMS-Repo, Worktree von `origin/test` als `feature/unterkunft`, `git ls-files | grep -c node_modules` prüfen, nur Dateien einzeln stagen):**
- Modify: `src/lib/funktionenView.js` (`FUNKTIONEN` Eintrag `{ key:'unterkunft', label:'Buchungskalender', bestaetigen:{…wie shop} }`, `aktionen()`, `siteKey`, Hilfen für Anfrage-Zeilen generalisieren statt kopieren: `anfrageZeilen(eintrag, art)`), `src/lib/teamApi.js` (`getUnterkunft(customerId)`, `entscheideUnterkunftAnfrage(customerId, body)`), `src/components/customer/CustomerFunktionen.jsx` (Panel `UnterkunftPanel` nach `ShopPanel`-Muster: Anfrage-Zeilen, Annehmen/Ablehnen, Stripe-Status, Zahl Unterkünfte/offene Anfragen), `scripts/setup-legal.mjs` (DOCUMENTS anhängen: `leistungsbeschreibung-unterkunft` „Leistungsbeschreibung Buchungskalender“, `avv-unterkunft` „AVV Buchungskalender“, `liveUrl https://project.webklar.com/rechtliches/<key>.html` → sort 13 und 14)
- Test: `tests/unit/funktionenView.test.mjs`, `tests/setup-legal.test.mjs` (Länge 14, letzter Eintrag `avv-unterkunft`)
- [ ] **Step 1: Failing tests** (WOMS `node --test` mit `node_modules`-Symlink auf `../build/node_modules`): `FUNKTIONEN` enthält `unterkunft`; `aktionen()` für `unterkunft` wie Shop; `anfrageZeilen` liefert für beide Arten die richtigen Zeilen; setup-legal Länge 14.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS**; Vite-Build: `NODE_OPTIONS=--max-old-space-size=1200 node node_modules/vite/bin/vite.js build --outDir /tmp/claude-0/uk-woms-build` läuft durch.
- [ ] **Step 5: Commit** (WOMS) `Buchungskalender: Funktionsanfragen und Rechtliches-Karten`.
---
## Strang 3: Widget (`dev-uk-widget`, Donau-Repo `/root/reports/donau-home-website`)
### Task 16: Widget `public/embed/unterkunft.js`
**Files:**
- Create: `public/embed/unterkunft.js`, `public/embed/unterkunft-demo.html`
- Test: `test/unterkunftEmbedStatic.test.js` (Muster `shopEmbedStatic.test.js`), `test/unterkunftKalenderLogik.test.js`
**Interfaces:**
- Consumes: öffentlicher Vertrag `GET /:siteKey`, `GET /:siteKey/belegung`, `POST /:siteKey/anfrage`.
- Produces: IIFE; Einbau `<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div>`. Die reine Kalenderlogik steht im Widget in einem Objekt `WKU_LOGIK` und wird nur dann als `globalThis.__wkuLogik` bereitgestellt, wenn `document.currentScript?.dataset.test === '1'`; der Test lädt die Datei per `node:vm` mit `test/support/fakeDom.js` und diesem Attribut. Bei `{aktiv:false}` oder Ladefehler zeigt das Widget nur „Anfragen gerne per Telefon oder E-Mail.“ Funktionen: `monatsRaster(jahr, monat) → [{tag|null}] (Montag zuerst)`, `nachtBelegt(tag, belegt) → boolean`, `waehle(zustand, tag, belegt, mindestNaechte) → zustand` (erster Klick Anreise; zweiter Klick nach Anreise → Abreise, wenn keine belegte Nacht dazwischen, sonst neue Anreise; Klick vor Anreise → neue Anreise), `zusammenfassung(von, bis) → '12.–30. Nov. · 18 Nächte'`.
- Gestaltung: N1 (`@layer wku`, `.wku-`, Variablen). Zwei Spalten ab 760 px (Container-Query), Kalender links (1 Monat, 2 Monate ab 1100 px), Formular rechts: Unterkunft (nur bei > 1), Personen (1…max), Firma (opt.), Name, E-Mail, Telefon (opt.), Nachricht (opt.), Honeypot `kontakt_url` (versteckt, `tabindex=-1`, `autocomplete=off`), Datenschutzhinweis mit Link `anbieter.datenschutzUrl`, Knopf „Anfrage senden“. Ohne Zeitraum → Meldung „Bitte wählen Sie Anreise und Abreise im Kalender.“. Erfolg: „Danke! Ihre Anfrage A-XXXX ist eingegangen. Sie erhalten gleich eine Bestätigung per E-Mail.“ Tastatur: Pfeile bewegen Fokus, Enter/Leertaste wählt; `aria-label` „Dienstag, 10. November 2026, frei“ / „belegt“; `aria-live` für Zusammenfassung. Alle Texte per `textContent`.
- [ ] **Step 1: Failing tests:** Statik: kein `innerHTML` mit Variablen, kein `eval`, API-Pfad `/api/public/unterkunft/v1/`, Präfix `.wku-`, `@layer wku`; Logik: `monatsRaster(2026,11)` beginnt mit 6 Leerfeldern (1.11.2026 ist Sonntag); `waehle` über belegte Nacht → neue Anreise; Abreise auf belegter Anreisetag-Grenze (Nacht davor frei) erlaubt; unter Mindestnächten → Hinweis-Zustand `{fehler:'mindestnaechte'}`; `zusammenfassung('2026-11-12','2026-11-30') === '12.–30. Nov. · 18 Nächte'`, über Monatsgrenze `'28. Nov. – 3. Dez. · 5 Nächte'`.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Website-Widget mit Kalender und Anfrageformular`.
### Task 17: Standalone-Seiten und Buchungsseite
**Files:**
- Modify: `server/routes/unterkunftSeite.js` (GET `/unterkunft/:siteKey` → Widget auf neutraler Seite; GET `/unterkunft/:siteKey/buchung/:id` → Buchungsseite; Export-Route kommt aus Task 9 — Reihenfolge `ical` zuerst)
- Create: `public/unterkunft-seite.html` (Platzhalter `__SITEKEY__`, `__ANSICHT__` wie `shop-seite.html`, CSP `script-src 'self'`, `frame-ancestors 'none'`, `noindex`), `public/unterkunft-buchung.js`, `public/unterkunft-buchung.css`
- Test: `test/unterkunftSeite.test.js`
- [ ] **Step 1: Failing tests:** Routen liefern HTML mit escaptem siteKey, CSP-Header, `Cache-Control: no-store`; ungültiger siteKey → 404; `unterkunft-buchung.js` (fakeDom, Vertrag-Attrappe): Status `freigegeben` zeigt alle Pflichtangaben aus Spec §6 in dieser Reihenfolge direkt über dem Knopf — Anbieter, Unterkunft + Zeitraum mit Check-in/-out, Personen, Posten, Gesamtpreis (inkl. USt-Ausweis bzw. „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“), Zahlarten, Stornobedingungen, „Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Nr. 9 BGB).“, Datenschutzhinweis, „Reserviert bis …“; Knopftext exakt „Zahlungspflichtig buchen“; Option „online“ fehlt, wenn nicht in `zahlarten`; nach Buchung: „Ihre Buchung ist bestätigt“ + Rechnung-Link + bei unbezahlt online „Jetzt online bezahlen“; `abgelaufen` → „Die Reservierung ist abgelaufen. Bitte fragen Sie erneut an.“; 404 → „Dieser Link ist ungültig.“; `wk_zahlung=abbruch` → Hinweis „Zahlung abgebrochen – Sie können jetzt oder vor Ort bezahlen.“
- [ ] **Step 2: FAIL.** **Step 3: Implementieren** (Akzentfarbe aus Antwort als CSS-Variable). **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Buchungsseite für Gäste`.
### Task 18: Einbau auf der Donau-Home-Seite
**Files (Repo `/root/reports/donau-home-website`, Branch `main`, auf 5f8465a; vorher `git status` sauber und `git log -1` prüfen — hat der Inhaber gepusht, auf `origin/main` aufsetzen, nie überschreiben):**
- Modify: `index.html` Abschnitt `#anfrage` (Z.~555-590) und Skript (Z.~668-690): altes Formular und Datums-Skript entfernen; linke Spalte = Kopf + Einleitung „Wählen Sie Ihren Zeitraum im Kalender, nennen Sie die Zahl der Personen – wir melden uns mit Verfügbarkeit und Preis.“ + Kontaktliste; rechte/ganze Breite = `<div data-webklar-unterkunft="SITEKEY_FOLGT" data-api="https://project.webklar.com"></div>` + Script-Tag. Da das Widget selbst zweispaltig ist (Kalender links, Formular rechts), wird `.anfrage` zu: Kopf + Kontakt oben (zweispaltig: Text links, Kontakt rechts), darunter das Widget über volle Breite — so steht der Kalender links.
- Modify: CSS-Block `/* Anfrage */`: Variablen `--wku-akzent: var(--eiche)`, `--wku-hintergrund: var(--leinen)`, `--wku-text: var(--tinte)`, `--wku-rand: var(--linie)`, `--wku-radius: 10px`, `--wku-schrift: 'Figtree', system-ui, sans-serif` (genaue Token-Namen aus dem `:root` der Seite übernehmen).
- Fallback ohne aktives Widget liefert das Widget selbst (Task 16); ohne JS bleibt die Kontaktliste sichtbar.
- [ ] **Step 1:** Mit lokalem Widget-Demo prüfen: `public/embed/unterkunft-demo.html` um eine Variante „Donau“ mit den Variablen ergänzen; Screenshot 390 px und 1280 px per Playwright nach `/root/reports/buchungskalender-2026-10-07/donau/`.
- [ ] **Step 2:** `index.html` ändern; Seite lokal statisch öffnen (Widget mit API-Attrappe über `page.route`), Screenshots wie oben; keine horizontale Scrollleiste bei 390 px.
- [ ] **Step 3: Commit** im Donau-Repo `Anfrage: Buchungskalender statt Anreise/Abreise-Feldern` (Autor Kenso Grimm). Kein Push. siteKey wird beim Livegang eingesetzt (Livegang-Notiz).
---
## Strang 4: Mails, PDF, Recht (`dev-uk-mail`)
### Task 19: PDF-Writer und Rechnungs-PDF
**Files:**
- Create: `server/services/unterkunft/pdf.js`, `server/services/unterkunft/rechnungPdf.js`
- Test: `test/unterkunftPdf.test.js`
**Interfaces:**
- Produces:
- `pdf.js`: `class PdfDoc { constructor({ breite=595.28, hoehe=841.89 }); seite(); text(x, y, str, { groesse=10, fett=false, rechts=false }); linie(x1,y1,x2,y2,{dicke=0.5}); breite(str, groesse, fett) → number; umbrechen(str, maxBreite, groesse) → string[]; toBuffer() → Buffer }` — Standardschriften Helvetica/Helvetica-Bold, `WinAnsiEncoding`, Breitentabelle der Helvetica-AFM für 32–255 eingebettet, Zeichen außerhalb WinAnsi → `?`; `€` = 0x80, Umlaute/ß nach CP1252; Strings PDF-escaped (`\`, `(`, `)`); xref mit korrekten Offsets; kein Kompressionsfilter.
- `rechnungPdf.js`: `rechnungPdf(inhalt, { art, nummer, datum }) → Buffer` — Layout A4: Absenderzeile klein, Empfängeradresse, rechts Rechnungsnummer/Datum/Leistungszeitraum, Titel „Rechnung“ bzw. „Stornorechnung zu <bezugNummer>“, Leistung „Beherbergung <Unterkunft>, <von>–<bis> (<n> Nächte, <p> Personen)“, Posten-Tabelle (Text, Menge, Einzelpreis, Betrag), Summen (netto je Satz, USt je Satz, Gesamt) bzw. § 19-Satz, Zahlart („Zahlung vor Ort“ / „Online-Zahlung“), Fuß: Firma, Anschrift, Steuernummer/USt-ID, Bank (falls vorhanden). Seitenumbruch bei vielen Posten.
- [ ] **Step 1: Failing tests:** Buffer beginnt `%PDF-1.4`, endet `%%EOF\n`; `startxref`-Wert zeigt auf `xref`; jeder xref-Offset zeigt auf `n 0 obj`; Text „Übernachtung“ als CP1252-Bytes enthalten; `€` als Byte 0x80; `(` im Text escaped; 40 Posten → 2 Seiten (`/Count 2`); Rechnung enthält Nummer, „Gemäß § 19 UStG“ bei Kleinunternehmer, „USt 7 %“ bei Regel; gleicher `inhalt` → byte-gleiches PDF (deterministisch, kein Zeitstempel außer `datum`).
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS**; zusätzlich ein Beispiel-PDF nach `/root/reports/buchungskalender-2026-10-07/beispiel-rechnung.pdf` schreiben und mit `pdftotext` (falls vorhanden) oder Chrome prüfen.
- [ ] **Step 5: Commit** `Buchungskalender: PDF-Writer und Rechnungs-PDF`.
### Task 20: Mail-Vorlagen und Versand
**Files:**
- Create: `server/mail-templates/G16 Buchungsanfrage eingegangen.html`, `G17 Buchungsanfrage freigegeben.html`, `G18 Buchungsanfrage abgelehnt.html`, `G19 Buchungsbestätigung.html`, `G20 Zahlung eingegangen (Unterkunft).html`, `G21 Reservierung abgelaufen.html`, `G22 Buchung storniert.html`, `K31 Neue Buchungsanfrage.html`, `K32 Neue Buchung.html`, `K33 Kalenderabgleich fehlgeschlagen.html`, `K34 Anfrage Buchungskalender.html`, `T22 Neue Anfrage Buchungskalender.html`
- Modify: `server/services/unterkunft/mails.js` (Stub ersetzen, Signaturen aus Task 4 bleiben; `_aufrufe` bleibt als Testhilfe erhalten), `server/mailer.js` bzw. `server/services/mailer.js` (`sendGuestMail({ …, attachments })`), `server/services/termine/outbox.js` (beim Senden: `anhang.art==='ukRechnung'` → `rechnungPdf` erzeugen und als `attachments:[{filename:'Rechnung-<nr>.pdf', content, contentType:'application/pdf'}]` übergeben)
- Test: `test/unterkunftMails.test.js`, `test/mailTemplates.test.js` (Pins), `test/termineOutbox.test.js`
**Interfaces:**
- Produces: `mails.anGast` reiht über `einreihen({ featureId, bookingId: anfrage.$id, art: 'uk-'+art, an: gast.email, fromName: einstellungen.firma, replyTo: einstellungen.email, template: art, values, blocks, anhang? }, now)` ein; G19/G22 mit `anhang:{art:'ukRechnung', rechnungId}`. Platzhalter je Vorlage im Kopfkommentar; gemeinsame Werte: `gast_name`, `firma`, `unterkunft`, `zeitraum` („12.11.2026 – 30.11.2026“), `naechte`, `personen`, `nummer`, `gesamt`, `posten_text` (mehrzeilig), `frist`, `buchungs_link`, `stornobedingungen`, `anbieter_kontakt`, `rechnung_nummer`, `erstattung`, `nachricht_vermieter`. K-Mails an `besitzerEmail` mit `portal_url` = `${publicBaseUrl}/dashboard.html#buchungen?projekt=<id>`.
- Inhalt: G17 enthält Preis, Frist, Stornobedingungen, Knopf-Link „Jetzt verbindlich buchen“ und Satz „Erst mit dem Klick auf „Zahlungspflichtig buchen“ auf der Buchungsseite ist Ihre Buchung verbindlich.“; G19 (dauerhafter Datenträger) alle Angaben der Buchungsseite + Stornobedingungen vollständig + Hinweis kein Widerrufsrecht + bei online unbezahlt Bezahl-Link.
- [ ] **Step 1: Failing tests:** `templateIds()` enthält alle 12 IDs; jede Vorlage rendert mit Beispielwerten ohne `problems`; G-Vorlagen in Sie-Form ohne WEBklar-Logo; Outbox-Eintrag mit `anhang` → gesendete Mail hat 1 Anhang `Rechnung-RE-2026-0001.pdf` beginnend mit `%PDF`; ohne `anhang` unverändert (Termine-Tests grün); `anGast('G17',…)` Mail hat `fromName` Firma und `replyTo` Vermieter-E-Mail.
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
- [ ] **Step 5: Commit** `Buchungskalender: Mails an Gäste, Vermieter und Team`.
### Task 21: Rechtstexte und Doku
**Files:**
- Create: `docs/rechtliches/leistungsbeschreibung-unterkunft.md`, `docs/rechtliches/avv-unterkunft.md` (Muster der Webshop-Texte; Fassung 2026-10-07; Inhalte: Leistung, 199 € einmalig ohne USt § 19, Laufzeit solange Website bei WEBklar, Pflichten Vermieter [Preise, Steuer, Stornobedingungen, Meldeschein, Kurtaxe], Rechnungsstellung im Auftrag § 14 Abs. 2 S. 4 UStG, Verfügbarkeit iCal ohne Gewähr, Stripe als eigener Vertrag), `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md`, `docs/rechtliches/unterkunft-kanzlei-fragen.md` (mind.: Button-Lösung per Mail-Link, Pflichtinfos Art. 246a, Widerrufsausschluss § 312g Abs. 2 Nr. 9 auch bei Monteursunterkünften für Unternehmer/Verbraucher, USt Endreinigung 7 %, Rechnung im Auftrag, Aufbewahrung 8 Jahre durch WEBklar, Rolle AVV, Haftung bei Doppelbuchung durch iCal-Verzögerung), `docs/funktionen/unterkunft-datenschutz-absatz.md`, `docs/funktionen/unterkunft-storno-muster.md`, `docs/funktionen/unterkunft-einbau.md`, `docs/funktionen/unterkunft-livegang.md`
- Modify: `scripts/build-legal-html.mjs` (Modus `unterkunft`), danach `node scripts/build-legal-html.mjs unterkunft` → `public/rechtliches/*-2026-10-07.html`; PDFs über `scripts/build-legal-pdfs.sh unterkunft` (Host-Chrome; falls nicht vorhanden: im Livegang-Dokument als Schritt vermerken)
- Test: `test/legalDocs.test.js` (featureLegalReady('unterkunft') true, wenn Dateien da)
- [ ] **Step 1:** Texte schreiben (Kennzeichnung „Entwurf – Prüfung durch Kanzlei offen“ in LB/AVV-Kopf).
- [ ] **Step 2:** HTML/PDF bauen, `--check` grün; `featureLegalReady('unterkunft')` Test PASS.
- [ ] **Step 3:** Livegang-Notiz mit Schritten: (1) Inhaber prüft Texte; (2) `node scripts/unterkunft-setup.mjs --check` dann ohne; Termine-Setup für Outbox-Feld `anhang`; (3) `stripe-products.mjs --anlegen` (unterkunftSetup); (4) compose/.env `UNTERKUNFT_ENABLED=pilot`, `UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimi>`; (5) Push `feature/unterkunft:main` (Portal), `feature/unterkunft:test` (WOMS) — nie unter eigenem Namen; (6) Rechtstexte in WOMS Rechtliches hochladen (macht Claude, Schlüssel `/root/.appwrite-rechtliches.key`); (7) Mimi im WOMS freischalten (`activate`), siteKey in Donau-`index.html` einsetzen, Donau pushen; (8) Pilot: Testanfrage → Freigabe → vor Ort buchen → Rechnung prüfen → stornieren; (9) Datenschutz § 6f in `public/datenschutz.html` übernehmen; (10) nach Kanzlei `on`.
- [ ] **Step 4: Commit** `Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz`.
---
## Abschluss
### Task 22: Zusammenführen und Gesamtdurchlauf
**Files:**
- Merge in `feature/unterkunft`: `feature/uk-server`, `feature/uk-mail`, `feature/uk-widget`, `feature/uk-ui` (in dieser Reihenfolge; Konflikte nur in `server/routes/unterkunft.js`, `unterkunftSeite.js` erwartet)
- Create: `test/ui/unterkunftTestServer.mjs` (Muster `shopTestServer.mjs`), `test/ui/unterkunft.ui.mjs`
- [ ] **Step 1:** Merges, danach alle `test/unterkunft*.test.js`, `test/buchungen*.test.js` und die in Tasks 1–21 genannten Bestands-Tests einzeln nacheinander grün.
- [ ] **Step 2:** Abgleich: Stornomuster-Konstante in `buchungen-tab-einstellungen.js` = `docs/funktionen/unterkunft-storno-muster.md`; Stub-Reste (`_aufrufe` nur in Tests genutzt, 501-Fallback in Task 7 entfernen).
- [ ] **Step 3: UI-Durchlauf** (`free -m` vorher): Widget auf Demo-Seite → Zeitraum + Anfrage → Portal „Anfragen“ → Posten ändern → Freigeben → Link aus Mail-Sink → Buchungsseite „vor Ort“ → „Zahlungspflichtig buchen“ → Rechnung-PDF abrufbar → Portal „bezahlt (vor Ort)“ → zweite Anfrage online → Fake-Stripe-Zahlung → `bezahlt` → Stornieren mit Teilerstattung. Screenshots nach `/root/reports/buchungskalender-2026-10-07/ui/`.
- [ ] **Step 4: Commit** `Buchungskalender: Zusammenführung und UI-Durchlauf`.
### Task 23: Gesamtreview
- [ ] **Step 1:** Ein Review-Agent (sonnet) prüft den ganzen Branch gegen Spec + Review Focus; Funde beheben, je Fund ein Commit.
- [ ] **Step 2:** Ledger-Abschluss, Memory-Notiz aktualisieren, dem Inhaber Livegang-Notiz verlinken.
@@ -0,0 +1,684 @@
# Kundenansicht neu: Umsetzungsplan, Übersicht und Verträge
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
> Diese Übersicht ist der Vertrag zwischen den Paketen. Namen, Signaturen, Datenformen, Endpunkte und Dateizuständigkeiten stehen nur hier und gelten für alle Paket-Pläne. Weicht ein Paket-Plan davon ab, gilt diese Übersicht.
**Goal:** Die Kundenansicht des Portals wird weiß und schlank wie das Ticketsystem, bekommt die Handy-Leiste „Fahrstuhl-Zeile“ und die Funktionen mit „Meine/Entdecken“. Die Funktionen stecken im Abo und werden eingeschaltet statt gekauft. Bestätigungen schrumpfen auf das rechtlich Nötige. Alles liegt hinter einem Pilot-Schalter, bis der Inhaber umschaltet.
**Architecture:**
- Die neue Oberfläche ist eine eigene Seite `public/neu/dashboard.html` mit eigenen ES-Modulen und CSS unter `public/neu/`. Der Server liefert sie unter derselben Adresse `/dashboard.html` aus, wenn der Pilot-Schalter für die Sitzung gilt.
- Die Funktionsmodule (`public/termine.js`, `shop.js` …) bleiben gemeinsam. Ihr Aussehen kommt aus dem CSS der jeweils geladenen Seite, ihr Verhalten unterscheidet sich nur über das Feld `modell` der Server-Antwort.
- Neue Server-Logik ist nur für Sitzungen mit neuer Ansicht aktiv (`neueAnsicht(req)`) oder rein additiv. Die alte Ansicht verhält sich bis zum Umschalten exakt wie heute.
**Tech Stack:**
- Portal: Node 20 (Prod-Container `node:20-alpine`, Host 24), Express 4, Appwrite-REST (`server/services/appwriteAdmin.js`), Stripe SDK 19, Vanilla JS ohne Build.
- Tests Portal: `node --test` mit den Nachbauten aus `test/support/` (`appwriteMock`, `fakeStripe`, `testApp`, `fixtures`, `fakeDom`, `mailSink`) und Playwright-Skripte `test/ui/*.ui.mjs`.
- WOMS: React 18, Vite, Tests `node --test` über `tests/unit/load.mjs`.
**Spec:** `docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (Commit d3d2849, Abschnitte 1–8 mit E1–E21 und C1–C15). Jeder Implementierer liest die Spec, diese Übersicht und seinen Paket-Plan.
**Code-Karten** (Stand 16.10., nur gelesen): `/root/reports/kundenansicht-neu-2026-10-15/karten/`
- `huelle-tests.md`: dashboard, app.js, Router, Tests, Pins
- `funktionen-server.md`: Funktionen, Zustände, Bestellen, Aktivieren, Abo-Abgleich
- `abo-server.md`: Stundenkonto, Abo, Wechsel, Widerruf, Fassungen, Kundenart, Mails
- `woms-vorschau.md`: WOMS-Stellen, Vorschau
Dazu die Navigations-Demo `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html`.
---
## Ausführungsmodell
```
Phase A P0 Werkzeug, Pilot-Schalter, Vorschau-Gerüst (1 Agent, Hauptzweig)
Phase B parallel in eigenen Worktrees:
Strang UI: P1 Fundament+Hülle → P2 Website/Chat/Verträge
Strang Server: P5 Abo-Modell Kern → P6 Abo-Modell Betrieb
Strang Recht: P8 Rechtsrunde Fassung 6 (Entwürfe + Fassungs-Technik)
Strang WOMS: P9 WOMS (gegen die Verträge V4/V9; Portal-Teile von P9 erst nach Merge von P5)
Phase C Merge Server → Hauptzweig, dann Strang UI weiter: P3 Funktionen-Rahmen → P4 Funktionsmodule
Phase D Merge Recht + UI → P7 Bestellseiten schlank (Hauptzweig)
Phase E Integration, ganze Suite, Vorschau aktualisieren, Abschlussprüfung (mehrere Blickwinkel), Übergabe
→ der Inhaber pusht (Schalter off = für Kunden unsichtbar), Pilot fürs Team, Rechtsrunde mit dem Inhaber
Phase F P10 Umschalten und Aufräumen (nur mit ausdrücklichem OK des Inhabers)
```
- **Ein Implementierer je Strang**, Modell `sonnet`. Die Tasks eines Strangs laufen der Reihe nach mit TDD.
- **Prüfungen:** keine Reviewer je Task. Jeder Paket-Plan endet mit einer Selbstprüfung: eigene Tests grün, Verträge abgehakt, `git status` sauber. Eine gemeinsame Abschlussprüfung gibt es in Phase E. Kleine Nachprüfungen mit `haiku`. Kein `opus`, kein `fable` (Memory „feedback-billige-modelle“).
- **Merge-Reihenfolge in den Hauptzweig:** P0 → Server (P5, P6) → Recht (P8) → UI (P1–P4) → P7. Konflikte entstehen nur in den Dateien, die die Zuständigkeitstabelle als „geteilt“ markiert.
- **Tags:** nach jedem Paket `kn-p<N>` im jeweiligen Zweig. Nach dem Merge auf den Hauptzweig `kn-p<N>-haupt`.
### Pakete, Plan-Dateien, Worktrees
| Paket | Plan-Datei (`docs/superpowers/plans/`) | Worktree | Branch | Start |
|---|---|---|---|---|
| P0 Werkzeug, Schalter, Vorschau | `2026-10-16-kundenansicht-01-werkzeug-vorschau.md` | `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht` | `feature/kundenansicht-neu` | sofort |
| P1 Fundament und Hülle | `…-02-fundament-huelle.md` | `…/dev-kn-ui` | `kn/ui` | nach P0 |
| P2 Website, Chat, Verträge | `…-03-website-chat-vertraege.md` | `…/dev-kn-ui` | `kn/ui` | nach P1 |
| P3 Funktionen-Rahmen | `…-04-funktionen-rahmen.md` | `…/dev-kn-ui` | `kn/ui` (rebased auf Hauptzweig nach Server-Merge) | nach P2 und Server-Merge |
| P4 Funktionsmodule | `…-05-funktionsmodule.md` | `…/dev-kn-ui` | `kn/ui` | nach P3 |
| P5 Abo-Modell Kern | `…-06-abo-modell-kern.md` | `…/dev-kn-server` | `kn/server` | nach P0 |
| P6 Abo-Modell Betrieb | `…-07-abo-modell-betrieb.md` | `…/dev-kn-server` | `kn/server` | nach P5 |
| P7 Bestellseiten schlank | `…-08-bestellseiten.md` | `…/dev-kundenansicht` | `feature/kundenansicht-neu` | nach Merge von UI und Recht |
| P8 Rechtsrunde Fassung 6 | `…-09-rechtsrunde.md` | `…/dev-kn-recht` | `kn/recht` | nach P0 |
| P9 WOMS | `…-10-woms.md` | `/home/webklar/apps/ticket.webklar.com/dev-kundenart` | `feature/kundenart` (WOMS, von `origin/test`) | nach P0 |
| P10 Umschalten und Aufräumen | `…-11-umschalten-aufraeumen.md` | `…/dev-kundenansicht` | `feature/kundenansicht-neu` | nach Phase E und OK des Inhabers |
**Worktrees der Stränge** entstehen aus dem Stand nach P0, zum Beispiel:
`git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht worktree add ../dev-kn-ui -b kn/ui kn-p0`
- Danach `ln -s ../build/node_modules node_modules` im neuen Worktree. Der Symlink ist unversioniert, deshalb **nie `git add -A`**, Dateien immer einzeln hinzufügen.
- Der WOMS-Worktree entsteht von `origin/test`: `git -C /home/webklar/apps/ticket.webklar.com/build worktree add ../dev-kundenart -b feature/kundenart origin/test`. Danach `npm ci` mit Speicherbremse, siehe Global Constraints.
- Der WOMS-Zweig `fix/akquise-sichtbar` (`5f13553`, Worktree `dev-akquise-sichtbar`) ist nicht gepusht und ändert `CreateLeadForm` und `createLead`. P9 baut auf `origin/test`. Beim Merge in `test` gilt die Reihenfolge, die der Inhaber festlegt. Ist `fix/akquise-sichtbar` bis dahin in `test`, rebased P9 darauf.
### Dateizuständigkeit (Konfliktschutz)
Jede Datei hat genau einen Besitzer. Wer etwas aus einer fremden Datei braucht, nutzt die Verträge V1–V12.
| Datei / Bereich | Besitzer |
|---|---|
| `server/services/kundenansicht/schalter.js` (neu), Route `/dashboard.html` in `server/index.js`, Konfig `config.kundenansichtNeu`, Feld `ansicht` in `/api/auth/me`; ein **Platzhalter** `public/neu/dashboard.html` für den Routentest (ab P1 gehört die Datei P1) | P0 |
| Werkzeug `/root/reports/kundenansicht-neu-2026-10-15/werkzeug/` (eigenes Git), `test/ui/kundenansicht-neu*.ui.mjs` | P0 legt an, P1–P4 und P7 ergänzen nur eigene Ansichten und Fixtures |
| `public/neu/dashboard.html`, `public/neu/tokens.css`, `basis.css`, `huelle.css`, `public/neu/dom.js`, `api.js`, `router.js`, `routen.js`, `huelle.js`, `leiste.js`, `konto.js`, `fristen-streifen.js`, `zaehler.js`, `etage.js`, `reiter.js` | P1 |
| `public/neu/ansicht-website.js`, `ansicht-chat.js`, `ansicht-vertraege.js`, `ansicht-stunden.js`, `ansicht-speicher.js`, `ansicht-gutschrift.js`, `ansicht-passwort.js`, `public/neu/ansichten.css`; `public/neu/ansicht-bestellen.js` und `ansicht-wechsel.js` als **1:1-Übernahme** der heutigen Bestell- und Wechselseiten im neuen Aussehen (alte Texte, Fassung 5, C14); öffentliche Seiten im neuen Stil unter `public/neu/oeffentlich/` (`login.html`, `kuendigen.html`, `widerrufen.html`, `offline.html`), die erst P10 ausliefert | P2 |
| `public/neu/ansicht-funktionen.js`, `funktion-einschalten.js`, `public/neu/funktionen.css`, `public/neu/bilder/*` | P3 |
| `public/neu/module.css` (neues Aussehen aller Funktionsmodule). In `public/termine*.js`, `shop*.js`, `buchungen*.js`, `zeiten*.js`, `funktionen.js` (nur News), `analyse*.js` und `funktion-bestellen.js` nur Verzweigungen nach `modell === 'abo'` und die Kurzlabels der Reiter (C2) | P4 |
| Umbau von `public/neu/ansicht-bestellen.js` und `ansicht-wechsel.js` auf das schlanke Muster (Fassung 6). In `server/routes/abo.js` nur die Pfade für Fassung 6. `server/services/planSwitch.js:351` mit Umgebung. `server/services/vertragsNachweis.js` (neu) | P7 |
| `server/services/kundenart.js` (neu), `server/services/aboFunktionen.js` (neu), `server/services/funktionEinschalten.js` (neu), `server/services/fristen.js` (neu), `server/routes/funktionen.js` (Katalog, Einschalten), `server/routes/fristen.js` (neu), `server/routes/auth.js` (nur `sanitizeCustomer` + Kundenart), `server/services/customerAdmin.js` (Kundenart), `server/services/featureRegistry.js` (Katalogfelder), `server/services/featureConsents.js` (Attribut `kundenart`), `scripts/kundenansicht-setup.mjs` (neu), Shop-/Unterkunft-Anfrage (Unternehmer aus Kundenart), Termine-Zustimmung ohne `gesundheit` im Abo-Modell | P5 |
| `server/services/hourLedger.js` (Vorschuss), `server/routes/team/customers.js` (Stundenkonto-Endpunkt), `server/services/siteFeatures.js` (`adminActivate` mit Abo-Prüfung und Abrechnung), `server/routes/admin/funktionenAdmin.js`, `server/services/aboFunktionenAbgleich.js` (neu), Wechsel mit gemerkter Funktion in `planSwitch.js` (nur `details.funktion`, `completeSwitch`, `markSwitchFailed`) und `GET /api/abo` (`wechselFunktion`), Ticket-Texte zum Stundenkonto, neue Mailvorlagen K38–K41 und T24–T25, Anpassung K02/K03/K22/K29/K30/T16 nur für das Abo-Modell | P6 |
| `docs/rechtliches/*-v6*.md` und neue Entwürfe, `public/vertragsunterlagen/v5/*` (Archiv), `public/neu/rechtliches/*` (neue Fassungen), `server/services/products.js` (Fassungstabelle), `config.abo.termsVersionNeu`, `scripts/build-legal-*`, `scripts/rechtliches-hochladen.mjs` (neu), `docs/rechtliches/kundenansicht-entscheidungen.md` (neu) | P8 |
| WOMS-Repo komplett | P9 |
| `scripts/kundenart-bestand.mjs` (neu), Umschalten, Aufräumen alter Dateien | P10 |
| **geteilt:** `server/config.js` (P0: Abschnitt `kundenansichtNeu`; P5: Abschnitt `aboFunktionen`; P8: `abo.termsVersionNeu`; jeder nur sein eigener Abschnitt), `test/support/schema.js` (nur neue Collection-/Attribut-Quellen ergänzen), `scripts/setup-abo-collections.mjs` (nicht ändern; neue Attribute nur in `scripts/kundenansicht-setup.mjs`) | wie angegeben |
Bestehende Testdateien ändert ein Paket nur dort, wo sein Plan sie ausdrücklich nennt, etwa für Pins auf Texte, die das Paket ändert.
---
## Global Constraints
- **Alte Ansicht unverändert.** Bis P10 ist jede Verhaltensänderung für Kunden ohne neue Ansicht verboten. Neue Server-Logik prüft `neueAnsicht(req)`, ist rein additiv (neue Felder, neue Endpunkte) oder betrifft nur Datensätze, die erst über die neue Ansicht entstehen (`unlockedBy: 'abo' | 'projekt'`).
- **Schalter:** `KUNDENANSICHT_NEU` ist `off | pilot | on`, Vorgabe `off`. `KUNDENANSICHT_NEU_PILOT_EMAILS` ist eine Komma-Liste, `*` heißt alle. Der Modus `off` schlägt jede Liste.
- **Aussehen** (Spec 5.1, Werte wörtlich aus dem Ticketsystem `src/styles/global.css:7-51`):
- Farben: `#fff` Fläche, Text `#1a1f36`, Zweittext `#5e6678`, Tertiär `#8a93a3` nur für Nebensächliches, Linien `#ebeef1` und `#d8dde3`, Hover `#f6f8fa`, Fläche `#f8f9fb`, Akzent `#111`.
- Status-Pillen (Fläche/Schrift): grau `#ebeef1`/`#414552`, blau `#e0f2fe`/`#075985`, orange `#fcedb9`/`#a82c00`, violett `#efeafe`/`#5b21b6`, grün `#d7f7c2`/`#006908`, rot `#ffe7f2`/`#b3093c`. Fehler `#c0123c`.
- Radien 6 und 12 px. Ring-Schatten `0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06)`.
- Systemschrift `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`. Kein Gewicht 800, Zahlen `tabular-nums`.
- Verboten: Karten, Verläufe, Glow, Punktraster, Glas als Deko, Dunkelmodus, Lila.
- Ladezustände als Text („Wird geladen …“). Dialoge schließen nur per × und Esc.
- **Anrede „du“** in allen neuen Texten der Oberfläche. Ausnahmen: Rechtsdokumente und bestehende Mails (Spec 5.7); die Bestellseiten bekommen „du“ erst mit Fassung 6 (C14, P7).
- **Keine Preise für Funktionen** in der neuen Oberfläche, nirgends.
- **Hashes:** Alle alten Hashes bleiben gültig (`VIEW_HASHES`, Aliase `#wartung`, `#abo`, `#fortschritt`). Die Query nach `?` gelangt unverändert ins Modul. Neue Hash-Namen nur aus `[a-z]` wegen der Login-Whitelist `/^\/dashboard\.html(#[a-z]+)?$/` (`public/app.js:466`, `public/login.html`).
- **Rechtlich fest** (Spec 4):
- Kerninfo und „Zahlungspflichtig bestellen“ stehen auf demselben Bildschirm.
- Der Knopftext bleibt wörtlich.
- „Verträge hier kündigen“ ist immer erreichbar. „Vertrag widerrufen“ steht immer im Konto.
- `kuendigen.html` und `widerrufen.html` bleiben schlicht.
- **Kundenart:** Der Server ist die einzige Quelle. Er nimmt nie die Kundenart vom Client. Eine leere oder unbekannte Kundenart gilt als `privat` (E4), überall über `kundenartWirksam()` (V4).
- **Nachweise:** Wortlaut, Fassung, Zeit, IP, Browser und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt.
- **Tests Portal** (aus dem jeweiligen Worktree):
- Einzeldatei: `timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/<datei>.test.js`.
- Mehrere Dateien in einem Aufruf nur mit `--test-concurrency=1`.
- Die ganze Suite nur in Phase E und P10: `timeout 2400 nice -n 10 systemd-run --scope -q -p MemoryMax=2500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 node --test --test-concurrency=2 test/*.test.js`.
- Vor jedem schweren Lauf `free -m`. Bei weniger als 2000 MB verfügbar warten und den Controller melden.
- **Tests WOMS:** `cd /home/webklar/apps/ticket.webklar.com/dev-kundenart && timeout 900 nice -n 10 systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 npm test`.
- **Playwright** nur mit dem Werkzeug aus P0 (`run-browser.sh`, eine Instanz, Speicherbremse). `chromium-1234`, `playwright-core` 1.62.1.
- **Commits:** Dateien einzeln hinzufügen. Nachrichten deutsch, mit der Zeile `Co-Authored-By: …` aus dem eigenen Systemhinweis der ausführenden Sitzung (kein fester Modellname, Nachtrag N50).
- Kein Push, kein `--force`, kein Merge auf `main` oder `test`. Pushen tut nur der Inhaber.
- Keine Änderungen in `build/`, weder im Portal noch im WOMS.
- **Sicherheitsblock** in jedem Agent-Auftrag (Memory „feedback-destruktive-skripte“):
- Kein `rm -rf` außerhalb des eigenen Scratch-Ordners, nie `/dev`, `/etc` oder `/` anfassen.
- Kein `docker run`, keine globalen Installationen.
- Das Vorschau-Skript nie mit Ziel aufrufen, nur `--pruefen`. Nie mit Pfaden wie `..` testen.
- `TMPDIR` auf den eigenen Scratch-Ordner setzen.
- Nach großen Läufen prüfen, dass `ls -l /dev/null` `crw-rw-rw-` zeigt.
- **Prod-Daten:** Kein Paket schreibt in die Live-Datenbank. Ausnahmen sind die Setup-Skripte (`scripts/kundenansicht-setup.mjs`) und `scripts/kundenart-bestand.mjs`. Beide laufen nur auf ausdrückliche Anweisung des Controllers nach OK des Inhabers, vorher immer mit `--pruefen`.
## Review Focus
1. **Kundenart leer, unbekannt oder vom Client gefälscht:** Ohne gesetzte Kundenart sieht der Kunde das eine Häkchen, und der Server verlangt es auch. Ein Body-Feld `kundenart` vom Client wird ignoriert. Tests: P5 Task „kundenartWirksam“ und „Einschalten privat in Frist“, P7 Task „Bestellung Fassung 6“.
2. **Pilot-Schalter an den Rändern:** Groß-/Kleinschreibung und Leerraum der E-Mail, `*`, leere Liste, `off` mit `*`, Mitarbeiter-Kundenansicht (`viewAs` + `viewId`) im Pilot. Ohne neue Ansicht liefern die Abo-Modell-Endpunkte 404, und `/dashboard.html` ist die alte Seite. Tests: P0 Tasks „Schalter“ und „Dashboard-Weiche“, P5 Task „Einschalten nur in neuer Ansicht“.
3. **Alte Links nach dem Umbau:** `#termine?bestellt=1&projekt=x`, `#webshop?stripe=zurueck&projekt=x`, `#rechnungen`, `#stunden`, `#gutschrift`, `#wartung`, `#fortschritt`, `?abo=success`, Login mit `next=/dashboard.html#chat`. Jeder Link landet am richtigen neuen Ort, und die Query bleibt erhalten. Test: P1 Task „Routen“.
4. **Doppeltes oder widersprüchliches Einschalten:** Zwei Taps, schon aktiv, zweite Website, Hosting-Kunde bei einer Wartungs-Funktion (Wechsel mit gemerkter Funktion), gescheiterter Wechsel, Privatkunde in der Frist ohne Häkchen, Funktion per Schalter aus. Ergebnis: höchstens ein Ticket, klare 409-Codes, kein Zustand ohne Nachweis. Tests: P5 Task „Einschalten“, P6 Task „Wechsel mit Funktion“.
5. **Vorschuss statt Mehraufwand:** Nicht gedeckte Einrichtungsminuten erscheinen nie als Mehraufwand. Normale Arbeit über dem Guthaben bleibt Mehraufwand. Eine spätere Gutschrift gleicht den Vorschuss zuerst aus. Bei Vertragsende bleibt ein Restminus stehen, wird aber nicht berechnet. Gilt auch für Git-Push-Arbeitsblätter auf Einrichtungs-Tickets. Tests: P6 Task „Vorschuss im Stundenkonto“, P9 Task „Abo-Vorgabe bei Einrichtungs-Tickets“.
---
## Nachträge nach der Planrunde (verbindlich, 16.10.2026)
Die Paket-Pläne wurden parallel geschrieben; ihre Vertragsfragen sind hier entschieden. Diese Nachträge gehen V1–V13 und der Zuständigkeitstabelle vor. Wo ein Paket-Plan davon abweicht, gilt dieser Abschnitt.
Kürzel in den Fundstellen: `Task n` und `VFn` = Task bzw. Vertragsfrage n im genannten Paket-Plan (P0 bis P10), `Fn` = Frage n der Planrunde (`/root/reports/kundenansicht-neu-2026-10-15/planrunde.json`, Feld `vertragsfragen`).
### Schalter und Sitzung
- **N1 Gültige Sitzung (V1).** Kunde heißt `customerId` und `appwriteUserId` ohne `role: 'admin'`; Kundenansicht heißt `viewAs === true` mit `viewId`. Alte Mitarbeiter-Sitzungen (`role: 'admin'` ohne `viewId`) sind in jedem Modus `false`, auch bei `*`. Eine `email`, die kein Text ist, zählt als leer.
(P0 VF3; F12)
- **N2 Schalter für Jobs ohne Cookie (V1).** `schalter.js` bekommt den vierten Export `export function neueAnsichtFuerKunde({ customerId, email }) // boolean`. Gleiche Modus- und Listenregeln wie `neueAnsichtFuerSession` (`off` false, `on` true, `pilot` bei `*` oder getrimmter, klein geschriebener `email` in der Liste), aber ohne Sitzungsprüfung und ohne Mitarbeiter-Regel; leere oder nicht-textliche `customerId` und Nicht-Objekte ergeben `false`. P5 `aboModellFuerKunde(customerId)` liest das Kundendokument und ruft sie; `runAboCheck` und P6 (Team-Aktivierung, Widerruf des Wechsels, Tagesjob) nutzen nur sie, nie `neueAnsichtFuerSession`.
(P0 Task 2, VF10; P5 Task 13; F12)
- **N3 Weiche (V1).** `server/services/kundenansicht/dashboardWeiche.js` ist eine Fabrik (`server/index.js` startet beim Import den Server). Der Direktaufruf `/neu/dashboard.html` wird für Sitzungen ohne neue Ansicht per 302 auf `/dashboard.html` geleitet; Module und CSS unter `/neu/` bleiben frei abrufbar. Bei Modus `on` liefert die Weiche von P10 (`UMSCHALT_SEITEN`, Zeile `/dashboard.html`) die neue Seite auch ohne Sitzung; `neueAnsichtFuerSession` bleibt unverändert.
(P0 VF1, VF2; P10 Task 3, VF7; F12)
- **N4 `/api/auth/me`.** `ansicht` steht nur in den beiden `authenticated: true`-Antworten von `GET /me`. P5 ändert in `auth.js` allein `sanitizeCustomer` (`kundenart`, `kundenartWirksam`); `test/kundenansichtMe.test.js` pinnt nur die Schlüssel der obersten Ebene, damit `customer` wachsen darf.
(P0 Task 4, VF4; P5 Task 2; F13)
- **N5 Platzhalter-Marker.** P0 Task 3 setzt im Platzhalter beide Marker (`<meta name="kn-seite" content="neu">` und `data-kn-platzhalter`) und pinnt in `istNeu` nur die Meta-Zeile. P1 Task 12 ersetzt die Seite, ohne einen P0-Test zu ändern; die Werkzeug-Ansichten `platzhalter` und `platzhalter-abgemeldet` bleiben, bis P1 sie ersetzt.
(P1 VF1, Task 12/13; P0 Task 3; F11)
### Hülle, Routen, Reiter, Zähler
- **N6 Routen (V2, „Pflichtinhalt von ROUTEN“).** Die Zeile „`vertraege`, `abo`, `wartung` → vertraege, reiter `abo`“ entfällt. Neu: `vertraege` → Bereich `vertraege` ohne `reiter` (die Ansicht wählt den Standardreiter, bei offener Rechnung `rechnungen`, sonst `abo`); `abo` und `wartung` → Bereich `vertraege`, reiter `abo`, kanonischer Hash `#abo` (`#wartung` wird `#abo`, `#vertraege` bleibt `#vertraege`); `?abo=success|cancel` → `#abo`. `waehleReiter` (P2) entscheidet nur über `route.reiter`; Reiterklicks navigieren mit `ersetzen: true` zu `#abo`, `#rechnungen`, `#stunden`, `#speicher`, und `ankerKette`/`elternVon` liefern weiter `#vertraege` als Bereichs-Anker.
(P1 Task 3/4, VF4; P2 Task 16, VF8; F17)
- **N7 Routenfelder und Exporte (V2).** Route-Felder zusätzlich `produkt` (`'hosting' | 'wartung'`) und `scroll` (`'aktivitaet'`); `query` ist immer ein String. Zusätzliche Exporte von `routen.js`: `hashFuerAnsicht(name)` (die `data-view`-Brücke der gemeinsamen Module), `startHash`, `ankerKette`, `elternVon`, `titelFuer`, `FUNKTIONS_ETAGEN`. Die Projekt-ID heißt einheitlich `#projekt?projekt=<id>` (Lesen von `id=` bleibt Rückfall); ein `data-project` an einem `[data-view]`-Element wird zu `?projekt=<id>`.
(P1 Task 3, VF4; P2 VF4; F17)
- **N8 Kleinere Festlegungen der Hülle (V2).** `api(…, { roh })` meint einen Rohdaten-Body (`application/octet-stream`) wie `fn-util.js request({ raw })`, nicht eine rohe Antwort; der V2-Kommentar ist so zu lesen. `?abo=` bleibt in `location.search`; P2 räumt mit `history.replaceState(history.state, '', location.pathname + location.hash)` auf, und P4 ändert die `history.replaceState`-Aufrufe der Module nicht (`stempeln()` aus P1 stempelt danach neu). `public/neu/manifest.webmanifest` hat dieselbe `id`, `start_url` und `scope` wie die alte; `sw.js` bleibt gemeinsam und wird in P1 Task 11 registriert. Ansichten rendern keine eigene `h1`; der Titel kommt aus `titelFuer(route)` oder `kn:titel` (P7: `meldeTitel`).
(P1 VF7 bis VF11; P7 Task 10/11/13; F20)
- **N9 Reiter-Baustein und Meldungen (V2, Hülle).** `baueReiter({ eintraege: [{ key, label, kurz? }], aktiv, art, ariaLabel, bei(key) })` in `public/neu/reiter.js` liefert direkt ein `HTMLElement` mit `setzeAktiv`, `setzeAnzeige`, `messen`; P2 und P3 passen sich an. Inhalte in Etage 2 melden Reiter und Titel ohne Import per `CustomEvent` mit `bubbles: true`: `kn:reiter` (`detail { eintraege, aktiv, bei }`, leere Liste = keine Reiter) und `kn:titel` (`detail { text }`).
(P1 Task 5, VF5; P2 Task 16; P3 Task 5; F18)
- **N10 Reiter der Funktionsmodule in Etage 2.** Terminplaner, Webshop, Buchungskalender und Analyse zeichnen im Abo-Modell keine eigene Leiste, sondern melden `kn:reiter` (die Analyse aus `mountCockpit` mit `bei: pickTab`: Übersicht, Heatmap, Einstellungen). Die Ausnahme „außer analyse“ im Gate `p4` entfällt; der Satz „die Analyse behält ihre eigene Leiste“ in F18 ist überholt. Kurzlabels stehen in `KURZLABELS` (`public/fn-reiter.js`: Team, Start, Recht, Preise), die Vorschau prüft sie (C2).
(P4 Task 1/8/15, VF2; P1 VF5; F4, F18)
- **N11 Zähler-Ereignis (V3).** `kn:zaehler-neu` wird am `document` gesendet (`bubbles: true`), `detail.was` ist `'chat' | 'vertraege' | 'alle'`. `zaehler.stand()` liefert nur `{ chat, vertraege }`; die Hülle setzt den Chat-Zähler beim Öffnen des Chats auf 0.
(P1 Task 7/11, VF6; P2 VF2; F19)
- **N12 Zeile „Rechnung offen“.** P1 zeichnet sie nicht und reicht kein `zaehler` an die Ansichten; `zeigeX(wurzel, { route, me, nav })` bleibt wie in V2. P2 liefert `public/neu/rechnung-offen.js` (`offeneRechnungen`, `rechnungOffenZeile`, `ladeOffeneRechnungen`, optional `ladeOffeneRechnungen(apiFn = api)`); P3 `zeigeFunktionen` lädt `GET /api/invoices` selbst über `abh.api` und zeichnet die Zeile in Meine und Entdecken. Fällt der Abruf aus, fehlt nur die Zeile.
(P3 Task 5, VF5; P2 Task 3, VF3; P1 VF6; F1, F19)
- **N13 Option `einschalten` für die Funktionsmodule (V2, Absatz „Funktionsmodule“).** Die Hülle ruft `initNews`, `initTermine`, `initShop`, `initBuchungen`, `initZeiten` als `init*(wurzel, { einschalten })` mit `einschalten(platz, { key, projectId, onFertig, viewAs? }) → Promise`, das lazy `zeigeEinschalten` aus `public/neu/funktion-einschalten.js` (P3) lädt (`MIT_EINSCHALTEN`, `einschaltenOption(lade)` in `huelle.js`); `initAnalyse(wurzel)` bleibt unverändert, die Projektwahl bleibt `wurzel.dataset.wunschProjekt`. `test/knHuelleStart.test.js` („Option einschalten“) pinnt die Verdrahtung; P4 Task 15 ersetzt seine Zeile `command grep -a -n einschalten public/neu/huelle.js` durch den Lauf dieses Tests.
(P1 Task 11, VF12; P3 VF1; P4 VF1; F1, F21)
- **N14 Overlay.** Ansichten dürfen `oeffneOverlay` und `beendeOverlay` aus `router.js` rufen (P3 für die Bestätigungsseite). Es ist nie mehr als ein Overlay offen; P3 und das Konto-Sheet schließen vor dem Öffnen ein offenes Overlay.
(P3 Task 6, VF5; P1 R6; F22)
- **N15 Chat-Composer.** Die neue Seite lädt `/neu/chat-composer.css`, eine lilafreie Kopie von `public/chat-composer.css` (Akzent `#111`/Grau, weder `#9d3dfe` noch Gewicht 800 noch `backdrop-filter`); `/chat-composer.js` bleibt gemeinsam, das alte CSS unverändert. P2 Task 7 legt die Kopie an und ändert dafür genau die zwei Composer-Zeilen in `dashboard.html`; `test/knHuelleSeite.test.js` akzeptiert `/neu/chat-composer.css` statt `/chat-composer.css`.
(P1 VF3; P2 Task 7, VF10; F23)
- **N16 Analyse-Bild und Alt-Klassen.** `public/neu/bilder/analyse.webp` erzeugt P4 über die Werkzeug-Ansicht `kn-modul-analyse` (Fall `m-analyse-aktiv`); bis dahin entfernt der `error`-Handler in „Entdecken“ das fehlende Bild. `public/neu/module.css` Abschnitt 0 (P4) definiert `kb-panel`, `kb-btn*`, `kb-empty`, `kb-link`, `kb-checks`, `abo-actions`, `abo-btn`, `hidden`, `muted`, `error`, außer eine Datei unter `public/neu/` tut es schon; P2 und P3 nutzen eigene `kn-*`-Klassen.
(P3 VF2; P4 Task 9/14, VF3, VF5; F24)
### Katalog, Einschalten, Analyse
- **N17 Katalogfelder (V5).** Jeder `katalog[]`-Eintrag trägt `consent: { version, knopf: 'Einschalten', items: [{ id, text, haken, nurPrivatInFrist? }] }` (`null` bei `analyse`) in der Reihenfolge `einschalten-zeile`, `hinweis`, `beginn` und dazu `hinweis: string | null`, den Text des Items `hinweis` (nur `shop`, `unterkunft`, `termine`: die Pflichten der alten Häkchen `verantwortung`, `stripe`, `gesundheit`). Der Server liefert beides, nie der Client; der Nachweis speichert den Hinweis als Text `hinweis` (`haken: false`). P3 `renderEinschalten` zeichnet `p.kn-fn-hinweis` direkt über dem Knopf (nach der Linkzeile, vor `beginn`), die P0-Fixtures und `vertragNeu` tragen beide Felder.
(P5 Task 6/10/11, VF2; P3 Task 3; P0 Task 6/7, VF11; P8 VF3; F3, F27)
- **N18 `anfrageStatus` (V5).** Der Pfad ist `projects[].features.<shop|unterkunft>.anfrageStatus` (`'' | 'offen' | 'angenommen' | 'abgelehnt' | 'zurueckgezogen'`), gebaut von `anfrageStatusJeProjekt` in `server/services/aboKatalog.js`; P3 liest ihn nur über `anfrageStatusVon(projekt, key)`.
(P0 VF9; P5 Task 11; P3 Task 1; F16)
- **N19 Antwortcodes der Einschalt-Route (V5).** Die Prüfreihenfolge nach P5 Task 10: 400 `zustimmung_fehlt`; Projektprüfung (403, 409 archiviert); dann 409 `nicht_verfuegbar` (danach 503 `unterlagen_fehlen`), `abo_fehlt`, `nur_betrieb`, `bestellung_offen`, `beginn_noetig`, `bereits_eingeschaltet`, `bestellung_laeuft` (Zustand `ordered`), `abgeschaltet` (Zustand `disabled`), `fassung_veraltet`. 503 kommt zusätzlich bei nicht lesbarem Abo oder gescheitertem Ticket; P3 zeigt den Text des Servers.
(P5 Task 10, VF3; P3 VF4; F27)
- **N20 Probezeit (V5).** `aboDeckt` kennt zusätzlich den Grund `beginnt_spaeter` mit `ab` (ISO, = `trialEnd`) für `trialing` mit offener Probezeit (Privatkunde ohne Sofortbeginn-Häkchen). `abo_fehlt` trägt dann `body.ab`; der Text lautet „Dein Abo beginnt am TT.MM.JJJJ. Ab dann kannst du einschalten.“, und P3 (`TEXTE`, `fehlerAktion`) zeigt Text und Datum im Knopf. P6 (`adminActivate`) und P9 kennen den Grund; die Analyse-Eignung meldet in der Probezeit weiter `kein_abo`.
(P5 Task 4/9/10, VF9; P3 VF4; F8)
- **N21 Abrechnung am Funktionsdokument (V7).** Das Attribut `portalFeatures.einrichtungAbrechnung` (string 8, `'abo' | 'projekt' | ''`, angelegt in `scripts/kundenansicht-setup.mjs`) hält die Abrechnung am Dokument, damit `ticketNachlegen` und `holeZeitenNach` den Marker `workorders.einrichtungAbrechnung` erneut setzen. P6 nutzt es für `ticketNachlegen` (Dokumente mit `unlockedBy` `abo` oder `projekt` ohne Stripe-Sitzung heilen über `einschalten`) und für die Ticket-Zeilen in `termine/ticketDetails.js`, `shop/ticketDetails.js`, `unterkunft/ticketDetails.js`; K27 bleibt die Einschalt-Mail der Zeiten.
(P5 Task 1, VF5; P6 VF3, VF4; F27)
- **N22 Analyse im Abo-Modell.** In der neuen Ansicht verlangt `POST /api/funktionen/analyse/activate` `audienceConfirmed === true` (sonst 400 `zielgruppe_fehlt`, du-Text) und schreibt einen Nachweis in `featureConsents`: Linkzeile A1, Zielgruppen-Zeile A2 (`haken: true`), Knopf `Analyse einschalten`, Kundenart und Fassungen; bei einem Fehler wird alles zurückgenommen. P4 sendet `audienceConfirmed: true`, die alte Ansicht bleibt unverändert.
(P5 Task 13, VF8; P4 Task 8, VF4; F29)
### Kundenart und Push-Reihenfolge
- **N23 Kundenart-Pflicht beim Anlegen (V4; Entscheidung des Controllers, F2 und F28 widersprachen sich).** `createCustomerWithPortalAccess` verlangt `kundenart` nur bei `neueAnsichtModus() !== 'off'` (sonst 400 `kundenart_fehlt`); bei `off` ändert sich das Anlegen im WOMS und im Portal nicht. `updateCustomerWithPortalAccess` nimmt `kundenart` weiter optional mit gleicher Prüfung. Die unbedingte Pflicht aus F28 gilt nicht.
(P5 Task 3, VF1; P9 VF3; F2, F28)
- **N24 Push-Reihenfolge (Entscheidung des Controllers).** Der Inhaber pusht in dieser Reihenfolge: (1) `fix/akquise-sichtbar`, falls gewünscht; (2) WOMS-Tag `kn-p9-kundenart`; (3) Portal ab P5; (4) WOMS `kn-p9` zusammen mit P6. Vor dem Setzen von `KUNDENANSICHT_NEU=pilot` muss die WOMS-Kundenart (`kn-p9-kundenart`) live sein. Wird `fix/akquise-sichtbar` später gepusht, rebased P9 darauf, nur auf Anweisung des Controllers. Die Übergabe-Notiz nennt die Reihenfolge und vermerkt, dass die Abo-Vorgabe in WOMS zunächst nur für Pilotkunden wirkt (P6 wertet `abrechnung` nur mit `aboModellFuerKunde` aus).
(P5 VF1, VF10; P9 VF3, VF4; F2, F28)
### Stundenkonto, Team, Mails
- **N25 Antwortformen des Stundenkontos (V8).** `nextExpiry` ist `{ minutes, at } | null`; bei `produkt: 'hosting'` und ohne Abo ist `ledger` gleich `null`. `workorders.einrichtungFuer` und `einrichtungAbrechnung` sind für Mitarbeiter lesbar (Setup-Skript P5). T24 trägt den Block `vorschuss`, wenn beim Einschalten `balanceMinutes <= 0` gilt oder schon `vorschussMinutes > 0` besteht; eine Schätzung je Funktion gibt es nicht.
(P6 Task 2/4, VF6; P9 VF2; F33)
- **N26 Geltung der Team-Aktivierung (V10).** Das neue Verhalten (`aboDeckt`-Prüfung, `einschalten` mit Ticket) gilt nur, wenn `aboModellFuerKunde(customerId)` wahr ist; sonst wird `abrechnung` im Body ignoriert und alles läuft wie heute, mit `on` (P10) für alle. P9 sendet `abrechnung` immer. Die 409-Antwort bei fehlendem Abo ist flach: `{ error, code: 'abo_fehlt', grund, wechsel }` mit `grund` aus `kein_abo | falsches_abo | gekuendigt | nicht_aktiv | beginnt_spaeter`; WOMS zeigt eigene Texte, nie den Kundentext in „du“-Form. Erfolg wie bisher `projects[0].features[key]` mit `state: 'setup'` und `ticketWoid`.
(P6 Task 7, VF2; P9 VF2; F32)
- **N27 Abo-Modell-Dokument (V10, V11).** Ein Dokument gehört zum Abo-Modell, wenn `unlockedBy` in `abo | projekt` steht und `einrichtungAbrechnung` in `abo | projekt` (`istAboModell` in `server/services/aboModell.js`); die Gratis-Zeiten der alten Ansicht (`unlockedBy: 'abo'` ohne Marker) bleiben unberührt. V11 ändert sich: Der Widerruf des Wechsels beendet alle Abo-Modell-Dokumente, deren Schlüssel nur in `wartung` steckt, also auch `projekt`-Dokumente; die Analyse eines Hosting-Kunden bleibt.
(P6 Task 5/13, VF4; F32)
- **N28 Team-Einschalten „im Projekt“ ohne Abo (E9/E21; Entscheidung des Controllers, dem Inhaber in der Übergabe zur Bestätigung vorlegen).** Es gilt die Aufbauphasen-Regel von P6 (`server/services/aboAufbauphase.js`, `ohneAboBisher(customerId)`, P6 Task 7, 8, 10). `abrechnung: 'abo'` braucht ein deckendes Abo, sonst 409 `abo_fehlt`. `abrechnung: 'projekt'` braucht es auch, **außer in der Aufbauphase**: Hatte der Kunde noch nie ein Abo (abgebrochene Bestellungen zählen nicht), wird eingeschaltet und mit 200 plus `warnung: { code: 'abo_fehlt', grund: 'kein_abo' }` geantwortet, flach neben `projects`. Das boolesche `warnung: true` für ein fehlendes K16 bleibt für die alte Ansicht. WOMS zeigt die Warnung (P9), der Wortlaut sagt dem Team: „Wartungs-Abo vor dem Livegang vereinbaren“. Der Tagesabgleich lässt `projekt`-Dokumente in der Aufbauphase in Ruhe. Ab dem ersten Abo gilt die Abo-Regel wie für `abo`: Deckt das Abo die Funktion nicht (Hosting, beendet), endet sie mit K39. Hat der Kunde schon ein nicht deckendes Abo, bleibt es auch im Projekt bei 409.
(P6 Task 7/8/10, VF8; P9 Task 10/11; F5)
- **N29 Mailkennungen (V13).** Belegt sind K38 „Deine Funktion wird eingerichtet“ (Einschalten im Abo-Modell, nicht für `zeiten`, dort bleibt K27); K39 „Deine Funktion wurde beendet“ (News, Terminplaner mit Block `termine`, Webshop, Unterkunft; Platzhalter `funktion`, `projekt`, `loeschtag`); K40 und K41 (Zeiten: Abo-Ende mit „läuft noch bis {{frist}}“, abgeschaltet; ohne Preis, ohne Behalten); T24 ans Team mit Block `vorschuss`; K42 (Webshop-Zusage) und K43 (Buchungskalender-Zusage) im Abo-Modell ohne Preiszeile, ohne „€“ und ohne „bestell…“ (Builder `anfrageEntschiedenMail`, Weiche in `sendeEntscheidung` über `aboModellFuerKunde`). K24 und K34 bleiben bytegleich. K44 und T25 bleiben frei; eine Rückkehr-Mail gibt es nicht (die Rückkehr geschieht still, K39 kündigt sie an), außer bei Zeiten im bestehenden Modul. K02 und K03 bekommen den Block `funktionen_abo`; K22, K29, K30 und T16 bleiben bytegleich. Alle neuen Vorlagen in „du“-Form und im Layout von K16.
(P6 Task 3/4/5/15, VF1, VF5; F31)
- **N30 Behalten-Weg der Zeiten.** Für Abo-Modell-Dokumente ist `POST /api/funktionen/zeiten/behalten` gesperrt (409, P6). Die Route und die Mailtexte K29/K30 (Builder `k29Mail`, `k30Mail` auf K40/K41 umgestellt, Vorlagen-Dateien bleiben) räumt P10 Task 20/21 auf. Die WOMS-Texte (`zeitenStatusText`, Bestätigungstexte „Aktivieren (ohne Zahlung)“) ändert ein eigener WOMS-Nachzug nach P10 Task 22 (Controller, Push durch den Inhaber, geführt in der Checkliste Task 6). Alt-Zweige hinter `neueAnsicht(` bleiben als toter Code; ihre Entfernung entscheidet der Inhaber.
(P6 VF7; P9 VF5; P10 VF11, VF12; F34)
- **N31 Abo-Ende-Verhalten (Rechtstexte = Server).** Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob). Öffnungszeiten live läuft nach dem Abo-Ende 30 Tage unverändert weiter (Zeiten-Modul, K40, P4 Text Z2), danach 30 Tage gespeicherte Eingaben; kein Behalten gegen 30 €. Löschfristen: 30 Tage (Terminplaner, News, Öffnungszeiten), 90 Tage (Webshop, Buchungskalender), 60 Tage (Analyse-Messdaten); ein neues passendes Abo in der Frist holt die Funktion zurück, P6 baut den News-Löschjob. P8 passt Texte und Tests an (R8 „mit P4/P6 abgestimmt“).
(P8 Task 4/7, VF5; P6 Task 8/10; P4 Text Z2; F39)
### Bestellseiten, Fassung 6, Nachweis
- **N32 Pfade der Fassung 6 (V12).** `config.abo.termsVersionNeu = '2026-10-16-v6'` (Entwurfstag, bei der Freigabe wird das Datum gesetzt); `FUNKTIONEN_FASSUNG_V6 = '2026-10-16'`. Bis zum Umschalten gelten die Abo-Seite `/neu/rechtliches/abo-bedingungen.html`, das PDF `/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf` und die Funktionsunterlagen `/neu/rechtliches/<slug>-2026-10-16.html|pdf`; `termsDocFor` liefert sie, `/abo-bedingungen-v6.html` aus V12 entfällt. P7-Fixtures und -Tests nutzen `termsDocFor(config.abo.termsVersionNeu).url` und `config.abo.termsVersionNeu` statt Literalen wie `'2026-10-30-v6'`. P10 Task 14 verschiebt Seite und PDF nach `public/` (`/abo-bedingungen.html`), stellt in `FASSUNGEN` v5 auf die `archiv`-Adressen (`public/vertragsunterlagen/v5/`) und v6 auf die Wurzel-Adressen, setzt 301-Weiterleitungen und passt `DOKUMENTE_V6`, den Modus `abo6` und die festen Links an.
(P8 Task 1/5, VF1; P10 VF3; P7 Task 16; F35)
- **N33 Fassung 6 verdrahten: ein Task, zwei Pläne (V5, V12).** P5 Task 15 und P7 Task 0 beschreiben dieselbe Arbeit und laufen einmal, als erste Aufgabe von P7 im Hauptzweig nach dem Merge von `kn/server` und `kn/recht`, mit den Dateien beider Tasks. Quellen sind `WORTLAUT_V6`, `FUNKTIONEN_FASSUNG_V6`, `FUNKTIONEN_UNTERLAGEN_V6` und `mitFassung` aus `server/services/products.js` (P8 liefert, verdrahtet nicht). Verdrahtet werden `config.aboFunktionen.fassung` und `texte.beginn` (Literale, `config.js` importiert keine Dienste), `consentAbo` mit dem Item `hinweis`, `aboKatalog.js` (`unterlagen`, `hinweis`), `aboUnterlagen.js` und `sichereAdresse` in `funktion-einschalten.js` (lässt `/neu/rechtliches/` zu). `bestelltexte.js` bleibt importfrei, `test/knBestelltexte.test.js` pinnt Linkzeile, Häkchen und Fassungsangabe gleich zu `WORTLAUT_V6` (eine einheitliche Form der Fassungsangabe), danach läuft `test/aboWortlautV6.test.js` grün. Die Analyse (`config.analyse.avvVersion`, `legalPaths`, K18-Weiche mit den PDFs aus `public/neu/rechtliches/`) folgt dieser Verdrahtung erst nach der Freigabe; wo P5 Task 15 sie sofort auf Fassung 6 stellt, gilt diese Regel.
(P5 Task 15, VF7, VF10; P7 Task 0/1, VF12; P8 VF2; F3, F29, F30)
- **N34 Vertragsnachweis (V12).** `speichereVertragsNachweis({ customerId, art, bezug = '', texte, fassungen, kundenart, req, at })` schreibt selbst mit `createDocument` und `texteHash` (aus `featureConsents.js`), weil `speichereZustimmung` `projectId = ''` ablehnt; `featureKey = art`, `projectId = ''`. `bezug` und `wortlaut` (`WORTLAUT_STATUS`) stehen als Schlüssel in `fassungen` (höchstens 1000 Zeichen), `texte` bleibt die reine Anzeigereihenfolge. `stripeSessionId` bleibt leer, weil der Nachweis vor der Stripe-Sitzung, dem Wechsel, der Speicheränderung und dem Antrag entsteht (kein Zustand ohne Nachweis); die Zuordnung läuft über Zeit, Konto und `bezug`.
(P7 Task 2, VF3 bis VF5; P8 VF4; F36)
- **N35 Fassung in den Bestellrouten.** P7 übernimmt die Stellen, die `config.abo.termsVersion` fest lesen: `stripeBilling.js:606-607` (`createAboCheckout`: Fassung aus `consent.termsVersion`, Vorgabe unverändert), `abo.js:236`, `:328`, `:388` (Prüfung) und `abo.js:201` (`GET /api/abo` liefert `termsVersion` über `aktuelleAboFassung(req)`). `fassungDerAnfrage(req, termsVersion)` prüft die Neu-Fassung zuerst; bei gleichen Werten von `termsVersion` und `termsVersionNeu` gilt `neueAnsicht(req) ? 'neu' : null`, sodass P10 Task 14 hier nichts ändert. `server/routes/takeoverCredits.js` `POST /` schreibt den Nachweis bei Fassung 6.
(P7 VF2; P8 VF4; P10 VF4; F36, F42)
- **N36 Speicher und Gutschrift (Fassung 6).** P7 übernimmt `ansicht-speicher.js` (Plus-Block) und `ansicht-gutschrift.js` (Zustimmungsblock) wie `ansicht-bestellen.js` und `ansicht-wechsel.js`: P2 baut sie 1:1 (Fassung 5), P7 baut sie auf das schlanke Muster um. Die Orakel-Vergleiche gegen `app.js` werden durch Pins auf die Fassung-6-Texte ersetzt; der Hash-Pin des Blocks `TAKEOVER_CONDITIONS` (`conditionsVersion` 2026-09-30) bleibt. Die Gutschrift-Bedingungen werden die eigene Ansicht `#gutschrift?bedingungen=1` mit unverändertem Rechtstext; `test/support/knWortlautOrakel.js` entfernt P10 Task 17 erst mit `app.js`.
(P2 VF5; P7 VF1, VF8; P10 Task 17; F25)
- **N37 Texte der Bestellseiten.** Fehlertexte der Dienste (`planSwitch.js`, `mailStorage.js`, `stripeBilling.js`, Abo-Routen) bleiben im Quelltext in Sie-Form und werden für Sitzungen mit neuer Ansicht über `server/services/duFehler.js` in du übersetzt; ein Quelltext-Test meldet jeden neuen Sie-Satz ohne Übersetzung. Die Zeilen „Anbieter“ und „Abrechnung“ entfallen aus dem Block der Bestellseiten (der Anbieter steht im Impressum im Konto) und gehen als Kanzlei-Frage in die Rechtsrunde (N46). K01 und K12 behaupten für Betriebe kein „ausdrücklich verlangt“ mehr (Block `sofortverlangen`). Bewusst der Mail-/Textrunde (Spec 5.7) überlassen: Validierungssätze in `takeoverCredits.js`, `custom_text` der Stripe-Zahlungsseite, die übrigen Mails.
(P7 Task 6/14, VF6, VF7, VF11; F37)
### Öffentliche Seiten und Login
- **N38 Kündigen und Widerrufen in du.** P7 Task 15 stellt `public/neu/oeffentlich/kuendigen.html` und `widerrufen.html` auf du um; `public/legal-form.js` und `public/auftrag-widerruf.js` zeigen du nur im Zweig `data-anrede="du"` (ohne Attribut unverändert). Die Seiten speichern keinen Wortlaut und keine `termsVersion`, es gibt also keine neue Version (C14). Text-Pins in `test/knOeffentlichDu.test.js` ersetzen für diese zwei Seiten den Paarvergleich aus P2 Task 17 (Feldattribute und Skripte werden weiter verglichen); P10 Task 17 stellt die Sie-Pins in `test/publicForms.test.js:1439-1450`, `test/auftraegeKundenportalWiderruf.test.js:9` und `test/auftraegeAblauf.test.js:288` auf diese du-Sätze um.
(P7 Task 15, VF13; P2 Task 17; F6)
- **N39 Neue öffentliche Seiten im Pilot.** Angemeldete Pilot-Sitzungen (`neueAnsicht(req)`) bekommen `/kuendigen.html` und `/widerrufen.html` schon im Pilot aus `public/neu/oeffentlich/`; ausgeloggte Besucher, der Login (`/login.html`) und `/offline.html` bleiben bis `on` alt. P0 setzt das mit zwei weiteren Zeilen in `dashboardWeiche.js` um (sie liefert nur, wenn die Datei existiert, sonst `next()`); P10 Task 3 übernimmt die Zeilen in `UMSCHALT_SEITEN` und passt den Test „off und pilot liefern die Datei von heute“ für diese zwei Seiten an.
(P10 Task 3; P0 Task 3; P2 Task 17; F7)
- **N40 Login, Service Worker, Auftragsseite.** P10 Task 16 (`public/neu/oeffentlich/login.js`) übernimmt `initLoginPage` samt Helfern (`app.js:421-568`) vor dem Löschen von `app.js`; die neue Hülle registriert `/sw.js` selbst (P1 Task 11), P10 zieht in `sw.js` die `OFFLINE_URL` mit. iOS-Fokuszoom: 16-px-Felder in P1/P2-CSS genügen, ein JS-Schutz nur im Login; der doppelte `beforeinstallprompt`-Hörer (Hinweis P2, Konto P1) darf P10 zusammenlegen. P10 nimmt `/auftrag.html` in `UMSCHALT_SEITEN` und in die Nachprüf-Marker auf (Marker: `/neu/oeffentlich/oeffentlich.css` im Quelltext, kein `/auftrag.css`); P2-Tests, die alte Dateien lesen (`app.js`, alte Seiten, `auftrag.css`, `chat-composer.css`), friert P10 vor dem Löschen ein oder entfernt sie.
(P2 VF6; P10 Task 16/17, VF5; F26)
### Werkzeug und Vorschau
- **N41 Werkzeug-Erweiterungen durch P1 bis P4 und P7.** Erlaubt sind nur `views.mjs`, `gates.mjs` (Gate `p<N>`), `fixtures/endpunkte-p<N>.mjs` mit je einem Import in `fixtures/endpunkte.mjs`, `fixtures/faelle-p<N>.mjs` (P4: `fixtures/module.mjs`) mit einem Import in `fixtures/faelle.mjs`, `vertrag-endpunkte.mjs` und der Marker in `test/statik.test.mjs`. Neue Fälle hängen nur in eigenen Dateien hinter den vierzehn Kernfällen; P0 lockert den Pin „Fälle in fester Reihenfolge“ auf `FALL_NAMEN.slice(0, 14)` und den `boot.js`-Test auf diese vierzehn (`boot.js` bekommt `FALL_MUSTER = /^[a-z0-9-]{1,40}$/`, die Leiste zeigt nur Kernfälle). P4 (17 Fälle `m-*`) und P7 (`privat-ohne-abo`, `hosting-privat`) ändern Pin und `boot.js` nicht; ändert ein Paket eine Antwortform, ergänzt es den Vertragseintrag im selben Commit.
(P0 Task 6/11, VF5; P1 VF2; P4 VF5; P7 Task 16, VF10; F14)
- **N42 Strang-Umgebung und Ansicht-IDs.** Die Strang-Umgebung kommt nur über `KN_STRANG_ENV=…/tmp/strang-ui.env`, nie über `KW/strang.env`. Ansicht-IDs tragen das Paketpräfix (`kn-huelle-`, `kn-p2-`, `kn-funktionen-`, `kn-modul-`, `b7-`); wer fremde Ansichten aufruft, nennt die echten IDs (P7: `--only kn-p2-bestellen,kn-p2-bestellen-hosting,kn-p2-wechsel,kn-p2-email,kn-p2-email-plus,kn-p2-gutschrift,kn-p2-gutschrift-frage`). Fixtures für V5, V6 und V8 folgen den Nachträgen oben. Die Ansicht für `/auftrag.html` ergänzt P2 Task 19 mit einem Eintrag in `vorschau/seiten.mjs`.
(P3 VF3; P2 VF7; P7 Task 16; F14)
- **N43 Vorschau-Technik.** `dark-surface` erlaubt genau `#111` (Akzent) für Elemente bis 25 % der Viewport-Fläche; andere dunkle Flächen brauchen `ignorieren` je Ansicht. In der Vorschau hat `public/neu/oeffentlich/<seite>` Vorrang vor `public/<seite>`; `boot.js` feuert nach dem Laden einmal `DOMContentLoaded` und `load`, Code prüft trotzdem `document.readyState`. Der Arbeitsauftrag an den Inhaber (WOMS „Neues Projekt“, Subdomain `kundenansicht-neu`, Status `ready` oder `deployed`) steht in `KW/UEBERGABE-P0.md` und blockiert Phase B nicht; `KN_ERSTLAUF_FREMDES_OK=ja` nur auf ausdrückliche Anweisung des Controllers.
(P0 Task 10/11/14, VF6 bis VF8; F15)
- **N44 WOMS-Werkzeug.** „Playwright nur mit dem Werkzeug aus P0“ gilt für das Portal; für das WOMS gilt der eigene Strang `werkzeug-kundenart` (P9 Task 1). Ein kleiner Auftrag „WOMS-Werkzeug auf `test` heben“ (Aufträge/Projektkonten, Unterkunft, `arbeitsblaetter/abgerechnet`, Reiter „Abgesagt“) läuft vor Phase E. Beide Werkzeuge halten die Speicherbremse (`free -m` über 2000 MB) und starten nie mehr als einen Browser zugleich.
(P9 Task 1, VF1; F41)
### Rechtsrunde
- **N45 Rechtsrunden-Technik.** Die drei fehlenden WOMS-Karten (`leistungsbeschreibung-news`, `leistungsbeschreibung-website-analyse`, `avv-website-analyse`) legt `scripts/rechtliches-hochladen.mjs --karten-anlegen` mit OK des Inhabers vor dem ersten Upload an; `--probe` meldet fehlende als `STOPP: Karte fehlt: <key>`, das Hochladen ohne Karte bricht ab. Die Karte der Analyse-Leistungsbeschreibung bekommt eine abgeleitete ID (`kartenId`, ihr Schlüssel hat 37 Zeichen, erlaubt sind 36), WOMS liest Karten über das Attribut `key`, und die drei Karten werden nicht in `setup-legal.mjs` eingetragen. Die Entwurfsseiten unter `/neu/rechtliches/` gehen mit dem normalen Push live (der Pilot braucht die Links), tragen Entwurfsmarke und `<meta name="robots" content="noindex">` (P8 Task 5 bis 10 ergänzen) und werden bei der Freigabe ersetzt.
(P8 Task 11/13, VF6, VF7; F40)
- **N46 Entscheidungsliste.** `docs/rechtliches/kundenansicht-entscheidungen.md` führt R1 bis R17 (statt R1 bis R15; `test/kundenansichtEntscheidungen.test.js` entsprechend): R16 ist die Du-Anrede der Seiten Kündigen und Widerrufen (N38), R17 sind die Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37), je mit Vorschlag, Alternative und Kanzlei-Frage.
(P8 Task 11; P7 VF7; F6, F37)
### Livegang und P10
- **N47 Vor dem Pilot (Ablaufhinweis 4 ergänzt).** Zwei Schritte vor dem Push des Inhabers, beide mit Sicherung und OK des Inhabers: (1) Compose- und `.env`-Zeilen `KUNDENANSICHT_NEU` (Modus `pilot`) und `KUNDENANSICHT_NEU_PILOT_EMAILS` (Pilot-Liste) in `/home/webklar/apps/docker-compose.yml` (zwei Zeilen `- KUNDENANSICHT_NEU=${KUNDENANSICHT_NEU:-off}` und `- KUNDENANSICHT_NEU_PILOT_EMAILS=${KUNDENANSICHT_NEU_PILOT_EMAILS:-}` bei den `ZEITEN_*`-Zeilen) und `/home/webklar/apps/.env`, Sicherung `.bak-<Datum>-kundenansicht`, dann `docker compose up -d --no-deps kundenbereich-server`, nie `preview-portal`; (2) `scripts/kundenansicht-setup.mjs --pruefen`, nach OK des Inhabers ausführen (sonst scheitert jeder Nachweis ohne `featureConsents.kundenart` mit 500). `UEBERGABE-P0` und die Übergabe-Notiz tragen beide Punkte, P10 Task 9 prüft sie nur noch.
(P0 UEBERGABE-P0; P7 VF9; P10 VF2, VF13, Task 9; Befund zur Übersicht; F9, F38)
- **N48 Vor `on`.** P10 Task 1 und 9 prüfen: `WORTLAUT_STATUS` steht auf `'freigegeben'` (E20); die Gleichheitstests `test/knBestelltexte.test.js`, `test/knVerdrahtung.test.js` und `test/aboWortlautV6.test.js` sind grün; `scripts/rechtliches-hochladen.mjs --probe` meldet keine fehlende Karte und `--pruefen` endet mit Exit 0 (alle elf `documentKey` aus `DOKUMENTE_V6` haben eine aktuelle Version mit passender `source`, deshalb prüft Task 9 Step 2 mit `--pruefen` und nicht nur mit `--probe`); `scripts/kundenart-bestand.mjs` (P10 Task 8) und `scripts/kundenansicht-setup.mjs` sind gelaufen.
(P7 VF9; P8 VF9; P10 VF9, VF13; F38)
- **N49 Bestätigungen zu P10.** Die Alt-Zweige der P4-Dateien entfernt P10 Task 21, die Playwright-Skripte der alten Seite löscht Task 17. Zwischen Task 11 und Task 14 liegen sieben Tage Beobachtung; Task 14 ist der Punkt ohne Rückweg per Schalter und braucht ein eigenes OK des Inhabers. Der Rückbau (Task 20) beschränkt sich auf die beiden Kaufrouten und ihre Helfer; Alt-Preise in `config.js` und die Antwortfelder von `GET /api/funktionen` bleiben.
(P10 VF4, VF6, VF8, VF10, VF11; F42)
- **N50 Commit-Zeile (Global Constraints).** Jede ausführende Sitzung nutzt die `Co-Authored-By`-Zeile aus ihrem eigenen Systemhinweis. Commit-Beispiele in den Plänen mit `Claude Opus 5.5` und Formulierungen wie „wörtlich laut Übersicht“ stehen dafür als Platzhalter.
(alle Pläne, Global Constraints „Commits“; P8 Schlussvermerk; F43)
### Pilot-Nachträge
- **N52 Kurzinfo in „Meine“ (V5, Pilot-Feedback 2026-10-17, Strang kn/meine-pilot).** Mit neuer Ansicht und nur auf Anfrage (`GET /api/funktionen?kurzinfo=1`, sendet allein „Meine“; Etagen, Einschalt-Ablauf und Module fragen ohne den Parameter, dann fehlt das Feld und nichts wird gelesen; Runde 1, 17.10.) trägt jede Funktion unter `projects[].features.<news|termine|shop|unterkunft|zeiten>` zusätzlich `kurzinfo: string | null`, den Text der zweiten Zeile in „Meine“. Nur aktive Funktionen bekommen einen Text, alle anderen `null`; die Analyse trägt kein Feld (ihre Zahlen kommen aus dem Auswertungsdienst, nicht aus Appwrite). Quelle `server/services/kundenansicht/kurzinfo.js`: Terminplaner „3 Termine heute · nächster 10:30“ bzw. „Heute keine Termine“ (bestätigte und erledigte Termine ohne Sperren, Berliner Tag), Webshop „2 neue Bestellungen“ bzw. „Keine offenen Bestellungen“ (Status `bezahlt`), News „4 Beiträge sichtbar“ bzw. „Keine Beiträge sichtbar“ (`isHanging`), Buchungskalender „1 offene Anfrage“ (Status `neu`), sonst „Nächste Anreise 24.10.“ („Anreise heute“, „Anreise morgen“), sonst „Keine offenen Anfragen“, Öffnungszeiten live der Status der Website (`zeitenStatus.text`, ohne eigene Abfrage; ohne gespeicherte Zeiten „Noch keine Öffnungszeiten eingetragen“). Je Funktion höchstens zwei schmale Abfragen (`limit`, `select`, Filter auf Index-Felder), alle gleichzeitig; jede Funktion einzeln auf `kurzinfoDeps.limitMs` (1500 ms) begrenzt, Fehler oder Zeitüberschreitung ergibt `null`, eine volle Seite (Zahl unbekannt) ebenfalls. Die Texte enthalten nie Namen, Adressen oder Beträge. Die Oberfläche zeigt bei `null` „Aktiv“ und setzt die Zeile nur als Text (höchstens 80 Zeichen); eine Pille trägt nur noch eine Zeile, deren Funktion nicht läuft. Ohne neue Ansicht bleibt die Antwort bytegleich und es wird nichts gelesen.
(Pilot-Feedback 2026-10-17; `test/kundenansichtKurzinfo.test.js`, `test/knFunktionen*.test.js`)
### Zuständigkeit
- **N51 Zusätzliche Dateien und geteilte Stellen.** Die folgende Untertabelle ergänzt die Zuständigkeitstabelle um alle in Plänen und Vertragsfragen genannten Zusatzdateien und legt für die Hotspots eine feste Reihenfolge fest. Stubs aus P1 gehören nach der Anlage P2, P3 und P4; P7 übernimmt `ansicht-speicher.js` und `ansicht-gutschrift.js`; P0 besitzt `server/services/kundenansicht/` ganz.
(P0 VF1; P1 VF3; P2 VF1; P4 Dateiübersicht; P5 VF4; P6 VF3; P7 VF1, VF2; P10 VF1; Befund zur Übersicht; F10)
| Datei / Stelle | Besitzer | Reihenfolge, Hinweis |
|---|---|---|
| `server/services/kundenansicht/` ganz (`schalter.js`, `dashboardWeiche.js`), `test/kundenansichtSchalter.test.js`, `kundenansichtDashboard.test.js`, `kundenansichtMe.test.js` | P0 | andere Pakete legen dort nichts ab |
| Werkzeug `views.mjs`, `gates.mjs`, `fixtures/*`, `vertrag-endpunkte.mjs`, `vorschau/seiten.mjs` (eigenes Git) | P0 legt an | P1 bis P4 und P7 ergänzen nur nach N41 |
| `public/neu/manifest.webmanifest`, `test/support/knHuelle.js`, `test/knHuelle*.test.js` | P1 | P2 bis P4 dürfen die Testhilfe nutzen |
| Stubs aus P1: `public/neu/ansicht-{website,chat,vertraege,passwort,gutschrift,wechsel,bestellen}.js`, `ansichten.css`; `ansicht-funktionen.js`, `funktionen.css`; `module.css` | P1 legt an; danach P2, P3, P4 | Besitz geht nach der Anlage an das Paket der Tabelle oben |
| `public/neu/dashboard.html` | P1 | P2 ändert genau zwei Zeilen (Composer-Skript, Stil `/neu/chat-composer.css`) |
| `public/neu/ansicht-hilfen.js`, `rechnung-offen.js`, `install-hinweis.js`, `website-details.js`, `vertraege-rechnungen.js`, `vertraege-abo.js`, `chat-composer.css`, `oeffentlich/oeffentlich.css`, `oeffentlich/auftrag.html`, `test/support/knDaten.js`, `knWortlautOrakel.js`, `test/knAnsicht*.test.js` | P2 | `knWortlautOrakel.js` friert P10 vor dem Löschen von `app.js` ein |
| `public/neu/ansicht-bestellen.js`, `ansicht-wechsel.js`, `ansicht-speicher.js`, `ansicht-gutschrift.js` | P2 baut 1:1, P7 baut um | P7 erst nach dem Merge von UI und Recht |
| `public/neu/funktion-einschalten.js`, `test/support/knFunktionenDaten.js`, `test/knEinschalten*.test.js`, `knFunktionen*.test.js` | P3 | P7 Task 0 ändert nur `sichereAdresse` |
| `public/neu/bilder/*` | P3 | `analyse.webp` erzeugt P4 |
| `public/fn-reiter.js` (neu), `test/support/modulAbo.js`, `test/kundenansichtNeuModul*.test.js`, Pins `test/shopWiring.test.js:31`, `test/unterkunftWiring.test.js:130` | P4 | |
| `server/services/aboUnterlagen.js`, `aboKatalog.js`, `funktionEinschalten.js` (neu), `featureRegistry.js` (auch `consentAbo*`, `abowegAnalyse`), `test/featureSetup.test.js:191-192`, `test/aboFassung6Verdrahtung.test.js` | P5 | Verdrahtung der Fassung 6 siehe N33 |
| P5, minimal in fremden Dateien: `server/routes/admin/customers.js` (`code`), `server/services/ticketAdmin.js` (`abrechnung`), `server/services/zeiten/einschalten.js` (`abrechnung`, `vorpruefung`), `server/services/analyse.js`, `server/services/shop/anfrage.js` (`stelleAnfrage`), `server/routes/shop.js` (Anfrage) | P5 | P6 vermeidet diese Funktionen |
| `server/services/aboModell.js`, `aboFunktionenMails.js`, `aboFunktionenMeldung.js`, `aboAufbauphase.js`, `aboFunktionenAbgleich.js` (alle neu) | P6 | Aufbauphasen-Regel siehe N28 |
| Mailvorlagen K38 bis K43, T24; Block `funktionen_abo` in K02 und K03 | P6 | K44 und T25 frei (N29) |
| P6, minimal in fremden Dateien: `server/routes/admin/funktionenAdmin.js`, `termine/aboAbgleich.js`, `zeiten/aboAbgleich.js`, `zeiten/mails.js`, `zeiten/behalten.js`, `termine/ticketDetails.js`, `termine/einbauanleitung.js`, `shop/ticketDetails.js`, `unterkunft/ticketDetails.js`, `shop/anfrage.js` und `unterkunft/anfrageFunktion.js` (nur `sendeEntscheidung`) | P6 | nur die in Plan 07 benannten Stellen |
| `server/services/bestelltexte.js`, `duFehler.js`, `vertragsNachweis.js`, `public/neu/bestell-bausteine.js`, `test/support/knBestellDaten.js`, `test/kn*.test.js` (Plan 08) | P7 | |
| P7, minimal in fremden Dateien: `server/services/stripeBilling.js` (`createAboCheckout`), `server/routes/takeoverCredits.js`, `server/mail-templates/K12 Wechsel Wartungs-Abo.html` (Block `sofortverlangen`), `public/neu/oeffentlich/kuendigen.html`, `widerrufen.html`, `public/legal-form.js`, `public/auftrag-widerruf.js` (nur `data-anrede="du"`) | P7 | |
| `test/aboWortlautV6.test.js`, `aboBedingungenV6.test.js`, `legalV6Build.test.js`, `funktionenRechtV6.test.js`, `kundenansichtEntscheidungen.test.js`, `rechtlichesHochladen.test.js` | P8 | |
| `server/routes/umschaltSeiten.js`, `scripts/umschalten-nachpruefung.mjs`, `scripts/kauf-bestand-pruefen.mjs`, `docs/umschalten-kundenansicht.md`, `test/support/dateiListe.js`, `modulKlassen.json`, `oeffentlichStand.json`, `public/neu/oeffentlich/login.js`, `public/sw.js:5`, Tests `knUmschalt*`, `knFassung6Fest`, `knLoginModul`, `knAufraeumen`, `knKaufRueckbau`, `knAltZweigeWeg`, `knSchalterWeg`, `knOfflineCache`, `kundenartBestand`, `kaufBestandPruefen` | P10 | Rückbau der Kaufpfade und Alt-Zweige erst in Phase F |
| **geteilt:** `server/routes/abo.js` | P6, dann P7 | P6: K02/K03, `POST /switch`, `GET /`, `withdrawOrder`, Widerruf des Wechsels; P7: nur die Pfade der Fassung 6 und `router.use` für `duFehler` |
| **geteilt:** `server/services/planSwitch.js` | P6, dann P7 | P6: `details.funktion`, `completeSwitch`, `markSwitchFailed`; P7: `switchToWartung` (Z. 347-351, 388, 483), `performSwitch` (`details`, K12-Aufruf) |
| **geteilt:** `server/services/siteFeatures.js` | P5, dann P6 | P5: `featureView`, `legeTicketAn`; P6: `adminActivate`, `ticketNachlegen` |
| **geteilt:** `server/index.js` | je eine eigene Zeile | P0 Weiche vor `express.static`; P5 1 Import, 1 Mount; P6 1 Import, 1 Intervallblock; P10 eine Zeile direkt vor `app.use(express.static(publicDir))` |
| **geteilt:** `server/services/products.js` | P8, dann P10 | P8: Fassungstabelle, `WORTLAUT_V6` und Konstanten; P10: Zeilen v6 und v5 der Tabelle |
| **geteilt:** `server/config.js` | je eigener Abschnitt | P0 `kundenansichtNeu`, P5 `aboFunktionen`, P8 `abo.termsVersionNeu`; Ausnahme: die Literale `aboFunktionen.fassung` und `.texte.beginn` setzt die Verdrahtung (N33) |
| **geteilt:** `server/services/featureRegistry.js`, `funktionEinschalten.js` | P5 | `consentAbo` ändert nur die Verdrahtung (N33); P6 fügt in `funktionEinschalten.js` genau einen Aufruf `meldeEingeschaltet` ein |
| **geteilt:** `public/neu/ansichten.css` | P2, dann P7 | P7 hängt einen Abschnitt an |
| **geteilt:** `test/mailTemplates.test.js` | P6, dann P7 | P6: Z. 9, 11, 718; P7: Z. 379 |
---
## Verträge
### V1 Pilot-Schalter (P0)
`server/config.js`:
```js
kundenansichtNeu: { mode: 'off'|'pilot'|'on', pilotEmails: string[] } // aus KUNDENANSICHT_NEU, KUNDENANSICHT_NEU_PILOT_EMAILS (commaList)
```
`server/services/kundenansicht/schalter.js`:
```js
export function neueAnsichtModus() // 'off' | 'pilot' | 'on'
export function neueAnsichtFuerSession(session) // boolean
export function neueAnsicht(req) // = neueAnsichtFuerSession(req.session ?? parsePortalSession(req))
```
Regeln für `neueAnsichtFuerSession`:
- `off` → false.
- `on` → true für jede gültige Kunden- oder Kundenansicht-Sitzung.
- `pilot` → true, wenn eine dieser Bedingungen gilt:
- `session.viewAs === true && session.viewId` (Mitarbeiter-Kundenansicht),
- die Liste enthält `'*'`,
- die Liste enthält die getrimmte, klein geschriebene `session.email`.
- Ohne Sitzung → false.
Auslieferung und Clients:
- `app.get('/dashboard.html')` steht **vor** `express.static` (`server/index.js:154`). Gilt die neue Ansicht, liefert die Route `res.sendFile(public/neu/dashboard.html)` mit `Cache-Control: no-cache`. Sonst `next()`, und die alte Seite kommt wie heute.
- `GET /api/auth/me` bekommt zusätzlich `ansicht: 'neu' | 'alt'`.
- Die alte `app.js` lädt ohne Weiche. Die neue Seite lädt nur `/neu/*`-Dateien und die gemeinsamen Funktionsmodule.
### V2 Dateien und Module der neuen Oberfläche (P1, P2, P3)
**Grundregeln**
- Alle neuen Dateien liegen unter `public/neu/`. ES-Module (`<script type="module" src="/neu/huelle.js">`), kein Build.
- Wurzel `<body class="kn">`. Klassen und Tokens tragen das Präfix `kn-` bzw. `--kn-`.
- Die gemeinsamen Funktionsmodule behalten ihre Klassen (`tm-*`, `sh-*`, `bk-*`, `zt-*`, `an-*`, `fn-*`, `kb-*`). Ihr neues Aussehen steht in `public/neu/module.css` (P4) unter `.kn`.
**`public/neu/dom.js`**
```js
export function h(tag, attrs = {}, ...kinder) // wie termine-dom.js/shop-dom.js; attrs.on = { click: fn }
export function leeren(el)
export function text(el, wert)
```
**`public/neu/api.js`**
```js
export async function api(pfad, { method = 'GET', body, roh = false } = {})
// Wirft ApiFehler { status, code, message, field, body }. Bei code 'session_revoked' Redirect auf /login.html.
// Netzwerkfehler: status 0.
export class ApiFehler extends Error {}
```
**`public/neu/routen.js`** (rein, ohne DOM, testbar)
```js
export const BEREICHE = ['website', 'funktionen', 'chat', 'vertraege']
export const ROUTEN // hash → { bereich, reiter?: string, etage?: string }
export function routeAusHash(hash) // '#termine?bestellt=1' → { bereich:'funktionen', etage:'termine', query:'bestellt=1' }
export function hashFuerRoute(route) // Umkehrung
```
Pflichtinhalt von `ROUTEN`:
- `''`/`'#'` → website
- `projekt` → website, etage `projekt`
- `fortschritt` → website (scrollt zur Aktivität)
- `funktionen` → funktionen, reiter `meine`; `entdecken` → funktionen, reiter `entdecken`
- `news`, `termine`, `webshop`, `buchungen`, `zeiten`, `analyse` → funktionen, etage = gleicher Name
- `chat` → chat
- `vertraege`, `abo`, `wartung` → vertraege, reiter `abo`
- `rechnungen` → vertraege, reiter `rechnungen`
- `stunden` → vertraege, reiter `stunden`
- `speicher` (neu) → vertraege, reiter `email`
- `gutschrift` → vertraege, etage `gutschrift`
- `wechsel` → vertraege, etage `wechsel`
- `bestellen-hosting`, `bestellen-wartung` → vertraege, etage `bestellen`, plus Produkt
- `passwort` → website, etage `passwort` (Rückweg = Herkunft)
**`public/neu/router.js`**
```js
export function starteRouter({ zeige }) // liest location.hash, hört auf popstate, ruft zeige(route, { nav: 'tab'|'push'|'back'|'same' })
export function geheZu(hash, { ersetzen = false } = {}) // Reiterwechsel: replaceState; Etage 2, Sheet, Seiten: pushState
```
**Hülle**
- `huelle.js` (Einstieg): lädt `/api/auth/me` und leitet ab wie heute.
- `leiste.js`: Fahrstuhl-Zeile Handy, Menü links am Desktop.
- `konto.js`: Konto-Sheet bzw. Popover.
- `fristen-streifen.js`, `zaehler.js`.
- `etage.js`: Etage 2 mit Zurück und Reitern.
- `reiter.js`: gemeinsame Unterstrich- und Balken-Reiter.
**Ansichten.** Jede exportiert:
```js
export async function zeigeX(wurzel, { route, me, nav }) // rendert in wurzel, darf Daten selbst laden
```
**Funktionsmodule** werden von der neuen Hülle wie heute aufgerufen: `initTermine(document.getElementById('termine-body'))` usw. Die Projektwahl übergibt die Hülle einheitlich als `wurzel.dataset.wunschProjekt` (C1). News und Analyse lesen dasselbe Attribut zusätzlich zu ihren bisherigen Modulvariablen (P4).
### V3 Fahrstuhl-Zeile und Hülle (P1)
- **Referenz** ist `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html`: Balken mit Overlay-Kopie und Gegenbewegung, Rückfall `fb`, Etage 2 als `reel` mit `translateY(-56px)`, Blatt `t2-dp`.
- **Handy-Layout** gilt mit derselben Medienabfrage wie heute: `(max-width: 880px), (pointer: coarse) and (max-height: 500px)`.
- **Leiste:**
- Wörter: `Website`, `Funktionen`, `Chat`, `Verträge`. Schrift 15 px/600, bei Platzmangel 14 px.
- Balken: Radius 6, Höhe 36 px.
- Tap-Ziele mindestens 44 px. Unten `env(safe-area-inset-bottom)`.
- Bei offener Tastatur wird die Leiste ausgeblendet. Die `visualViewport`-Logik von heute wird übernommen.
- **Etage 2:** „Zurück“ und die Reiter der Unterseite. Hat die Unterseite keine Reiter, füllt „Zurück“ die ganze Zeile. Bei Platzmangel scrollt die Zeile waagerecht, und der aktive Reiter wird in den sichtbaren Bereich gerückt (C2).
- **Desktop:** Menü links mit 232 px und Haarlinie rechts, aktiver Eintrag schwarz. Unterseiten zeigen eine Brotkrume „Funktionen / Terminplaner“. Oben rechts das Profilbild, das Konto öffnet als Popover.
- **Konto** (Reihenfolge laut Spec 5.2):
1. Name und E-Mail
2. Passwort ändern
3. Datenschutz
4. Impressum
5. App installieren (nur wenn sinnvoll)
6. Verträge kündigen (`/kuendigen.html`)
7. Vertrag widerrufen (`/widerrufen.html`, immer da; hervorgehoben mit der Pille „Frist läuft“, wenn `GET /api/fristen` `anzeigen` liefert)
8. Abmelden
- **Widerrufsstreifen:** erscheint, wenn `anzeigen === true`, mit dem Text `Widerrufsfrist bis TT.MM. · Widerrufen` (früheste Frist, V6). Nicht wegtippbar, bei offener Tastatur ausgeblendet.
- **Zähler:**
- Chat: `GET /api/chat/unread-count`, Poll alle 45 s wie heute.
- Verträge: Zahl der Rechnungen mit `balance > 0` aus `GET /api/invoices` plus Rückfragen aus `GET /api/abo/takeover-credits` (`status === 'rueckfrage'`). Beides wird nach einer Zahlung, bei der Rückkehr in den Tab und beim Reiterwechsel neu geladen.
- „!“ steht bei `subscription.paymentProblem`. **Nicht** bei `switchPending`.
- **Bewegung reduziert:** keine Federn, nur 150 ms Überblenden.
### V4 Kundenart (P5)
`server/services/kundenart.js`:
```js
export const KUNDENARTEN = ['unternehmen', 'privat'] // Re-Export aus auftraege/konstanten.js
export function kundenartGespeichert(customerDoc) // 'unternehmen' | 'privat' | ''
export function kundenartWirksam(customerDoc) // 'unternehmen' | 'privat' (leer/unbekannt → 'privat')
```
**Wo die Kundenart erscheint**
- `GET /api/auth/me` liefert in `customer` zusätzlich `kundenart: kundenartGespeichert(doc)` und `kundenartWirksam: kundenartWirksam(doc)`. Das gilt für alle drei Zweige (Login, Kundenansicht, Kunde).
**Anlegen und Ändern (`customerAdmin.js`)**
- `createCustomerWithPortalAccess(payload)` nimmt `kundenart`. Pflichtwert ist `'unternehmen' | 'privat'`, sonst 400 mit `code: 'kundenart_fehlt'`.
- `updateCustomerWithPortalAccess` nimmt `kundenart` optional, mit gleicher Prüfung.
- `sanitizeCustomer` gibt `kundenart` zurück.
**Nachweis**
- `featureConsents` bekommt das Attribut `kundenart` (string 12). Es wird in `scripts/kundenansicht-setup.mjs` angelegt und von `test/support/schema.js` gelesen.
- Jede neue Zustimmung speichert `kundenartWirksam` zum Klickzeitpunkt.
### V5 Katalog und Einschalten (P5)
**Abo-Zuordnung.** `server/services/aboFunktionen.js`:
```js
export const ENTHALTEN_IN = Object.freeze({ analyse: ['hosting','wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'] })
export async function aboProdukt(customerId) // { produkt: 'hosting'|'wartung'|null, laeuft: boolean, gekuendigt: boolean, widerrufsfristBis: string|null /*ISO, nur Abschluss/Wechsel*/ }
export async function aboDeckt(customerId, key) // { ok: true } | { ok: false, grund: 'kein_abo'|'falsches_abo'|'gekuendigt'|'nicht_aktiv' }
```
- `laeuft` gilt für die Status `active | trialing | past_due`, wie `ACTIVE_STATUSES`.
- `gekuendigt` bedeutet „Kündigung vorgemerkt“. `aboDeckt` liefert dann `gekuendigt`, eingeschaltet wird nicht.
**`GET /api/funktionen`.** Ohne neue Ansicht bleibt die Antwort unverändert. Mit neuer Ansicht kommen hinzu:
```js
modell: 'abo',
kundenart: 'unternehmen'|'privat',
abo: { produkt, laeuft, gekuendigt, widerrufsfristBis },
katalog: [{
key, // Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse
name, beschreibung, // beschreibung: höchstens 2 Sätze, du-Form
bild: '/neu/bilder/<key>.webp',
enthaltenIn: 'hosting'|'wartung', // kleinstes Abo, das die Funktion enthält
verfuegbar: boolean, // Schalter der Funktion für diesen Kunden (off → false, Pilot → Liste)
ablauf: 'direkt'|'fragebogen'|'anfrage', // termine: fragebogen; shop, unterkunft: anfrage; sonst direkt
einrichtung: string, // Dauer, z. B. 'meist in 1–2 Wochen'
unterlagen: { leistungsbeschreibung: {version,url}|null, avv: {version,url}|null },
nurBetrieb: boolean // shop: true (C15)
}]
```
- Je Projekt und Funktion zusätzlich `anfrageStatus: ''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen'` (nur Shop und Unterkunft).
- Auf Anfrage (`?kurzinfo=1`, nur „Meine“) je Projekt und Funktion zusätzlich `kurzinfo: string | null` (Live-Detail der Zeile in „Meine“, nur aktive Funktionen, Fehler → `null`; ohne den Parameter fehlt das Feld; N52).
- Funktionen mit `verfuegbar: false` stehen trotzdem im Katalog. Die Oberfläche blendet sie aus (C9).
- Die Analyse erscheint im Katalog nur, wenn `ANALYSE_ENABLED === 'on'`.
**`POST /api/funktionen/:key/einschalten`.** Ohne neue Ansicht → 404. In der Kundenansicht → 403. Für `analyse` → 404, die Analyse nutzt weiter `POST /api/funktionen/analyse/activate`.
Body:
```js
{ projectId, zustimmung: { version, haken: [] | ['beginn'] } }
```
Antwort 200:
```js
{ feature: featureView(doc, key), ticketWoid }
```
Fehler 409 mit `code`, Prüfreihenfolge:
1. `nicht_verfuegbar` (Schalter)
2. `abo_fehlt` mit `body.grund` und `body.wechsel: true`, wenn Hosting läuft und die Funktion Wartung braucht
3. `nur_betrieb` (Shop bei privat)
4. `bestellung_offen` mit `body.offen`, wie heute: Fragebogen beim Terminplaner, Anfrage nicht angenommen bei Shop und Unterkunft
5. `beginn_noetig`, wenn `kundenartWirksam === 'privat'`, die Widerrufsfrist des Abos läuft und `haken` nicht `'beginn'` enthält; `body.ab` nennt das ISO-Datum nach Fristende
6. `bereits_eingeschaltet` (Zustand `setup` oder `active`)
7. `fassung_veraltet`
Erfolg:
- `einschalten({ customerId, projectId, key, by: 'kunde', abrechnung: 'abo', nachweis, now })` (V7).
- Der Nachweis kommt in `featureConsents` mit Texten. Pflicht sind `einschalten-zeile` (Linkzeile) und `knopf` (`Einschalten`). Das Häkchen `beginn` kommt nur dazu, wenn es nötig war.
**Zustimmungs-Register für das Abo-Modell.** Jede Funktion im Register bekommt `consentAbo`:
```js
{ version, knopf: 'Einschalten', items: [{ id: 'einschalten-zeile', text, haken: false }, { id: 'beginn', text, haken: true, nurPrivatInFrist: true }] }
```
- Den Wortlaut von `beginn` legt P8 fest. Bis dahin gilt der Entwurf aus Spec 5.6, Punkt 3, für die Einrichtung.
- Das Item `gesundheit` entfällt im Abo-Modell. Der Fragebogen sperrt den Gesundheitsbereich weiter.
**Anfragen im Abo-Modell**
- Shop: `unternehmer` wird nicht mehr vom Client verlangt, sondern aus `kundenartWirksam === 'unternehmen'` gesetzt. Privat führt zu 409 `nur_betrieb`.
- `keineAusgeschlossenen` bleibt Pflicht als Satz über dem Knopf. Der Client sendet `true` beim Klick auf „Anfrage senden“, der Nachweis kommt in die Anfrage.
### V6 Fristen (P5)
`server/services/fristen.js`:
```js
export async function sammleFristen(customerId, { now = new Date() } = {})
// → { fristen: [{ quelle: 'abschluss'|'wechsel'|'speicher'|'funktion'|'projekt', bezeichnung, bis /*ISO*/, featureKey?, projektName? }],
// fruehestesEnde: string|null, anzeigen: boolean }
```
- `anzeigen` ist wahr, wenn `kundenartWirksam(customer) === 'privat'` gilt und `fristen.length > 0`.
- Quellen: `openWithdrawals` (Abo), `paidFeatureOrders` + `withdrawalDeadline` (gekaufte Funktionen) und die Aufträge (nur bei `AUFTRAEGE_ENABLED` und privat).
Route `GET /api/fristen` (`server/routes/fristen.js`, Mount `/api/fristen`, `requireSession`). Antwort wie oben. In der Kundenansicht ist der Aufruf erlaubt, weil er lesend ist.
### V7 Generisches Einschalten und Tickets (P5, P6, P9)
`server/services/funktionEinschalten.js`:
```js
export async function einschalten({ customerId, projectId, key, by, abrechnung, nachweis = null, staff = null, now = new Date() })
// by: 'kunde'|'team'; abrechnung: 'abo'|'projekt'
// → { doc, ticketWoid }
```
- Setzt `state: 'setup'`, `unlockedBy: abrechnung === 'projekt' ? 'projekt' : 'abo'` und `unlockedByPurchase: false`.
- Legt das Ticket über `createFeatureTicket` an, unter der Sperre `feature-order:${customerId}:${projectId}`.
- Für `zeiten` nutzt die Funktion den bestehenden Weg (`einschaltenZeiten`, mit `unlockedBy` je nach `abrechnung`).
**Ticket-Attribute** (`workorders`, angelegt in `scripts/kundenansicht-setup.mjs`):
- `einrichtungFuer` (string 16): Funktionsschlüssel
- `einrichtungAbrechnung` (string 8): `'abo' | 'projekt' | ''`
Gesetzt werden sie von `createFeatureTicket`, sobald der Aufrufer `abrechnung` übergibt (neuer optionaler Parameter). Der Ticket-Text sagt bei `abo`: „Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).“ Bei `projekt`: „Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).“
**`unlockedBy`** hat die Werte `'kauf'|'abo'|'team'|'projekt'`. `unlockedByVon` und `featureView` liefern `unlockedBy` künftig für **alle** Funktionen, nicht nur für `aboInklusive`.
### V8 Stundenkonto mit Vorschuss (P6)
`computeLedger(credits, usage, opts)`:
- Ein Eintrag mit `einrichtung === true` ist gesetzt, wenn das Arbeitsblatt zu einem Ticket mit `einrichtungAbrechnung === 'abo'` gehört. Den Join macht `listPlanUsage`, auch für Git-Blätter.
- Der ungedeckte Rest eines solchen Eintrags zählt als `vorschussMinutes`, nicht als `overageMinutes`.
- Gedeckt wird wie heute: FIFO, rückwirkend, `expiresAt` nach der Arbeitszeit. **Neu:** Gutschriften, die erst nach der Arbeitszeit gebucht wurden, decken Einrichtungseinträge vor jedem anderen Eintrag. So gleicht eine spätere Gutschrift zuerst den Vorschuss aus.
- Ergebnis zusätzlich:
```js
{ vorschussMinutes, usage: [{ ..., einrichtung: boolean, vorschussMinutes }] }
```
- Bei `active === false` bleibt `vorschussMinutes` stehen. Die Oberfläche zeigt dann „verfällt, wird nicht berechnet“.
- Team-Endpunkt `GET /api/team/customers/:id/stundenkonto` (`requireStaffJwt`):
```js
{ produkt: 'hosting'|'wartung'|null, ledger: { balanceMinutes, vorschussMinutes, overageMinutes, nextExpiry } | null }
```
### V9 Kundenart und Abo-Vorgabe im WOMS (P9 gegen V4, V7, V8)
- **Pflichtfeld Kundenart**, ohne Vorgabe. Die Auswahl erfolgt per Segment-Schalter `.seg` mit den Labels „Unternehmen“ und „Privatperson“, Werte aus `KUNDENART_OPTIONEN` ohne den Leer-Eintrag. Es steht an diesen Stellen:
- `CreateLeadForm`: `createLead` schreibt `kundenart`.
- `CreateCustomerForm`: Body an `POST /api/admin/customers` mit `kundenart`.
- „Zum festen Kunden machen“: Upgrade nur mit gesetzter Kundenart.
- Arbeitsblatt-Fluss „Zugesagt“ (`worksheetFlow.js`): Ohne Kundenart wird nicht hochgestuft, das Team bekommt einen Hinweis.
- **Kundenliste:** Schalter „Kundenart fehlt“ in der Toolbar, kein neuer Reiter.
- **Arbeitsblätter:** Bei Tickets mit `einrichtungAbrechnung === 'abo'` steht `countsToPlan` standardmäßig auf an, im Modal und beim Nachtragen der Zeit eines Git-Blatts. Bei `'projekt'` steht es auf aus.
- **Ticket-Seite eines Einrichtungs-Tickets:** zeigt Kontostand und Vorschuss aus V8.
- **Funktionen-Reiter:**
- Kein Preis mehr (`funktionenView.js:22`).
- „Aktivieren“ fragt „aus dem Abo“ oder „im Projekt enthalten“ und sendet `abrechnung`.
- Die Abo-Prüfung macht der Server (V10). Antwortet er mit `abo_fehlt`, zeigt WOMS den Grund.
### V10 Team-Aktivierung (P6)
`POST /api/team/funktionen/:customerId/:key` mit `action: 'activate'` nimmt zusätzlich `abrechnung: 'abo'|'projekt'` (Vorgabe `'abo'`).
- Ohne `aboDeckt(customerId, key).ok` → 409 mit `code: 'abo_fehlt'` und `grund` (E21). Ausnahme: Dokumente mit `unlockedBy === 'kauf'` (Bestand).
- Die Aktivierung läuft über `einschalten({ by: 'team', abrechnung, staff })` mit Ticket. Wie bisher wird nicht direkt `active` gesetzt. Ausnahme: `action: 'activate'` auf einen Zustand `disabled` mit `disabledReason === 'team'` setzt wie heute direkt `active`.
- Die Textform-Regeln bleiben, wie sie heute sind (`textformFuerAktivierung`).
### V11 Wechsel mit gemerkter Funktion (P6)
- `POST /api/abo/switch` nimmt optional `funktion: { key, projectId }`, aber nur mit neuer Ansicht. Ohne neue Ansicht wird das Feld ignoriert. Es wird in `details.funktion` des Ereignisses `wechsel` gespeichert.
- `GET /api/abo` liefert in `subscription` zusätzlich `wechselFunktion: { key, projectId } | null`, nur mit neuer Ansicht:
- gesetzt, wenn das jüngste `wechsel`-Ereignis `erledigt` ist, `details.funktion` hat und diese Funktion für das Projekt noch `off` ist, längstens 14 Tage nach `doneAt`;
- bei `gescheitert` oder `widerrufen` → `null`.
- Ein Widerruf des Wechsels beendet zusätzlich alle Funktionen mit `unlockedBy === 'abo'`, deren Schlüssel nur in `wartung` steckt (`disable(..., { reason: 'abo_ende' })`).
- **Client** (P3 → P2/P7): Antwortet „Einschalten“ mit `abo_fehlt` und `wechsel: true`, schreibt die Oberfläche `sessionStorage['kn-wechsel-funktion'] = JSON.stringify({ key, projectId })` in einem try/catch und geht zu `#wechsel`. `ansicht-wechsel.js` sendet den Wert als `funktion` mit und löscht ihn nach Erfolg. Steht in `GET /api/abo` ein `subscription.wechselFunktion`, öffnet `ansicht-funktionen.js` beim nächsten Aufruf den Einschalt-Ablauf dieser Funktion.
### V12 Abo-Fassung 6 und Bestellseiten (P8, P7)
`server/config.js` (P8):
```js
abo.termsVersionNeu = '<JJJJ-MM-TT>-v6' // das Datum setzt P8; config.abo.termsVersion bleibt v5
```
`server/services/products.js` (P8): Statt `endsWith` steht dort eine Fassungstabelle.
```js
export const FASSUNGEN // [{ muster: /-v6$/, url:'/abo-bedingungen-v6.html'?, pdfFile, endParagraph }, { muster: /-v5$/, ... v5-Archiv }, { muster: /-v4$/, ... }, { sonst: v3 }]
export function aktuelleAboFassung(req) // neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion
export function isTermsV4OrNewer(t) // v4, v5, v6
```
- `termsDocFor` und `contractAttachmentFor` lesen aus der Tabelle.
- Die Seite der Fassung 6 entsteht unter einem eigenen Pfad, solange der Schalter nicht `on` ist. Erst P10 macht sie zu `/abo-bedingungen.html`, nach Archivierung von v5 unter `public/vertragsunterlagen/v5/`. Den Pfad legt P8 fest und trägt ihn in diese Übersicht nach.
Server-Regeln für Fassung 6 (P7). Sie gelten nur, wenn der Body `termsVersion === config.abo.termsVersionNeu` trägt und `neueAnsicht(req)` gilt:
**`POST /api/abo/checkout`**
- `acceptTerms === true` (der Client sendet es beim Klick auf „Zahlungspflichtig bestellen“), dazu `earlyStart: boolean`.
- `unternehmen` → `startChoice: 'sofort'`, `earlyStartConsentAt: ''`, Grund `kundenart: 'unternehmen'` im Ereignis.
- `privat` und `earlyStart` → `sofort` mit `earlyStartConsentAt`.
- `privat` ohne `earlyStart` → `nach_frist`.
**`POST /api/abo/switch`**
- `earlyStart` ist nur bei `privat` Pflicht.
- Bei `unternehmen` gilt der Wechsel ohne `earlyStartConsentAt`, der Grund steht im Ereignis. `planSwitch.js:351` bekommt dafür den Parameter `kundenart`.
**`POST /api/abo/storage`:** unverändert. Das Plus sendet `acceptTerms: true` beim Klick.
**Nachweis.** `server/services/vertragsNachweis.js`:
```js
export async function speichereVertragsNachweis({ customerId, art: 'abo'|'wechsel'|'speicher'|'gutschrift', bezug, texte, fassungen, kundenart, req, at })
```
- Speichert in `featureConsents` mit `featureKey = art` und `projectId = ''`.
- `stripeSessionId` ist die Sitzungs-ID, wenn vorhanden.
- `texte` = die angezeigten Zeilen, Häkchen und Knopf.
### V13 Mails (P6)
- Neue Kennungen K38–K41 und T24–T25 (frei laut `test/mailTemplates.test.js`). Belegt werden:
- **K38** „Deine Funktion wird eingerichtet“ (Einschalten im Abo-Modell)
- **K39** „Deine Funktion wurde beendet“ (Abo-Ende für News, Shop, Unterkunft im Abo-Modell; Platzhalter `funktion`, `projekt`, `loeschtag`)
- **T24** „Funktion eingeschaltet (Abo)“ ans Team, mit Block `vorschuss`, wenn das Guthaben nicht reicht
- K40, K41 und T25 bleiben frei für P6, falls nötig. P6 trägt die Belegung nach.
- Alle neuen Vorlagen sind in „du“-Form und im Layout von K16.
---
## Ablaufhinweise für den Controller
1. **P0 zuerst.** P0 endet mit dem Tag `kn-p0` und dem Arbeitsauftrag an den Inhaber: In WOMS „Neues Projekt“ (Website, interner Kunde) mit Subdomain `kundenansicht-neu` anlegen, damit `https://kundenansicht-neu.project.webklar.com` existiert. Vorher wird die Vorschau nicht ausgeliefert.
2. **Phase B:** vier Stränge parallel. P9 baut gegen V4, V7, V8 und V10, mit dem Testbackend der WOMS-Vorschau (Whitelist wie `customerAdmin.js`, siehe Karte woms-vorschau Fund 2).
3. **Merges** in den Hauptzweig nach der Reihenfolge oben. Danach jeweils die betroffenen Testdateien mit den Befehlen aus den Global Constraints.
4. **Phase E:**
- ganze Suite Portal und WOMS
- Werkzeug-Gates aller Ansichten auf Handy, Tablet und Desktop
- Vorschau aktualisieren (nur `cp`)
- Abschlussprüfung mit vier Blickwinkeln: Recht und Nachweise, alte Ansicht unverändert, Handy und Barrierefreiheit, Abo-Modell und Stundenkonto
- Korrekturen
- **Vor dem Push des Inhabers, beide mit Sicherung und OK des Inhabers (Nachtrag N47):**
- Compose- und `.env`-Zeilen `KUNDENANSICHT_NEU` (Modus `pilot`) und `KUNDENANSICHT_NEU_PILOT_EMAILS` (Pilot-Liste) anlegen, Sicherung `.bak-<Datum>-kundenansicht`, dann `docker compose up -d --no-deps kundenbereich-server`, nie `preview-portal`. `pilot` erst setzen, wenn WOMS `kn-p9-kundenart` live ist (Nachtrag N24).
- `scripts/kundenansicht-setup.mjs --pruefen`, nach OK des Inhabers ausführen (sonst scheitert jeder Nachweis ohne `featureConsents.kundenart` mit 500).
- Übergabe-Notiz unter `/root/reports/kundenansicht-neu-2026-10-15/uebergabe/` mit den Push-Befehlen für den Inhaber, der Push-Reihenfolge (N24) und den beiden Schritten oben
5. **Rechtsrunde:** P8 legt `docs/rechtliches/kundenansicht-entscheidungen.md` an. Der Controller geht die Liste mit dem Inhaber durch (E20). Hochgeladen wird erst nach seiner Freigabe, mit `scripts/rechtliches-hochladen.mjs`, zuerst `--probe`.
6. **P10** startet erst, wenn der Inhaber ausdrücklich „umschalten“ sagt.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,587 @@
# Öffnungszeiten live (Funktion im Kundenportal) – Design
> **Hinweis 2026-10-06:** Die Mails dieser Funktion heißen inzwischen K27 (eingeschaltet), K28 (Feiertage prüfen), K29 (Abo beendet) und K30 (abgeschaltet), weil K24–K26 seit dem Webshop-Livegang vergeben sind; das Feld `k24At` heißt `einschaltMailAt`. Dieses Dokument behält die alten Nummern.
Stand: 2026-10-04 · Status: vom Inhaber im Chat freigegeben (Abschnitte 1–5), Spec zur Durchsicht
Branch `feature/oeffnungszeiten` (Worktree `dev-zeiten`), aufgesetzt auf `feature/terminplaner-tp2` (6ae0f67).
WOMS: eigener Worktree auf `feature/terminplaner-tp2` des WOMS-Repos (6f03712), Branch `feature/oeffnungszeiten`.
Ideenfindung und Bewertung: `/root/reports/funktionen-ultra-update-2026-10-04/ideen.md` (21 Ideen, 3 Jurys),
Code-Fakten: `/root/reports/funktionen-ultra-update-2026-10-04/design-fakten.md`; Spec-Prüfung gegen den Code
(19 Punkte) eingearbeitet.
Keine Rechtsberatung – Texte gehen vor Livegang in die Rechtsrunde (Abschnitt 9).
## Ziel
Die „Killer-Funktion“ des Ultra-Updates im Funktionen-Tab. Der Kunde trägt seine Öffnungszeiten **einmal**
im Portal ein. Seine Website zeigt daraufhin überall dieselben Zeiten und einen Live-Status („Jetzt geöffnet ·
schließt um 18 Uhr“). Feiertage des Bundeslands sind schon eingerechnet, Betriebsurlaub und Sondertage
ebenso. Dazu kommen eine zeitgesteuerte Hinweis-Leiste oben auf der Seite, eine Kontakt-Leiste auf dem Handy
(Anrufen, Route, WhatsApp) und Google-Daten im Seitencode. Eine Mail erinnert vor Feiertagen, und ein Knopf
gibt die Zeiten für das Google-Unternehmensprofil aus. Alles läuft auf dem eigenen Server, ohne
Google-Cloud-Projekt und ohne Cookies.
## Entscheidungen (vom Inhaber)
| # | Entscheidung |
|---|---|
| Z1 | Eine einzelne Killer-Funktion statt eines Pakets: **Öffnungszeiten live** (mit Hinweis-Leiste). |
| Z2 | Umfang V1: Kern (Wochenplan mit bis zu 2 Fenstern/Tag, Live-Status, Feiertage des Bundeslands, Ausnahmen) **plus** Hinweis-Leiste, mobile Kontakt-Leiste, Feiertags-Erinnerung mit Google-Hilfe, JSON-LD. |
| Z3 | Google nur ohne Google Cloud. Automatisches Schreiben ins Unternehmensprofil braucht die Business-Profile-API (Cloud-Projekt, Zugangsantrag mit bis zu 14 Tagen Prüfung, vorher 0 Anfragen/Minute, Profil ≥ 60 Tage bestätigt mit Website). Deshalb V1 als Umweg: Kopieren in Googles Reihenfolge + Link zur Profilverwaltung + Erinnerung. Echter Abgleich ist eine mögliche spätere Stufe. |
| Z4 | Preis: **30 € einmalig** für jeden Kunden; mit **laufendem Wartungs-Abo ohne Aufpreis enthalten**. |
| Z5 | Einbau **hybrid**: Hinweis-Leiste, Kontakt-Leiste und JSON-LD kommen über das Skript, das der Runner beim Deploy einsetzt; Zeiten-Tabelle/Status/Badge setzt das Team per WOMS-Ticket an die richtige Stelle und ersetzt die fest eingetragenen Zeiten. |
| Z6 | **Mehrere benannte Zeitpläne** je Website; der Hauptplan steuert Live-Status und Leisten. |
| Z7 | Abo-Kunden **schalten selbst ein**; der Klick legt das Einbau-Ticket an. |
| Z8 | Launch-Zugabe: nur eine **Live-Station auf der Relaunch-Seite**, keine Ankündigungsmail, kein Portal-Banner. |
| Z9 | Ansatz A: **eigenes Modul** (eigene Collection, eigene öffentliche Schnittstelle, eigenes Skript), kein allgemeiner Inhalts-Baukasten, nicht aus den Terminplaner-Zeiten abgeleitet. |
| Z10 | Gebaut **auf dem TP2-Branch** (Funktions-Register `config.features`); live zusammen mit oder direkt nach TP2. |
| Z11 | Abo endet bei Gratis-Freischaltung: Mail mit „für 30 € behalten“, nach **30 Tagen** ohne Zahlung abgeschaltet, Daten noch 30 Tage, auf der Seite bleibt die Ersatz-Tabelle. |
Vom Inhaber in den Abschnitten 1–5 mitgetragene Festlegungen, die ich vorgeschlagen habe:
| # | Festlegung |
|---|---|
| Z12 | Bezahlen ist gesperrt, solange ein Wartungs-Abo läuft und nicht gekündigt ist; ein gekündigtes Abo führt auf den 30-€-Weg. |
| Z13 | Der Editor ist ab Bestellen/Einschalten offen und wird nie gesperrt. |
| Z14 | Hinweis-Leiste **schiebt den Inhalt** (liegt nicht darüber); kein Schließen-Knopf (keine Speicherung im Browser). |
| Z15 | Kontakt-Leiste bleibt automatisch aus, wenn die Seite schon eine eigene feste Anruf-Leiste hat. |
| Z16 | JSON-LD erzeugt das Skript im Browser: vorhandener LocalBusiness-Knoten wird ergänzt, sonst ein kleiner Block angelegt. Kein Neubau je Zeitenänderung. |
| Z17 | In jedem Platzhalter bleibt eine **statische Ersatz-Tabelle** stehen (ohne JavaScript, nach Abschaltung). Sie zeigt den Stand beim Einbau; Pflege siehe 4.5. |
| Z18 | Erinnerung **7 Tage** vorher, eine Mail für alle Tage im Fenster; Heiligabend, Silvester und landesweit geführte, aber örtlich nicht überall geltende Feiertage als „bitte prüfen“. |
| Z19 | „Im Wartungs-Abo enthalten“ ist ein **freiwilliges Extra**, keine Änderung der Abo-Bedingungen. |
| Z20 | Im Preis (und im Abo ohne Anrechnung aufs Stundenkontingent) ist **ein Einbau je Website** enthalten; Sonderwünsche nach Angebot/Stunden. |
| Z21 | Relaunch-Station: 4. Eintrag der Funktionen-Gruppe, „Bald verfügbar“ bis zum Portal-Livegang, kein Preis, Uhr-Regler bedienbar mit Automatiklauf. |
| Z22 | Schalter `ZEITEN_ENABLED=off|pilot|on` mit Pilotliste `ZEITEN_PILOT_EMAILS` wie beim Terminplaner. |
| Z23 | „Route“ öffnet Google Maps (`https://www.google.com/maps/dir/?api=1&destination=…`, erst beim Klick). |
## 1. Bausteine
1. **Register-Eintrag `zeiten`** in `config.features` (TP2): Katalogpreis `zeitenSetup` (3000 Cent, einmalig),
`requiresWartungsAbo: false`, `ticketFreischaltung: true`, neu `aboInklusive: true`, `consentFrei`
(Zustimmung für den Gratisweg), `teilweiseLive: ['setup', 'active']`; Eintrag in `STANDARD_HAKEN`
(`featureRegistry.js`) mit `verfuegbar` (Schalter/Pilot) und `freischaltbar` (4.4).
2. **Collections** `oeffnungszeiten` und `zeitenErinnerungen`; neue Felder an `portalFeatures`.
3. **Portal-Server:** Dienst `server/services/zeiten/` (Zeitlogik, Speichern, öffentliche Antwort, Freischalten,
Abgleich, Erinnerung), Kundenrouten `server/routes/zeiten.js`, öffentliche Route
`server/routes/publicZeiten.js`, Gratisweg `POST /api/funktionen/zeiten/einschalten`, Behalten-Checkout.
4. **Portal-Oberfläche:** Kachel im Funktionen-Tab (`zeitenAngebot` in `GET /api/funktionen`) und
Editor-Ansicht `public/zeiten*.js`.
5. **Website-Skript** `public/embed/zeiten.js` (ohne Abhängigkeiten).
6. **Runner:** zweite Einsetzung neben der Analyse, Feld `zeiten` im Build-Auftrag.
7. **WOMS:** Funktionen-Register, Team-Aktionen und -Einstellungen, Protokoll-Label, Rechtliches-Karte.
8. **Mails** K24–K27, Register-Texte für K15/K16/T16, Team-Ticket „Ersatz-Tabelle aktualisieren“.
9. **Rechtstexte** (Abschnitt 9) und **Relaunch-Station** (Abschnitt 10, eigenes Repo).
## 2. Daten
### 2.1 `oeffnungszeiten` (neu)
Dokument-ID = ID des zugehörigen `portalFeatures`-Dokuments (TP2-Konvention, `featureDocId(projectId,
'zeiten')`). Felder: `customerId`, `projectId`, `featureId`, `version` (Integer), `daten` (JSON-String,
≤ 32 KB), `teamVersion` (Integer), `team` (JSON-String, nur über WOMS änderbar), `ersatzHash` (String, Hash des
Hauptplan-Wochenplans beim letzten Ersatz-HTML), `updatedAt`, `updatedBy` (`kunde` | `team:<staffId>`).
Rechte wie die übrigen Funktions-Collections (nur Server-Key, Label-Team). Alle Schreibzugriffe laufen unter
`withLocks(['zeiten:<projectId>'])`; Versionen werden innerhalb der Sperre geprüft und hochgezählt.
`daten`:
```
{
land: 'RP', // Bundesland-Kürzel (16); Vorgabe RP
plaene: [{ id, kurz, name, haupt: true|false, // kurz = unveränderlicher Kurzname [a-z0-9-]{1,24}, im Editor sichtbar
tage: { 1..7: { modus: 'zu'|'zeiten'|'rund'|'vereinbarung',
fenster: [['09:00','12:30'], ['13:30','18:00']] } } }],
feiertage: { 'YYYY-MM-DD': { modus, fenster } }, // nur Abweichungen von der Vorgabe
ausnahmen: [{ id, art: 'urlaub'|'sondertag', von, bis, text, modus, fenster, plan: null|planId }],
hinweise: [{ id, art: 'info'|'urlaub'|'aktion'|'wichtig', text, knopf: null|{ art: 'link'|'anruf', ziel, text },
von, bis }], // von/bis = Kalendertage (YYYY-MM-DD), Berliner Zeit
kontakt: { an: bool, telefon, adresse, whatsapp: null|{ nummer, starttext, bestaetigtAt } }
}
```
`team`: `{ navSelektor: null|string, kontaktleisteAus: bool, hinweisleisteAus: bool }`.
**Grenzen und Prüfregeln** (Server, Fehler je Feld):
- Höchstens 5 Pläne, genau ein Hauptplan, der letzte Plan ist nicht löschbar.
- Zeiten `HH:MM` in 5-Minuten-Schritten; je Tag ≤ 2 Fenster ohne Überschneidung; `bis == von` ungültig;
`bis < von` (über Mitternacht) nur beim letzten Fenster des Tages und nur, wenn es nicht in das erste
Fenster des Folgetags hineinragt.
- Ausnahmen ≤ 100, Hinweise ≤ 20; Urlaub `von ≤ bis`, höchstens 366 Tage; „Aktion“ braucht `bis`.
Vergangene Ausnahmen/Hinweise und vergangene `feiertage`-Schlüssel (> 30 Tage) entfernt der Server beim
Speichern.
- Längen: Hinweistext ≤ 140, Plan-/Ausnahmetext ≤ 60, Knopftext ≤ 30, Starttext ≤ 200, Adresse ≤ 200,
Telefon ≤ 30, Link ≤ 300 Zeichen.
- `knopf.ziel` bei `link`: `new URL` muss gelingen, nur `https:`, ohne Benutzer/Passwort; bei `anruf`: nur
Ziffern, Leerzeichen, `+`, `/`, `-`. Telefon und WhatsApp-Nummer werden auf Ziffern (mit führendem `49`
für deutsche Nummern) normalisiert.
- `navSelektor` (nur Team): ≤ 120 Zeichen, nur `[A-Za-z0-9 #.\-_>:\[\]="']`, kein `<`, `{`, `}`, `;`.
- Schreib-Limit 60 Speichervorgänge/Stunde je Kunde (`writeLimiter`).
### 2.2 `zeitenErinnerungen` (neu)
Dokument-ID = sha1(`featureId|datum`) gekürzt; Felder `featureId`, `datum`, `createdAt`. Anlegen mit 409 =
„schon erinnert“. Der Tagesjob löscht Merker, deren Datum > 30 Tage zurückliegt.
### 2.3 `portalFeatures` (Erweiterung, Schema-Skript `feature-setup.mjs`)
- `unlockedBy` (String 8): `kauf` | `abo` | `team`. `unlockedByPurchase` bleibt und wird abgeleitet
(`kauf` ⇒ true, sonst false).
- `aboEndeAt` (Datetime): Zeitpunkt, an dem der Abgleich das Ende des Abos erkannt hat.
- `einbau` (String 12): `angestossen` | `manuell` (Ergebnis des Neubau-Anstoßes, 4.6).
- `behaltenSessionId` (String 128): offene Behalten-Zahlung (3.3).
- `k24At` (Datetime): Marker der K24 (Selbstheilung, 3.2).
- `featureView` liefert zusätzlich `unlockedBy`, `aboEndeAt`, `einbau`, `loeschungAm` (auch für `zeiten`).
## 3. Portal
### 3.1 Kachel „Öffnungszeiten live“
`GET /api/funktionen` liefert `zeitenAngebot` je Projekt (analog `termineAngebot`), mit
`bestellbar: null` bei einem Abo-Lesefehler (Kachel zeigt dann „Gerade nicht prüfbar – bitte später erneut“).
- **off, Wartungs-Abo läuft, nicht gekündigt, `active`:** „Im Wartungs-Abo enthalten“ · Haken
Leistungsbeschreibung · Knopf **„Einschalten“** (3.2).
- **off, Wartungs-Abo läuft, nicht gekündigt, aber nicht `active` (Probezeit, Zahlung offen):** „Im
Wartungs-Abo enthalten – einschaltbar, sobald dein Abo aktiv ist“; kein Bezahlen.
- **off, sonst** (kein Wartungs-Abo, Hosting-Abo, Kündigung vorgemerkt): „30 € einmalig“ · Bestellseite aus
dem Register (Haken Leistungsbeschreibung und Beginn-Verlangen) · **„Zahlungspflichtig bestellen · 30 €“**.
- **ordered:** „Zahlung wird bestätigt …“.
- **setup:** je nach `einbau`: „Hinweis- und Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen
(meist in wenigen Minuten)“ bzw. „Unser Team baut die Funktion ein (Ticket #…)“; bei leeren Plänen
zusätzlich „Trag jetzt deine Zeiten ein – erst dann kann das Team die Tabelle einbauen“ ·
**„Zeiten bearbeiten“**.
- **active:** Kurzstatus der Website („Jetzt geöffnet · schließt um 18 Uhr“) · **„Zeiten bearbeiten“**.
- **active/setup mit `aboEndeAt`:** zusätzlich „Dein Wartungs-Abo ist beendet – behalte die Funktion bis
<Datum> für einmalig 30 €“ · Knopf zum Behalten-Checkout (3.3).
- **disabled:** Grundtext aus `disabledMessage`; bei `abo_ende` bis zur Löschung Knopf
„Für 30 € wieder einschalten“ (3.3).
Bei `ZEITEN_ENABLED=off` erscheint die Kachel nicht; bei `pilot` nur für die Pilotliste.
### 3.2 Gratisweg `POST /api/funktionen/zeiten/einschalten`
Vorbild `activateAnalyse` (`server/services/analyse.js`). Stufen in dieser Reihenfolge, gleiche Fehlertexte
wie der Checkout: Kundenansicht (viewAs ⇒ 403, Mitarbeiter schalten nicht für den Kunden ein) → Zustimmung
(`consentFrei`, Fassung) → eigenes, nicht archiviertes Projekt → Schalter/Pilot → Unterlagen bereit. Dann unter
der Sperre `feature-order`:
1. Abo frisch prüfen: `bestellbar()` direkt (ohne Memo). `kein_abo`/`gekuendigt` ⇒ 409 mit Hinweis auf den
30-€-Weg; `nicht_aktiv` ⇒ 409 „sobald dein Abo aktiv ist“; Lesefehler ⇒ 503, nie stillschweigend erlaubt.
2. Steht das Dokument schon auf `setup`/`active` mit `unlockedBy: 'abo'` ⇒ **idempotent**: fehlende Schritte
(Ticket, K24) nachholen, 200 mit aktuellem Zustand. Andere Zustände außer `off` ⇒ 409
`orderedConflictMessage`.
3. Zustimmung speichern (`speichereZustimmung` ohne `stripeSessionId`).
4. Feature-Dokument auf `setup`, `unlockedBy: 'abo'`.
5. Neubau anstoßen (4.6) und `einbau` setzen.
6. Einbau-Ticket anlegen (Register-Text mit Ergebnis aus Schritt 5), dann K24 (Text nach `einbau`), dann
`k24At` – jeweils erst nach Erfolg.
**Selbstheilung:** Der 5-Minuten-Job (`runActivation`) legt für `zeiten` in `setup` ohne `ticketId` das
Ticket nach und sendet K24 nach, wenn `unlockedBy: 'abo'` und `k24At` leer ist. Ein erneuter Klick heilt
ebenso (Schritt 2).
### 3.3 Bezahlweg und Behalten
**Neubestellung** über den TP2-Checkout je Funktion; der Register-Eintrag liefert Preis, Zustimmung, Ticket-
und Mailtexte. Zusatz: Wartungs-Abo läuft und ist nicht gekündigt (`bestellbar` ok oder Grund `nicht_aktiv`)
⇒ 409 `inklusive` („Im Wartungs-Abo bereits enthalten – einfach einschalten“).
**Behalten** (neuer Modus, nicht über `markOrdered`/`markPaid` der Neubestellung, weil diese bei
`setup`/`active`/`disabled` mit 409 bzw. Anomalie antworten):
- `POST /api/funktionen/zeiten/behalten` (eigener Kunde, kein viewAs; nur wenn `aboEndeAt` gesetzt oder
`disabled` mit `abo_ende` und Daten vorhanden): gleiche Zustimmung wie die Neubestellung, Stripe Checkout
mit Metadata `zweck: 'behalten'`, `behaltenSessionId` am Dokument. **Der Zustand bleibt unverändert**
(Leisten bleiben während der Zahlung live).
- Webhook-Zweig `zweck: 'behalten'` unter `feature-admin:<projectId>`: Session passt zu
`behaltenSessionId` ⇒ `unlockedBy: 'kauf'`, `aboEndeAt` leer, `behaltenSessionId` leer; war das Dokument
`disabled`, zurück auf `active` (Ticket war geschlossen) bzw. `setup`, Neubau anstoßen (4.6).
**Kein neues Einbau-Ticket.** K15 mit Behalten-Wortlaut. Passt die Session nicht (schon behalten, Daten
gelöscht) ⇒ T19 „Erstattung fällig“ wie bei anderen Anomalien.
### 3.4 Editor (Ansicht `#zeiten`)
Oben eine **Live-Vorschau**: „So steht es gerade auf deiner Seite: *Jetzt geöffnet · schließt um 18 Uhr*“
(dieselbe Serverlogik) und ein Regler **„Uhrzeit testen“** (Tag + Uhrzeit, `GET /api/zeiten/:projectId/
vorschau?zeit=…`). Vor dem ersten Speichern: leerer Hauptplan und „Trag deine Öffnungszeiten ein“.
Darunter sechs Bereiche:
1. **Zeitpläne:** Hauptplan „Öffnungszeiten“ (Kurzname `oeffnungszeiten`), „Plan hinzufügen“ (Name, der
Kurzname wird einmalig daraus gebildet und angezeigt), Hauptplan wechseln, Plan löschen (nicht den letzten,
nicht den Hauptplan). Je Wochentag: Haken „geöffnet“, zwei `<input type="time" step="300">`-Paare,
Auswahl „rund um die Uhr“ / „nach Vereinbarung“.
2. **Feiertage:** Bundesland-Auswahl; Liste der Feiertage der nächsten 12 Monate mit Namen; je Eintrag
„geschlossen“ (Vorgabe) oder eigene Zeiten. „Bitte prüfen“ (Vorgabe: Wochenplan, nicht „geschlossen“):
Heiligabend, Silvester sowie Feiertage, die `feiertagejs` landesweit führt, die aber nur örtlich gelten
(Mariä Himmelfahrt in BY, Fronleichnam in SN und TH). Hinweis: Gemeinde-Feiertage sind nicht enthalten.
3. **Ausnahmen:** Betriebsurlaub (von–bis, Text) und Sondertag (Datum, geschlossen oder Zeiten); Geltung
„alle Pläne“ (Vorgabe) oder ein Plan; Liste kommend/vergangen. Beim Speichern eines Urlaubs ≥ 2 Tage:
„Hinweis-Leiste dazu anlegen?“ (füllt Text und Zeitraum vor; sichtbar ab 7 Tage vor Beginn bis Urlaubsende).
4. **Hinweis-Leiste:** Text, Art (Info/Urlaub/Aktion/Wichtig mit festen Farben), optionaler Knopf
(Link `https://…` oder Anrufen), Zeitraum von–bis (Kalendertage); „Aktion“ verlangt ein Enddatum. Enthält
der Text „%“ oder „statt“: Hinweis zu Preisangaben (Endpreise, bei Waren niedrigster Preis der letzten
30 Tage). Liste geplanter Hinweise.
5. **Kontakt-Leiste (Handy):** Ein/Aus; Telefon; Adresse für „Route“; WhatsApp optional mit Nummer,
Starttext und Pflicht-Haken „Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen“
(Zeitpunkt in `bestaetigtAt`; in der Kundenansicht setzen Mitarbeiter den Haken **nicht** – 403 wie
`routes/termine.js`).
6. **Google:** Knopf **„Für Google kopieren“** (Zwischenablage: Montag–Sonntag im Format
„Montag: 09:00–12:30, 13:30–18:00“, „geschlossen“, danach Sonderzeiten der nächsten 60 Tage) und Link
„Google-Profil öffnen“ (`https://business.google.com/`). Text: Die Google-Daten im Seitencode laufen
automatisch.
Speichern: `PUT /api/zeiten/:projectId` mit `version`; veraltete Version ⇒ 409 „Inzwischen geändert – bitte
neu laden“. Mitarbeiter können über die Kundenansicht speichern; Protokoll
`VIEW_ACTIONS += 'zeiten_gespeichert'` (Test-Pin und WOMS-`viewLog.js`-Label anpassen).
## 4. Website
### 4.1 Öffentliche Schnittstelle `GET /api/public/zeiten/v1/:siteKey`
CORS `*`, nur GET, `Cache-Control: public, max-age=60`, weiches ETag über den Body **ohne** `stand`,
Speichercache 30 s je siteKey als **LRU mit höchstens 500 Einträgen** (auch für „unbekannt“), 120
Anfragen/Minute je IP, `Access-Control-Expose-Headers: Date`. siteKey-Format wird vor jedem Appwrite-Zugriff
geprüft. Unbekannter Schlüssel, Schalter aus, Kunde gelöscht oder Zustand nicht in `setup`/`active` ⇒
`{ aktiv: false }`. Keine internen IDs, keine Kundendaten außer den veröffentlichten Kontaktangaben.
Antwort (gekürzt):
```
{ aktiv: true, stand: <ISO>,
plaene: [{ kurz, name, haupt,
woche: [{ tag: 1, modus, fenster }],
tage: [{ datum, art: 'regulaer'|'feiertag'|'urlaub'|'sondertag', name, modus, fenster }], // 60 Tage
wechsel: [{ ab: <epoch-ms>, offen: true|false|null, text, kurz }] }], // 8 Tage
hinweise: [{ art, text, knopf, ab: <epoch-ms>, bis: <epoch-ms>, rang }], // aktuell gültige + Beginn in 8 Tagen
kontakt: null|{ telefon, routeUrl, whatsappUrl },
jsonld: { openingHoursSpecification: [...], specialOpeningHoursSpecification: [...] },
firma: { name, adresse },
team: { navSelektor, kontaktleisteAus, hinweisleisteAus } }
```
- `hinweise[].ab/bis` rechnet der Server aus den Kalendertagen in Berliner Zeit (Beginn 00:00, Ende 24:00).
Der Browser zeigt zu jedem Zeitpunkt den gültigen Hinweis mit dem höchsten `rang`
(Wichtig > Urlaub > Aktion > Info; bei Gleichstand späterer Beginn).
- `firma.name` = Firmenname des Kunden; fehlt das Kundendokument, Projektname, sonst leer (nie 500).
`firma.adresse` = `kontakt.adresse`.
- Solange keine Zeiten gespeichert sind, ist `plaene` leer: kein Status, kein JSON-LD, Platzhalter behalten
ihr Ersatz-HTML; Leisten erscheinen nur, wenn eingerichtet.
- `whatsappUrl` = `https://wa.me/<ziffern>?text=<encodeURIComponent(starttext)>`; `routeUrl` nach Z23.
### 4.2 Skript `zeiten.js`
IIFE ohne Abhängigkeiten, Schutz gegen doppeltes Laden (`window.__wkZeiten`). siteKey aus `data-site` des
eigenen Script-Tags oder aus `[data-webklar-zeiten]`; API-Basis aus `data-api` oder der Herkunft des Skripts.
**Ein** Abruf je Seite für alle Teile. Der Browser rechnet keine Feiertage und keine Zeitzonen: Er wählt aus
`wechsel` den letzten Eintrag mit `ab ≤ jetzt`, stellt einen Timer auf den nächsten Wechsel (Statuswechsel
oder Hinweis-Beginn/-Ende), lädt höchstens alle 10 Minuten und bei `visibilitychange` neu. „Jetzt“ =
`Date.now()`; weicht die Uhr laut `Date`-Header um mehr als 5 Minuten ab, rechnet das Skript mit der
Serverzeit. Schlägt der Abruf fehl oder sind die Daten älter als 24 h: Leisten aus, Platzhalter behalten ihr
Ersatz-HTML. Keine Cookies, kein Storage, keine Anfragen an Dritte; Kundentexte nur über `textContent`;
Links aus der API werden im Browser erneut geprüft (`https:`/`tel:`), `rel="noopener noreferrer"`;
`querySelectorAll(navSelektor)` in try/catch.
### 4.3 Teile auf der Seite
1. **Hinweis-Leiste:** Shadow-DOM-Host als erstes Kind von `body`, schiebt den Inhalt. Feste/klebende
Elemente mit `top: 0` (oder `team.navSelektor`) werden, solange die Leiste sichtbar ist, um
`max(0, Leistenhöhe − scrollY)` nach unten gerückt (Inline-`top`, beim Entfernen zurückgesetzt);
`scroll-padding-top` wird um die Höhe erhöht. Farben je Art mit Kontrast ≥ 4,5:1; `role="region"`,
`aria-label="Hinweis"`. Aus bei `team.hinweisleisteAus`.
2. **Kontakt-Leiste** (nur bei `matchMedia('(max-width: 768px)')`): Shadow DOM, unten fest, `z-index: 40`,
Abstandhalter am Ende von `body` in Leistenhöhe, `scroll-padding-bottom`; Knöpfe Anrufen (`tel:`), Route,
WhatsApp – erst der Klick öffnet den fremden Dienst. Aus, wenn `team.kontaktleisteAus` oder die Seite
schon ein festes Element am unteren Rand mit `a[href^="tel:"]` hat.
3. **JSON-LD:** Gibt es einen `application/ld+json`-Block mit einem Knoten vom Typ LocalBusiness oder einem
Untertyp (auch in `@graph`), ersetzt das Skript dort nur `openingHoursSpecification` und
`specialOpeningHoursSpecification` (Text des Blocks per `JSON.stringify` neu, unparsbare Blöcke bleiben
unberührt); sonst legt es einen Block `{ @type: LocalBusiness, name, address?, url, … }` an. Regeln:
Fenster über Mitternacht mit `closes < opens`; „rund um die Uhr“ 00:00–23:59; „nach Vereinbarung“ wird
ausgelassen; Sonderzeiten der nächsten 60 Tage mit `validFrom`/`validThrough`, geschlossen als
`00:00`–`00:00`.
4. **Platzhalter** (vom Team gesetzt; das Skript füllt sie ab `setup`, damit das Team vor dem Schließen des
Tickets prüfen kann):
`<div data-webklar-zeiten data-modus="tabelle|status|badge" data-plan="<kurz>">Ersatz-HTML</div>`.
- *Tabelle:* echtes `<table>` mit Caption, Zeilenköpfe `th scope="row"`, heutiger Tag mit
`aria-current="date"`, darunter „Abweichungen in den nächsten 14 Tagen“.
- *Status:* Punkt + Text („Jetzt geöffnet · schließt um 18 Uhr“), kein `aria-live`.
- *Badge:* Kurzform („Geöffnet“ / „Geschlossen“ / „Nach Vereinbarung“).
Light DOM mit Präfix `wkz-`, `@layer wkz`, Variablen `--wkz-*`; erbt Schrift und Farben der Seite.
Unbekannter `data-plan`: Platzhalter bleibt beim Ersatz-HTML, Konsole-Warnung (kein stilles Umschalten
auf den Hauptplan). Bei `aktiv: false` bleibt das Ersatz-HTML unverändert stehen.
Barrierefreiheit: Tippflächen ≥ 44 px, sichtbarer Fokus, `prefers-reduced-motion` (keine Einblend-Animation).
### 4.4 Freischalten (Ticket schließen ⇒ `active`)
Der vorhandene 5-Minuten-Job setzt `active`, wenn das Ticket geschlossen ist **und** `freischaltbar`
(Register-Haken) erfüllt ist: Hauptplan gespeichert und mindestens ein Tag nicht „zu“. Sonst bleibt `setup`,
WOMS zeigt „wartet auf die Zeiten des Kunden“. Bei `active`: K16, `ersatzHash` setzen.
### 4.5 Ersatz-HTML
`GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=…&plan=<kurz>` liefert eine schlichte
Wochentabelle des Plans als ASCII mit Entities (`&auml;`, `&#8211;`); bei leeren Plänen 409. Das Team setzt sie
beim Einbau in jeden Platzhalter. Ändert sich der Wochenplan des Hauptplans später, bleibt das Ersatz-HTML
zunächst stehen (mit Skript unsichtbar). Wird die Funktion **abgeschaltet** und weicht der aktuelle Wochenplan
von `ersatzHash` ab, legt der Abschalt-Pfad ein WOMS-Ticket „Ersatz-Tabelle aktualisieren“ mit dem aktuellen
Ersatz-HTML an (gehört zum enthaltenen Einbau). K27 sagt dem Kunden, dass ab jetzt wieder feste Zeiten auf der
Seite stehen. Statisches Vorbefüllen durch den Runner bei jedem Deploy bleibt V1.1.
### 4.6 Einbau des Skripts
- **Seite läuft über den Runner:** `previewDeploy` liefert im Auftrag `zeiten: { siteKey } | null` über
`buildZeiten(project)` (nur bei `setup`/`active`; nie im Dev-Pfad – dort wie `analytics` auf `undefined`;
`null` für Projekte auf der Sperrliste; `isPublic` nicht nötig; wirft nie). Der Runner prüft das Feld
(Whitelist in `validate.js`, siteKey-Regex) und setzt in derselben Runde wie die Analyse vor `</head>` jeder
HTML-Datei ein:
`<script data-wk-zeiten src="<ZEITEN_PUBLIC_BASE>zeiten.js" data-site="<siteKey>" defer></script>`
(neue Runner-Variable `ZEITEN_PUBLIC_BASE`, Vorgabe `https://project.webklar.com/embed/`;
`injectIntoHtml` wird auf eine Marke je Tag verallgemeinert, heute fest `data-wk-analytics`).
- **Neubau anstoßen** bei jedem Übergang in einen ausgebenden Zustand (`setup`/`active`) aus `off`, `ordered`
oder `disabled` (Gratisweg, Zahlung, Team-Aktivierung, Abo-Rückkehr, Behalten nach Abschaltung):
`triggerRebuild` (Muster `analyseDeps.rebuild`), **serialisiert** über eine Portal-weite Sperre
`zeiten-neubau` (höchstens ein angestoßener Neubau gleichzeitig). Ergebnis in `einbau`: `angestossen`
(Auftrag angenommen) oder `manuell` (kein Repo, Preview aus, Dev-Modus, Sperrliste, Fehler oder Runner
ausgelastet). Änderungen an Zeiten/Leisten brauchen **keinen** Neubau. Beim Abschalten kein Pflicht-Neubau;
die API antwortet `aktiv: false`, das Tag verschwindet beim nächsten Deploy.
- **Sperrliste** `ZEITEN_KEIN_NEUBAU` (Subdomains; Vorgabe: Emmas Seiten, deren Dateien teilweise nicht in
Git liegen): weder angestoßener Neubau noch Einsetzen bei normalen Deploys (`buildZeiten` ⇒ `null`); das
Team setzt die Zeile von Hand ins Repo.
- **Seite liegt woanders** oder `einbau: manuell`: Das Ticket enthält die Einbau-Zeile; das Team setzt sie ein.
- **Ticket-Inhalt** (Register `ticket.details`): siteKey, Einbau-Zeile, Ergebnis `einbau`, Beispiele der
Platzhalter mit Plan-Kurznamen, Link auf das Ersatz-HTML (nach dem ersten Speichern des Kunden), Prüfliste:
feste Zeiten im Repo suchen und ersetzen (`grep -ril "öffnungszeit"` u. Ä., auch Footer, Kontaktseite,
eigene Status-Skripte), Header-Überlappung der Hinweis-Leiste prüfen (sonst `navSelektor`), eigene
Anruf-Leiste vorhanden?, vorhandenes JSON-LD, Datenschutz-Absatz auf der Website ergänzt (Abschnitt 9).
## 5. Zustände und Abläufe
```
off ──Checkout──▶ ordered ──Zahlung──▶ setup ──Ticket zu + freischaltbar──▶ active
├──Einschalten (Abo)───────────────▶ setup
└──Team „Aktivieren ohne Zahlung“───▶ setup
setup/active ──Abo-Ende + 30 Tage ohne Zahlung (nur abo) / Team „Abschalten“──▶ disabled
disabled (abo_ende) ──30 Tage──▶ Daten gelöscht, state off
```
- Die öffentliche Schnittstelle liefert in `setup` und `active` dasselbe (`teilweiseLive`);
`findActiveBySiteKey` bekommt eine Zustandsliste. `active` heißt „Einbau abgeschlossen“ (Ticket zu, K16).
- **Team** (WOMS): „Aktivieren ohne Zahlung“ läuft für `zeiten` über denselben Pfad wie der Gratisweg
(`setup`, `unlockedBy: 'team'`, Neubau, Ticket; Zustimmung entfällt, Aktion protokolliert) statt über
`adminActivate` (setzt heute direkt `active`). „Abschalten“ ⇒ `disabled`, Grund `team`, ohne Löschfrist.
`activate()` und jede Wiederherstellung räumen `aboEndeAt` und `behaltenSessionId` ab; eine Neubestellung
nach Datenlöschung setzt `ticketId`, `einbau`, `k24At`, `ersatzHash` zurück.
- **Abo-Abgleich** (eigener Tagesjob, nicht an `TERMINE_ENABLED` gekoppelt), nur `unlockedBy === 'abo'`,
Abo-Lage über `aboLage()` (Fehler ⇒ Kunde überspringen, nie abschalten):
1. Abo läuft nicht mehr und `aboEndeAt` leer ⇒ `aboEndeAt = jetzt`, **K26** (Variante nach `art`:
beendet / Wechsel zu Hosting / Wechsel widerrufen; Behalten-Link, Frist = `aboEndeAt` + 30 Tage).
2. `aboEndeAt` + 30 Tage erreicht ⇒ `disabled`, `disabledReason: 'abo_ende'`, **K27**, ggf. Ticket
„Ersatz-Tabelle aktualisieren“ (4.5).
3. Abo läuft wieder, solange die Daten noch da sind (also bis `aboEndeAt` + 60 Tage) ⇒ `aboEndeAt` leer,
Zustand zurück (`active`, wenn das Ticket geschlossen ist, sonst `setup`), Neubau bei Rückkehr aus
`disabled`, K24-Variante „wieder eingeschaltet“.
4. `disabled` mit `abo_ende` + 30 Tage ⇒ `daten`, `team` und Merker gelöscht, Feature-Dokument
`state: 'off'`. Bei Grund `team` keine automatische Löschung.
K16 kann nach K26 noch kommen (Ticket wird während der Frist geschlossen); das ist gewollt.
- **Kunde gelöscht** (`customerAdmin.js`, Löschpfad): `zeiten` wird wie `termine` abgeschaltet;
`oeffnungszeiten`, `zeitenErinnerungen` und der Cache-Eintrag werden gelöscht. `retention.js` bekommt
`zeiten` (Löschfristen wie oben).
## 6. Zeitlogik (Server, `server/services/zeiten/zeitlogik.js`)
Reine Funktionen auf Basis von `termine/zeit.js` (Europe/Berlin über `Intl`) und einem erweiterten
`termine/feiertage.js` (neu: `feiertagName(tag, land)` und `feiertageImZeitraum(von, bis, land)` über
`feiertagejs.getHolidays(jahr, land)` und die Methode `translate('de')` der Feiertags-Objekte). Der Browser
rechnet nichts davon nach.
- **Tagesauflösung** je Plan und Datum, Vorrang (spezifisch vor allgemein):
Sondertag > Betriebsurlaub > Feiertag (Abweichung oder Vorgabe) > Wochenplan. Bei gleicher Art schlägt eine
planbezogene Ausnahme eine für alle Pläne; zwei gleichrangige überlappende Ausnahmen: die später angelegte.
Bewusst anders als `slots.js` (dort schlägt Abwesenheit die Sonderöffnung), weil ein einzelner geöffneter
Tag im Urlaub hier gewollt ist.
- **Fenster** gehören zum Starttag; `bis < von` heißt „bis zum Folgetag“. Ein über Mitternacht laufendes
Fenster folgt der Regel seines **Starttags**, auch wenn der Folgetag Feiertag, Urlaub oder geschlossen ist
(Bar hat Samstag 18–02 offen, Sonntag ist Feiertag ⇒ bis 02:00 offen). Der Status zu einem Zeitpunkt prüft
die Fenster des Tages **und** das über Mitternacht laufende Fenster des Vortags. Aneinander grenzende Fenster
(auch über Tage, z. B. zweimal „rund um die Uhr“) werden vor der Textbildung zusammengefügt.
- **Sommerzeit:** Fenster sind Wanduhrzeiten. `zuUtc` liefert in der März-Lücke `null`; die Zeitlogik
rückt dann selbst auf die erste existierende Minute danach vor (03:00). In der doppelten Oktober-Stunde gilt
das erste Vorkommen (Verhalten von `zuUtc`).
- **Texte** (ohne Anrede):
- offen: „Jetzt geöffnet · schließt um 18 Uhr“ / „… um 18:30 Uhr“; Schluss um 00:00 des Folgetags:
„… schließt um Mitternacht“; Schluss mehr als 24 h entfernt: „Jetzt geöffnet · bis Dienstag 14 Uhr“;
durchgehend ≥ 7 Tage: „Rund um die Uhr geöffnet“. Nie „24 Uhr“.
- zu: „Geschlossen · öffnet heute um 14 Uhr“ / „morgen um 9 Uhr“ / „Montag um 9 Uhr“ (≤ 6 Tage) /
„am 18.08. um 9 Uhr“; keine Öffnung in 60 Tagen: „Derzeit geschlossen“. Grund an geschlossenen Tagen:
„Heute geschlossen (Betriebsurlaub bis 17.08.)“, „(Feiertag: Tag der Deutschen Einheit)“.
- nach Vereinbarung: „Heute nach Vereinbarung“ (`offen: null`).
- kurz: „Geöffnet“ / „Geschlossen“ / „Nach Vereinbarung“.
- **`wechsel`**: alle Statuswechsel der nächsten 8 Tage ab Beginn der aktuellen Minute; zusätzlich ein Eintrag
zu jeder Tagesgrenze, an der sich der Text ändert („morgen“ → „heute“).
## 7. Erinnerung und Mails
**Feiertags-Erinnerung** (Job in `server/index.js`, stündlich, nur 08:00–20:00 Berliner Zeit,
Überlappsperre, nur bei Schalter `pilot`/`on` und nur für freigegebene Kunden): Für jedes Dokument in
`setup`/`active` alle Tage in 1–7 Tagen, die ein Feiertag des Landes oder ein „bitte prüfen“-Tag (3.4) sind.
Nicht erinnert wird, wenn der Tag im Hauptplan ohnehin geschlossen ist, eine Ausnahme ihn abdeckt oder der
Kunde für den Tag schon eine Abweichung gespeichert hat. Für die übrigen Tage: erst Merker anlegen (409 ⇒
überspringen), dann **eine** Mail **K25** mit allen neuen Tagen („Stimmen deine Zeiten an diesen Tagen?“,
Knopf zum Editor, Hinweis „bei Google auch eintragen“). Lieber einmal zu wenig als doppelt; kein BCC ans Team.
**Mails** (im Design der Mailvorlagen vom 27.09., Texte in die Mailrunde des Inhabers):
| ID | Anlass |
|---|---|
| K15/K16/T16 | Wie gehabt, Texte aus dem Register (Kauf, bereit, Team); T16 beim Gratis-/Team-Weg mit Zusatz „im Wartungs-Abo enthalten“ bzw. „vom Team freigeschaltet“; K15 mit Behalten-Wortlaut (3.3). |
| K24 (neu) | Im Abo eingeschaltet; Text nach `einbau` („Leisten erscheinen nach dem nächsten Veröffentlichen“ / „das Team baut sie ein“), Tabelle folgt per Ticket; Variante „wieder eingeschaltet“. |
| K25 (neu) | Feiertags-Erinnerung. |
| K26 (neu) | Wartungs-Abo beendet (Varianten nach `art`): Funktion läuft noch bis <Datum>, „für einmalig 30 € behalten“. |
| K27 (neu) | Abgeschaltet: Leisten sind weg, auf der Seite stehen wieder feste Zeiten; Daten noch 30 Tage, Wiedereinschalten für 30 €. |
Mail-Pin-Test (K01–K27) und der Block `funktionen` in K02/K03 werden angepasst.
## 8. WOMS (eigener Worktree auf WOMS `feature/terminplaner-tp2`)
- `src/lib/funktionenView.js`: Eintrag `zeiten` (Name, Zustandstexte, „Leisten live – Einbau läuft“ bzw.
„wartet auf die Zeiten des Kunden“ für `setup`, Abo-Ende-Hinweis mit Frist, `loeschungAm`), `siteKeyOf` um
`zeiten` erweitert, Test-Pin angepasst.
- Funktionen-Panel: Aktionen „Aktivieren ohne Zahlung“ / „Abschalten“; Team-Einstellungen `navSelektor`,
`kontaktleisteAus`, `hinweisleisteAus` (`PATCH /api/team/funktionen/:customerId/zeiten/team` mit
`teamVersion`); Knopf „Ersatz-HTML kopieren“ je Plan.
- `viewLog.js`: Label für `zeiten_gespeichert`.
- Rechtliches: Karte „Leistungsbeschreibung Öffnungszeiten live“ über `scripts/setup-legal.mjs` (nach den
Terminplaner-Karten aus TP2 F14; setzt TP2 Block F voraus).
## 9. Rechtstexte
Grundsatz Z19: freiwilliges Extra, Abo-Bedingungen bleiben unverändert (kein § 15-Verfahren, kein P10-Risiko).
1. **Leistungsbeschreibung „Öffnungszeiten live“** (eigenständig, Gerüst der Analyse-Leistungsbeschreibung):
Leistung; automatischer Einbau nur bei WEBklar-Hosting, sonst durch das Team; Preis 30 € ohne
Umsatzsteuer nach § 19 UStG, mit laufendem Wartungs-Abo ohne Aufpreis und ohne Anrechnung aufs
Stundenkontingent („gehört nicht zum Leistungsumfang der Abo-Bedingungen; Leistungsbeschreibung B Nr. 3
gilt dafür nicht; der Abo-Preis ändert sich nicht“); ein Einbau je Website inkl. Aktualisierung der
Ersatz-Tabelle beim Abschalten, Sonderwünsche nach Angebot/Stunden; nicht enthalten: Übertragung zu
Google, Gemeinde-Feiertage; Pflichten des Kunden (Zeiten, Feiertage und Hinweistexte prüfen,
Preise/Rabatte rechtmäßig angeben, Datenschutz-Absatz auf der Website); **Haftung** (Angaben vom Kunden,
Live-Anzeige als Hilfsmittel, Softwarefehler inkl. Feiertagsdaten nur im Rahmen der üblichen Grenzen –
spiegelt Abo § 13 Abs. 1 und 2, damit Kunden ohne Abo abgedeckt sind); Ende der Leistung (Abo-Ende nach Z11)
und Einstellung/Änderung nach Vorbild Analyse Nr. 8.
2. **Zustimmung:** bezahlt mit Beginn-Verlangen (§ 356 Abs. 5 Nr. 2 BGB, Belehrung über TP2-Baustein F3);
gratis nur Haken für die Leistungsbeschreibung, Knopf „Einschalten“; Nachweis in beiden Fällen.
3. **Datenschutz Portal:** neuer § 6d (gespeicherte Angaben, Erinnerungsmail, Art. 6 Abs. 1 lit. b,
Löschfristen); § 6 um „Öffnungszeiten“ im Kundenauftrag erweitert.
4. **Absatz für die Kundenseite** (`docs/funktionen/website-datenschutz-absatz.md`, Varianten A/B):
Abruf von project.webklar.com (IP 14 Tage im Server-Log, keine Cookies, nichts im Browser gespeichert);
WhatsApp (Meta) und Google Maps erst nach Klick.
5. **AVV:** mit Abo reicht Anlage 1 (Server-Protokolle); ohne Abo/bei Fremdhosting gleiche Lücke wie bei
der News-Pinnwand ⇒ Frage ins Kanzlei-Paket (⚖ „AVV Funktionen“ per Haken?).
6. **Mitkorrektur News** (TP2 F2): „inkl. USt.“ → § 19 UStG, § 356 Abs. 4 → Abs. 5.
Reihenfolge der Rechtsrunde (gemeinsam mit TP2): gemeinsame Bausteine (TP2 Block F) → Leistungsbeschreibung →
Datenschutz § 6d/§ 6 und Website-Absatz → Oberflächen- und Mailtexte (Mailrunde) → ⚖-Fragen ans Kanzlei-Paket
→ Inhaber lädt Fassungen in WOMS „Rechtliches“ hoch → Schalter `on`.
## 10. Relaunch-Station (Repo webklar.com, eigener Branch ab `main`)
`main` des Relaunch-Repos ist live (Push = Deploy) – Arbeit auf eigenem Branch im eigenen Worktree, der Inhaber
pusht.
- `Kundenportal.tsx` `FUNKTIONEN`: 4. Eintrag nach der News-Pinnwand, `demo: 'zeiten'`, `bald: true`
(bis Portal-Livegang), kein Preis. Indizes 0–6 bleiben, „Als App“ rückt auf 8.
- Neuer Ordner `src/components/home/demos/zeiten/`: `ablauf.ts`, `ZeitenSeite.tsx`, `ZeitenHandy.tsx`,
`ZeitenRegler.tsx`, `zeiten.css`. Gemeinsame Bäckerei-Teile (`Logo`, `Pfeil`, `Stempel`) nach
`baeckerei/teile.tsx`; eigenes Wurzelelement ohne `.bk-inhalt` (sonst greift `--kp-v`).
- **MacBook:** Bäckerei Sonnenschein mit großer Status-Pille (≥ 30 pt) und Hinweis-Leiste
„Betriebsurlaub 12.–26. Oktober“ (≥ 24 pt, nicht unter dem iPhone). Demo-Datum Mo 5. Okt. 2026 (liegt im
7-Tage-Vorlauf des Urlaubs).
- **Handy:** Portal-Editor-Shot `phone-12-zeiten` (Fixture-Kopie mit Portal aus `dev-zeiten`, Profil
`baeckerei` mit `features.zeiten`, fester Uhrzeit, freier Port aus 5326/5327/5329), Finger tippt
„Hinweis-Leiste anlegen“, danach gleitet die Leiste auf der Seite ein.
- **Uhr-Regler:** echtes `<input type="range">` mit Label, Tippfläche ≥ 44 px; Automatiklauf
Mo 05:50 → 18:10 („Jetzt geöffnet“ → „Geschlossen · öffnet morgen um 6 Uhr“), danach Hinweis-Leiste; stoppt
bei Bedienung; bei reduzierter Bewegung fester Endzustand.
- Animation nur über `transform`/`opacity`; keine `https://` in Demo-Dateien; kein „24 h“; kein
Google-Ranking-Versprechen; WhatsApp nur als Text/Piktogramm ohne Logo.
- Budget: Haupt-JS ≤ +0,7 KB gz (Grenze 130 KB), neue Chunks ≤ 8 KB gz, Handy-Bild ≤ 60 KB.
- Tests: neuer `demo-zeiten.test.tsx` (Endzustand, nichts fokussierbar außer Regler, Status zu jeder Uhrzeit);
`relaunch-kundenportal.test.tsx`: `TITEL`, Karten-Obergrenze, `ziele`-Länge, Bilderzahl („2 + n“),
Handy-Bildzählung und `meldeMarke`-Indizes („Als App“ 7 → 8) aus der Stationsliste ableiten bzw. anpassen;
Spec-Text „höchstens 7 Karten“ anpassen.
- Das Handy-Bild entsteht erst, wenn der Portal-Editor steht.
## 11. Fehlerfälle
| Fall | Verhalten |
|---|---|
| API nicht erreichbar / Daten > 24 h alt | Leisten aus, Platzhalter zeigen Ersatz-HTML. |
| Browser-Uhr falsch (> 5 min laut `Date`) | Serverzeit wird verwendet. |
| Gleichzeitiges Speichern (Kunde + Team) | 409, Editor lädt neu und zeigt die neue Fassung. |
| Neubau nicht möglich / Sperrliste / Fremdhosting | `einbau: manuell`, Ticket enthält Einbau-Zeile; Kachel und K24 sagen „das Team baut ein“. |
| Ticket-Anlage oder K24 scheitert beim Gratisweg | Zustand bleibt `setup`; 5-Minuten-Job bzw. erneuter Klick holt nach. |
| Ticket geschlossen, Kunde hat keine Zeiten eingetragen | bleibt `setup`, WOMS „wartet auf die Zeiten des Kunden“. |
| Abo endet während `setup` | Abgleich wie 5 (Frist läuft unabhängig vom Einbau). |
| Behalten-Zahlung nach Datenlöschung | T19 „Erstattung fällig“. |
| Abo-Lesefehler | Einschalten 503, Kachel „gerade nicht prüfbar“, Abgleich überspringt den Kunden. |
| Hauptplan löschen | nicht möglich; erst anderen Plan zum Hauptplan machen. |
| Platzhalter mit unbekanntem `data-plan` | Ersatz-HTML bleibt, Konsole-Warnung. |
| Skript zweimal eingebunden (Runner + manuell) | zweiter Lauf beendet sich sofort. |
| Beliebige siteKeys (Abtasten) | Format-Prüfung, LRU-Cache mit Obergrenze, Rate-Limit. |
| Kunde gelöscht | API `aktiv: false`, Daten gelöscht (5). |
| Projekt archiviert | Funktion läuft weiter, solange `setup`/`active` (wie News); Neubestellung gesperrt. |
## 12. Tests
- **Zeitlogik** (`test/zeitenZeitlogik.test.js`): 29.03.2026 (Fenster 02:00–04:00, Öffnung 02:30 ⇒ 03:00),
25.10.2026 (Fenster 02:00–06:00, „schließt um 02:30“); Fenster 18:00–02:00 (Status um 01:00 am Folgetag;
Feiertag/Urlaub am Folgetag ⇒ trotzdem offen bis 02:00; Sondertag am Folgetag); Mo+Di rund um die Uhr (kein
„24 Uhr“, Wechsel am Ende von Dienstag); Feiertag an geschlossenem Tag; Feiertag mit Abweichung; „bitte
prüfen“-Tage (Heiligabend, Mariä Himmelfahrt BY); Urlaub über Feiertag; Sondertag im Urlaub (Sondertag
gewinnt); überlappende Urlaube; Urlaub über den Jahreswechsel; „nach Vereinbarung“ (Status `null`, nicht im
JSON-LD); mehrere Pläne mit planbezogenen Ausnahmen; Hauptplan wechseln; Texte
„heute/morgen/Wochentag/Datum/Derzeit geschlossen/Mitternacht“; `wechsel` über 8 Tage; Hinweis-`ab/bis`
über Sommerzeitgrenzen.
- **Prüfregeln:** Fenster-Regeln, Längen, Link-/Telefon-/Selektor-Regeln, Aufräumen vergangener Einträge.
- **Abläufe:** Gratisweg (alle Stufen, `kein_abo`/`gekuendigt`/`nicht_aktiv`, Lesefehler 503, Doppelklick
idempotent, Selbstheilung Ticket/K24 im Job), Bezahlweg mit 409 `inklusive`, Team-Aktivierung über den
Gratis-Pfad, Ausgabe in `setup` wie in `active`, leere Pläne, `freischaltbar`-Gate, Neubau bei jedem
Übergang in einen ausgebenden Zustand und Serialisierung, Abo-Abgleich (K26-Varianten, 30 Tage, K27, Ticket
Ersatz-Tabelle nur bei Abweichung, Rückkehr bis Tag 60, Löschung nur bei `abo_ende`), Behalten (Zustand
bleibt während der Zahlung, kein neues Ticket, Session-Abgleich, T19 nach Löschung), Kunde gelöscht,
Erinnerung genau einmal (Neustart zwischen Merker und Mail, Bundeslandwechsel, mehrere Tage in einer Mail,
keine Mail bei geschlossenem Tag/Ausnahme/Abweichung).
- **Schnittstelle:** `aktiv:false`-Fälle, ETag/304 trotz `stand`, LRU-Grenze, Rate-Limit, keine internen IDs,
`firma` ohne Kundendokument.
- **Runner:** Einsetzen beider Tags in einem Durchlauf, idempotent, Whitelist, Dev-Pfad ohne `zeiten`,
Sperrliste ohne Einsetzen.
- **Skript** (Playwright, Testseiten): fester Header (Rücken), klebender Header, eigene Anruf-Leiste
(Kontakt-Leiste aus), vorhandenes JSON-LD (ergänzt, kein Duplikat, unparsbarer Block unberührt), kein JSON-LD
(Block neu), `aktiv:false` (Ersatz-HTML bleibt), unbekannter `data-plan`, keine Cookies/kein Storage,
doppeltes Laden, Handy- und Desktop-Breite, reduzierte Bewegung, ungültiger `navSelektor`.
- **Editor** (UI-Test mit Fixture): Pläne mit Kurznamen, Fenster über Mitternacht, Validierungsfehler je Feld,
Urlaub ⇒ Hinweis-Vorschlag, „Für Google kopieren“, Vorschau-Regler, Kundenansicht ohne WhatsApp-Haken.
- **WOMS:** Register-Pin, Zustandstexte, Team-Einstellungen, viewLog-Label.
- Bestehende Suiten bleiben grün (Portal `npm test` inkl. Mail-Pin und `customerViewLog`-Pin, WOMS `npm test`
+ `vite build`, Relaunch-Tests).
## 13. Reihenfolge und Livegang
1. Spec (diese Datei) → Durchsicht durch den Inhaber.
2. Umsetzungsplan (`docs/superpowers/plans/2026-10-04-oeffnungszeiten-live.md`) → Freigabe.
3. Umsetzung in `dev-zeiten` (Portal) und einem WOMS-Worktree auf `feature/terminplaner-tp2` mit
Sonnet-Agenten; regelmäßig auf die TP2-Branches rebasen. Die TP2-Worktrees `dev-termine` werden nicht
angefasst.
4. Relaunch-Station auf eigenem Branch im Relaunch-Repo.
5. Rechtsrunde gemeinsam mit TP2 (Abschnitt 9).
6. Livegang nach TP2: Rebase auf `main` (mit TP2), Schema-Skripte (`feature-setup.mjs`, neue Collections),
Stripe-Katalog prüfen (`scripts/stripe-products.mjs --pruefen`, `PRODUCT_METADATA`), Runner-Variable
`ZEITEN_PUBLIC_BASE` und Sperrliste setzen, Inhaber pusht, `ZEITEN_ENABLED=pilot` (Testkonto), Probelauf auf
einer Testseite, dann `on`; Relaunch-Station `bald: false`.
## 14. Nicht im Umfang (V1)
Automatischer Abgleich mit dem Google-Unternehmensprofil (API/Cloud); statisches Vorbefüllen der Platzhalter
durch den Runner bei jedem Deploy (V1.1); Gemeinde-Feiertage; Schließen-Knopf der Hinweis-Leiste; Übernahme
der Zeiten in den Terminplaner; mehrere Hinweise gleichzeitig; Mehrsprachigkeit; Ankündigungsmail/Portal-Banner
(Z8).
@@ -0,0 +1,419 @@
# Aufträge und Rechnungen: Auftragsbestätigung und projektbezogene Rechnungen
Stand: 2026-10-06 · Status: freigegeben (Abschnitte im Chat einzeln freigegeben; Nachträge aus der Planrunde eingearbeitet, siehe „Nachträge“ am Ende)
Grundlage: Wunsch des Nutzers vom 06.10.2026 („Auftragsbestätigung und Rechnungen bei uns, wenn wir auf Kunden gehen;
Projekt auswählen, Zusammenfassung sehen, Festbeträge möglich“), Recherche im selben Gespräch (Quellen am Ende),
Bestand: Finanzen-Spezifikation `2026-10-01-finanzen-design.md` (Stripe-Rechnungen aus WOMS, live seit 01.10.2026).
## Ziel
Im Ticketsystem (WOMS) legt ein Admin beim Kunden **Auftragsbestätigungen (AB)** und **Rechnungen zu einem Projekt** an.
Nach der Projektwahl zeigt eine **Zusammenfassung**, was vereinbart ist, was schon berechnet und bezahlt ist, was noch
offen ist und wie viel Zeit im Projekt steckt. Die Rechnung entsteht als Anzahlung, Schlussrechnung, nach Aufwand oder
als frei gewählter **Festbetrag** – auch wenn die erfasste Zeit mehr wert wäre (Beispiel: Frau Wolf, 500 € laut
mündlicher Absprache).
Erfolg:
- Eine Projektrechnung (Festbetrag oder Aufwand) ist in unter zwei Minuten geschrieben und versendet.
- Eine AB geht mit PDF an den Kunden, der sie per Klick bestätigt; bei Privatkunden ist sie widerrufsrechtlich korrekt.
- Kein Arbeitsblatt wird doppelt berechnet, keine Anzahlung wird in der Schlussrechnung vergessen.
- Jede Rechnung erfüllt § 34a UStDV, jede gesendete AB liegt unveränderlich mit Prüfsumme vor.
## Feststehende Rahmenbedingungen (geprüft)
- **WEBklar GbR, Kleinunternehmer nach § 19 UStG.** Rechnungen müssen nach **§ 34a UStDV** (seit 01.01.2025) enthalten:
Name und Anschrift beider Seiten, **Steuernummer** (oder USt-IdNr./Kleinunternehmer-IdNr.), Ausstellungsdatum, Art
und Umfang der Leistung, Entgelt in einer Summe mit Hinweis auf die Kleinunternehmerbefreiung. Leistungszeitpunkt und
Rechnungsnummer sind für Kleinunternehmer keine Pflicht; § 33 UStDV (Kleinbetrag bis 250 €) bleibt unberührt.
**Befund:** Der heutige Rechnungsfuß (`config.stripe.invoiceFooter`) enthält keine Steuernummer.
- **Aufbewahrung:** Rechnungsdoppel 8 Jahre (§ 14b UStG seit 2025), Auftragsbestätigungen als Beleg 8 bzw. als
Geschäftsbrief 6 Jahre. E-Rechnungen muss ein Kleinunternehmer nicht ausstellen.
- **Verbraucher:** Bei Fernabsatzverträgen (Telefon, E-Mail, Chat) muss der Unternehmer den Vertrag auf einem
dauerhaften Datenträger bestätigen, spätestens bevor er mit der Leistung beginnt (§ 312f Abs. 2 BGB), samt der Angaben
nach Art. 246a EGBGB und Widerrufsbelehrung. Ohne Belehrung läuft die Widerrufsfrist bis zu 12 Monate und 14 Tage; ohne
ausdrückliches Verlangen des Verbrauchers vor Fristende schuldet er bei Widerruf keinen Wertersatz.
- **Kostenschätzung:** Zeichnet sich eine wesentliche Überschreitung eines Kostenanschlags ab, muss der Unternehmer den
Besteller unverzüglich informieren (§ 649 Abs. 2 BGB).
- **AGB (webklar.com/agb):** Vergütung „ergibt sich aus dem Angebot“, Aufwand in 15-Minuten-Schritten, Unterstützung
zur Hälfte (Nr. 9), Mitwirkung (Nr. 7), Abnahme (Nr. 8), „Projektleistungen sind nach Vereinbarung fällig“.
Stundensatz in den Abo-Bedingungen: 100 € pro Stunde (Endpreis, § 19 UStG).
- **Stripe** (SDK `stripe@19`, API 2025-10-29.clover): Rechnungen mit bis zu **4 Zusatzfeldern** im Kopf (Name ≤ 40,
Wert ≤ 140 Zeichen), Seitengröße per `rendering.pdf.page_size` (Standard außerhalb Japans: US-Letter), Rechnungsposten
mit **negativem `amount`** (mindert den Betrag), Steuer-IDs des Kontos im Dashboard (Einstellungen → Billing →
Rechnungen → „Steuerinformationen für Rechnungen“). Stripe-„Quotes“ taugen nicht als AB (Aufpreis, PDF-Titel
„Angebot“, keine Verbraucher-Variante).
- **Bestand im Portal-Server:** `createProjectInvoice` in `server/services/finance/stripeFinance.js` (Entwurf/Senden,
Adress-Sperre über 250 €, Doppel-Schutz), Finanzen-API `/api/admin/finanzen/*` mit `requireAppwriteStaff` (Admin,
zweiter Faktor), Mailer `sendCustomerMail` mit Anhängen, Fristenlogik `deadlines.js` (`withdrawalDeadline`),
Frühbeginn-Erklärungen nach Muster Terminplaner/Webshop, öffentliche Widerrufsseite `widerrufen.html`
(`/api/abo/public/withdraw`), Kundenansicht-Sperre (fail-closed), Arbeitszeit-Regeln `worksheetMinutes.js`.
- **Bestand im WOMS:** Reiter „Rechnungen“ am Kunden (nur Admins), Dialog „Rechnung schreiben“ (freie Positionen),
Finanzen-Seite. Projekte (`websiteProjects`) hängen an genau einem Ticket (`ticketId`), Arbeitsblätter
(`worksheets.totalTime`, `supporters`, `countsToPlan`) an Tickets.
- **WOMS-Redesign** (weißes Design, Pakete P4 Kunden und P7 Finanzen) baut parallel genau die betroffenen Dateien um.
## Entscheidungen des Nutzers (06.10.2026)
1. **Abrechnungsarten:** Festpreis, nach Aufwand (Arbeitsblätter × Stundensatz) und Anzahlung mit Schlussrechnung; ein
freier Festbetrag geht immer.
2. **Auch Privatkunden:** Kunden bekommen die Kundenart *Unternehmen / Privatperson*. Bei Privatpersonen enthält die AB
Widerrufsbelehrung und Muster-Widerrufsformular; Arbeitsbeginn vor Fristende nur auf ausdrückliches Verlangen.
3. **AB-Versand:** Mail mit PDF und Knopf „Auftrag bestätigen“ (Link ohne Anmeldung, auch im Portal sichtbar); der
Klick wird protokolliert.
4. **Ansatz A:** Rechnungen bleiben Stripe-Rechnungen (erweitert); die AB ist ein eigenes Dokument des Portal-Servers.
5. **Abschnitte 1–4 dieses Entwurfs** (Oberfläche, AB, Rechnungen, Technik/Livegang) wie unten beschrieben.
6. **Reihenfolge:** Portal-Server und Kundenseiten zuerst; WOMS-Oberfläche im neuen weißen Design, sobald die
Redesign-Stränge Kunden (P4) und Finanzen (P7) zusammengeführt sind. **Zwei Pläne** (siehe unten).
## Begriffe
| Begriff | Bedeutung |
|---|---|
| **Auftrag / AB** | Eine Auftragsbestätigung an einen Kunden, meist zu einem Projekt. Art *Festpreis* (Positionen) oder *Aufwand* (Stundensatz, Schätzung). |
| **Projektkonto** | Zusammenfassung je Projekt: vereinbart, berechnet, bezahlt, noch offen, Zeit erfasst. |
| **Zeitquelle** | Die Tickets, deren Arbeitsblätter zu einem Projekt/Auftrag zählen (Standard: das mit dem Projekt verknüpfte Ticket). |
| **Rechnungsart** | `anzahlung`, `schluss`, `aufwand`, `festbetrag` (mit Projekt) oder `frei` (ohne Projekt, wie heute). |
| **Abo-Zeit** | Arbeitsblätter mit `countsToPlan = true`; sie laufen über das Abo-Stundenkonto und werden nie projektbezogen berechnet. |
## 1. Oberfläche in WOMS
**Ort:** Kunde → Reiter **„Aufträge und Rechnungen“** (bisher „Rechnungen“, `id` bleibt `invoices`), nur für Admins.
Gebaut im neuen weißen Design (Komponenten des Redesigns, z. B. `<Dialog>`).
Von oben nach unten:
1. **Projekte (Projektkonto):** je Projekt des Kunden eine Zeile mit Name, Art und Ticketnummer; Spalten *Vereinbart*
(Summe der gültigen Festpreis-ABs bzw. „nach Aufwand, 100 €/Std.“ bzw. „keine AB“), *Berechnet* (darunter „bezahlt
x €“), *Noch offen*, *Zeit erfasst* (Stunden, darunter „Wert x €“). Knöpfe **„Auftragsbestätigung“** und
**„Rechnung“**. Eine letzte Zeile **„Ohne Projekt“** sammelt Rechnungen ohne Projektbezug.
2. **Aufträge:** Liste der ABs (Nummer, Projekt/Titel, Datum, Summe bzw. „nach Aufwand“, Status, Aktionen: PDF,
Erinnerung senden, Neue Fassung, Stornieren, Entwurf bearbeiten/löschen).
3. **Rechnungen:** die bestehende Rechnungsliste mit den zusätzlichen Spalten *Projekt* und *Art*.
Der neue Dialog „Rechnung schreiben“ ersetzt den bisherigen überall. Seine Projektliste endet mit **„Ohne Projekt“**
(Art `frei`: freie Positionen wie bisher), so bleibt eine Rechnung ohne Projekt möglich.
**Kundenart:** In der Kunden-Übersicht ein Feld „Kundenart: Unternehmen / Privatperson“ (leer = unbekannt). Ist es
leer, fragt der AB-Dialog im ersten Schritt danach und speichert die Antwort am Kunden.
**Finanzen-Seite:** „Rechnung schreiben“ öffnet denselben neuen Dialog, mit Kundenwahl davor. Die Rechnungstabelle
bekommt die Spalten Projekt und Art.
**Arbeitsblätter (Ticketseite):** Ein abgerechnetes Arbeitsblatt zeigt das Abzeichen „abgerechnet (Rechnungsnummer)“.
Ändert jemand dessen Zeit, warnt der Dialog: „Dieses Arbeitsblatt ist abgerechnet (WEBKLAR-0009). Eine Änderung ändert
die Rechnung nicht.“ (Keine harte Sperre: WOMS schreibt Arbeitsblätter direkt in Appwrite.) Das Abzeichen sehen alle
Mitarbeiter (ohne Beträge).
### Dialog „Rechnung schreiben“ (neu)
1. **Projekt** (vorausgewählt, wenn vom Projektkonto geöffnet) oder „Ohne Projekt“.
2. **Zusammenfassung** des Projektkontos (siehe Abschnitt 4) und **Art**: Anzahlung, Schlussrechnung, Nach Aufwand,
Festbetrag. Anzahlung und Schlussrechnung sind nur mit gültiger Festpreis-AB wählbar (Hinweis statt Sperre ohne
Erklärung: „Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.“).
3. **Positionen** (je Art vorausgefüllt, änderbar), **Leistungszeitraum** (optional, bei Aufwand vorausgefüllt),
Zahlungsziel (Standard 14 Tage), Notiz; Summe mit § 19-Hinweis.
4. **„Als Entwurf speichern“** oder **„Senden“** (mit Rückfrage wie heute; Adress-Sperre über 250 € wie heute).
### Dialog „Auftragsbestätigung“ (neu)
1. **Projekt** (oder freier Titel, wenn es noch kein Projekt gibt; später zuordenbar über „Projekt zuordnen“ in der AB-Liste, ohne das PDF zu ändern) und, falls leer, **Kundenart**.
2. **Auftrag:** Datum der Absprache und Weg (mündlich, telefonisch, per E-Mail, vor Ort, im Chat). Art **Festpreis**
(Positionen: Beschreibung, Menge, Einzelpreis; Grenzen wie bei Rechnungen) oder **Nach Aufwand** (Stundensatz,
Standard 100 €; Schätzung in Stunden, optional, in 15-Minuten-Schritten).
3. **Zahlung und Termine:** Zahlungsplan „Alles nach Abnahme“ · „Anzahlung x % bei Auftrag, Rest nach Abnahme“
(Standard 50 %) · „Monatlich nach Aufwand“ (nur Aufwand) · „Frei“ (Text, höchstens 500 Zeichen); Zahlungsziel
(Standard 14 Tage); Start und voraussichtliche Fertigstellung (optional); Zeitquelle (Tickets des Kunden, Standard:
Ticket des Projekts).
4. **Vorschau** (PDF, nicht gespeichert) → „Als Entwurf speichern“ oder „Senden“ (mit Rückfrage).
## 2. Auftragsbestätigung (AB)
### Nummer und Status
- Nummer **`AB-JJJJ-NNNN`**, fortlaufend je Kalenderjahr (Berliner Datum des Sendens), vergeben erst beim Senden unter
einer Sperre im Portal-Server (eine Instanz). Entwürfe haben keine Nummer. Scheitert das Senden vor dem Speichern des
PDFs, wird keine Nummer verbraucht.
- Status: `entwurf` → `versendet` → `bestaetigt` → `abgerechnet` (nach finalisierter Schlussrechnung); außerdem
`storniert` (von Hand, ohne Mail), `ersetzt` (durch eine neue Fassung) und `widerrufen` (Privatperson).
- **Unveränderlich nach dem Senden.** Änderung = „Neue Fassung“: Kopie als neuer Entwurf mit Vermerk
„Ersetzt AB-… vom …“; beim Senden der neuen AB wird die alte `ersetzt`. Rechnungen der alten AB wandern intern mit
(`projektRechnungen.auftragId` auf die neue AB); Stripe-Metadaten und der auf finalisierten Rechnungen aufgedruckte
Bezug bleiben unverändert.
### Inhalt des PDFs (A4)
- Kopf: WEBklar-Logo, WEBklar GbR, Anschrift, Kontakt, **Steuernummer** (Einstellung `financeSettings.steuernummer`).
- Empfänger: Firma bzw. Name, Anschrift (Straße, PLZ, Ort Pflicht für das Senden – auch unter 250 €).
- Titel „Auftragsbestätigung“, Nummer, Datum, Projekt bzw. Titel, Kundennummer (falls vorhanden).
- Einleitung: „Vielen Dank für Ihren Auftrag vom TT.MM.JJJJ (telefonisch). Wir bestätigen ihn wie folgt:“
- Leistungsumfang: Positionen mit Menge, Einzelpreis, Summe und Gesamtpreis **oder** Aufwandsregel („100 € pro
Stunde in Schritten von 15 Minuten; unterstützt eine weitere Person, berechnen wir für sie die Hälfte der
Arbeitszeit (AGB Nr. 9). Geschätzter Umfang: etwa X Stunden (unverbindlich). Zeichnet sich eine deutliche
Überschreitung ab, melden wir uns vorher.“); Hinweis „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“
- Zahlungsplan mit Beträgen (z. B. „Anzahlung 50 % (400,00 €) nach Auftragsbestätigung, Rest (400,00 €) nach Abnahme;
zahlbar jeweils innerhalb von 14 Tagen nach Rechnung“), Termine.
- Mitwirkung und Abnahme laut AGB Nr. 7 und 8; „Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom
…, die dieser Bestätigung beiliegen.“
- **Privatperson:** zusätzliche Seiten „Widerrufsbelehrung“ (Projektauftrag) und „Muster-Widerrufsformular“.
- Fuß: Bankverbindung (`financeSettings.bankDetailsText`), Kontakt, Seitenzahl.
- Texte und Fassung kommen aus einer Konstante im Server (`texteFassung`, Muster wie `config.termine`), die AB merkt sich
die Fassung.
### Erzeugung und Ablage
- PDF mit **pdfkit** (reines JavaScript; kein Chrome zur Laufzeit wegen knappem Arbeitsspeicher), statische
Figtree-Schnitte (OFL) unter `server/assets/fonts/`, Logo `public/webklar-logo.png`.
- Beim Senden einmal erzeugt und unter `upload-data/auftraege/<kundenId>/<abId>.pdf` abgelegt, SHA-256 im Dokument;
danach wird nur noch diese Datei ausgeliefert. Die Datei liegt in der Hetzner-Serversicherung; jede Mail geht zudem
per BCC an das Team-Postfach.
- Die **AGB** werden als versionierte PDF-Datei ins Portal gedruckt (`public/rechtliches/agb-<fassung>.pdf` mit
`.sha256`, über `scripts/build-legal-pdfs.sh`, Quelle: Website-AGB). Jede AB merkt sich `agbFassung`.
### Versand
- Mail **K31 „Auftragsbestätigung“** (Kunde, Design der Mailvorlagen, **ohne** BCC ans Team, weil sie den
Bestätigungslink trägt): kurzer Text, Anhänge AB-PDF und AGB-PDF, Knopf **„Auftrag bestätigen“** →
`https://project.webklar.com/auftrag.html#t=<token>` (Fragment: landet nicht in Server-Protokollen); bei
Privatpersonen Hinweis auf Widerrufsbelehrung und Frühbeginn. Das Team bekommt eine Kopie **ohne** Link.
- Token: 32 Zufallsbytes (base64url), gespeichert nur als SHA-256, gültig 60 Tage ab dem letzten Versand.
**„Erinnerung senden“** (WOMS, von Hand) schickt K31 erneut mit Erinnerungs-Hinweis und neuem Token (der alte gilt
nicht mehr).
- Mail-Status am AB (`mailStatus`: gesendet / nicht zugestellt + Zeitpunkt); bei Fehler „Erneut senden“.
### Bestätigung
- **Öffentliche Seite** `auftrag.html#t=…` (ohne Anmeldung, `noindex`, Ratenbegrenzung je IP im Arbeitsspeicher wie
bei der Widerrufsseite): Zusammenfassung (Nummer, Datum, Projekt, Positionen bzw. Aufwandsregel, Summe,
Zahlungsplan, Termine), Links auf AB-PDF und AGB-PDF, Häkchen „Ich bestätige den Auftrag zu diesen Bedingungen.“,
Knopf „Auftrag bestätigen“. Nach der Bestätigung zeigt die Seite „Bestätigt am …“. Ungültiger oder abgelaufener Link:
Erklärung und Kontaktweg.
- **Privatperson:** zweites, freiwilliges Häkchen mit der Frühbeginn-Erklärung (Wortlaut aus der Rechtsrunde, Muster
der Funktionen). Ohne Häkchen zeigt WOMS „Arbeitsbeginn frühestens am TT.MM.“ = Tag nach
`withdrawalDeadline(gesendetAm)`.
- **Im Portal** (angemeldet): Reiter Rechnungen → Abschnitt „Aufträge“ mit Status, PDF und demselben Bestätigen-Ablauf.
- **Kundenansicht:** Bestätigen ist gesperrt (Erklärung im Namen des Kunden; der Endpunkt steht nicht auf der
Freigabeliste der Kundenansicht-Sperre).
- **Protokoll am AB:** `bestaetigtAm`, `bestaetigtUeber` (`link` | `portal`), `bestaetigtPdfSha256` (muss
`pdfSha256` entsprechen), `texteFassung`, `fruehbeginn` (ja/nein) und dessen Textfassung. **Keine IP-Adresse.**
Eine zweite Bestätigung ändert nichts (die erste zählt).
- Danach: Mail **K32 „Auftrag bestätigt“** an den Kunden (Eingangsbestätigung mit Datum, Uhrzeit, Frühbeginn ja/nein;
eine Frühbeginn-Erklärung wird wörtlich wiedergegeben) und **T22 „Auftrag bestätigt“** an das Team.
### Widerruf (Privatperson)
- `widerrufen.html` bekommt die Vertragsart **„Projektauftrag (Auftragsbestätigung)“** mit Feld AB-Nummer (optional,
„falls zur Hand“; der Widerruf ist formfrei). Die Erklärung läuft über den bestehenden Weg (Eingangsbestätigung an den Kunden, Team-Mail); passt die AB zu Nummer und
E-Mail-Adresse, wird sie `widerrufen` und WOMS zeigt es. Erstattung und Wertersatz bleiben Handarbeit.
## 3. Rechnungen
Alle Projektrechnungen bleiben **Stripe-Rechnungen** (`collection_method: send_invoice`, Mail mit PDF und Zahlungsseite,
Karte/SEPA, Bankabgleich und Finanzen-Übersicht unverändert).
### Arten
| Art | Voraussetzung | Positionen |
|---|---|---|
| `anzahlung` | AB *Festpreis*, Status `versendet` oder `bestaetigt` | Eine Zeile „Anzahlung 50 % gemäß Auftragsbestätigung AB-2026-0002 vom 06.10.2026 (Projekt)“; Betrag aus dem Zahlungsplan, änderbar in % oder €, höchstens AB-Summe minus bereits Berechnetes. Bezeichnung änderbar (auch „Teilrechnung“). |
| `schluss` | AB *Festpreis*, Status `versendet` oder `bestaetigt`; kein offener Entwurf zur AB | Alle AB-Positionen, zusätzliche Positionen (Nachträge) möglich, dann je früherer finalisierter, nicht stornierter Rechnung zur AB eine **Minuszeile** („abzüglich Anzahlung, Rechnung WEBKLAR-0007 vom 08.10.2026“, negativer `amount`). Summe muss > 0 sein, sonst „Nichts mehr offen“. Nach dem Finalisieren: AB `abgerechnet` (Storno setzt zurück). |
| `aufwand` | Projekt; Zeitquelle mit noch nicht abgerechneten Arbeitsblättern | Alle offenen Arbeitsblätter der Zeitquelle (ohne Abo-Zeit, ohne reine Kommentare, ohne 0 Minuten) sind vorausgewählt und abwählbar; **eine Zeile je Tag**: „06.10.2026: Startseite, Kontaktformular – 2,25 Std. à 100,00 €“ (Menge 1, Betrag = Stunden × Satz). Minuten je Arbeitsblatt wie `worksheetMinutes` (inkl. Unterstützung), je Eintrag auf 15 Minuten aufgerundet. Zeilentext = Kurzfassungen der Arbeitsblätter des Tages, gefiltert wie in der Kundenansicht (`customerProjectView`: ohne Git-Anhänge, SHAs, Kontonamen), auf 200 Zeichen gekürzt, im Dialog änderbar. Satz aus der jüngsten gültigen Aufwands-AB des Projekts, sonst 100 €. |
| `festbetrag` | Projekt | Freie Positionen wie heute (Frau Wolf: „Erstellung der Website anelia-wolf.com, Pauschalpreis laut Absprache“, 500 €). |
| `frei` | „Ohne Projekt“ gewählt | Freie Positionen wie bisher, ohne Zusatzfelder „Projekt“/„Auftrag“ (aber A4). |
Grenzen wie heute (1–20 Positionen inkl. Minuszeilen; Beschreibung ≤ 200 Zeichen); eine Schlussrechnung darf bis
40 Zeilen haben (AB-Positionen ≤ 20, Zusatzpositionen ≤ 10, Abzüge ≤ 10). Ergäbe eine Aufwandsrechnung mehr
als 20 Zeilen (zusätzliche Positionen zählen mit), werden die ältesten Tage zu Kalenderwochen zusammengefasst
(„KW 41/2026: … – 6,5 Std. à 100,00 €“), bis höchstens 20 Zeilen bleiben.
### Auf jeder Projektrechnung
- **Zusatzfelder** (`custom_fields`): „Projekt“ (Name, auf 140 Zeichen gekürzt), „Auftrag“ („AB-2026-0002 vom
06.10.2026“, nur mit AB), „Leistungszeitraum“ (bei Aufwand erster bis letzter Tag, sonst optional; immer änderbar).
- **Metadaten:** `webklarCustomerId`, `quelle`, `erstelltVon` (wie heute) plus `projektId`, `projektName`, `auftragId`,
`auftragNummer`, `art`.
- **`rendering.pdf.page_size = 'a4'`** – auch im bisherigen Dialog.
- Fuß unverändert (§ 19-Hinweis, Bankverbindung, Verwendungszweck).
### Abgerechnete Arbeitsblätter, Storno
- Jede Projektrechnung bekommt ein Dokument in `projektRechnungen` (Kennung = Stripe-Rechnungs-ID) mit Projekt, AB,
Art, Betrag und den **abgerechneten Arbeitsblatt-IDs**. Beim Anlegen prüft der Server, dass keines der Blätter in einer
aktiven Projektrechnung steckt (sonst 409 „Arbeitsblatt bereits abgerechnet“).
- Entwurf löschen oder Rechnung stornieren setzt das Dokument auf `aktiv = false`: die Blätter sind wieder frei, der
Betrag zählt nicht mehr als berechnet; eine Schlussrechnung setzt die AB auf den vorigen Status zurück.
### Steuernummer
- **Sofort, unabhängig vom Bau (Nutzer):** Steuernummer der GbR im Stripe-Dashboard (Einstellungen → Billing →
Rechnungen → „Steuerinformationen für Rechnungen“) hinterlegen und als Standard setzen – dann steht sie auf allen
Rechnungen, auch Abo-Rechnungen.
- Beim Livegang an einem Entwurfs-PDF prüfen, wie Stripe sie beschriftet. Ist die Beschriftung unpassend, kommt sie
stattdessen in den Rechnungsfuß (`invoiceFooter`, und per Skript in `invoice_settings.footer` bestehender
Stripe-Kunden).
- Für das AB-PDF: neues Einstellungsfeld `steuernummer` in WOMS Finanzen → Einstellungen.
### Kundenportal
- Die Rechnungsliste zeigt zusätzlich das Projekt (`metadata.projektName`).
## 4. Projektkonto (Rechenregeln)
Je Projekt, berechnet im Portal-Server bei jeder Anfrage (Stripe-Daten aus dem bestehenden 60-Sekunden-Zwischenspeicher;
fehlt eine Rechnung dort, wird sie einzeln gelesen):
| Feld | Regel |
|---|---|
| Vereinbart | Summe `summeCents` aller Festpreis-ABs des Projekts mit Status `versendet`, `bestaetigt` oder `abgerechnet`. Aufwands-ABs: Text „nach Aufwand, <Satz> €/Std.“ (mit Schätzung). Ohne AB: „keine AB“. |
| Berechnet | Summe der Rechnungen aus aktiven `projektRechnungen` des Projekts, die finalisiert sind (offen, überfällig, bezahlt, uneinbringlich); Entwürfe und Stornos zählen nicht. |
| davon bezahlt | Summe `amount_paid` dieser Rechnungen. |
| Noch offen | Festpreis: Vereinbart − Berechnet (nie unter 0). Aufwand: Wert der noch nicht abgerechneten Zeit. Ohne AB: „–“. |
| Zeit erfasst | Minuten aller Arbeitsblätter der Zeitquelle ohne Abo-Zeit (Regeln wie Aufwand), darunter „Wert x €“ zum Satz der AB bzw. 100 €; Abo-Zeit getrennt als „über Abo: y Std.“. |
Zeitquelle eines Projekts: Vereinigung der Zeitquellen seiner ABs, sonst das Ticket des Projekts; im Rechnungsdialog
können weitere Tickets des Kunden dazugenommen werden. „Ohne Projekt“: Stripe-Rechnungen des Kunden ohne aktive
`projektRechnungen`.
## 5. Daten (Appwrite `woms-database`, nur Server-Schlüssel)
Neue Collections (Setup-Skript `scripts/auftraege-setup.mjs`, idempotent wie `setup-finanzen.mjs`):
- **`auftraege`**: `nummer`, `jahr`, `laufnummer`, `status`, `kundenId`, `kundeJson` (Name, Firma, Anschrift, E-Mail,
Kundenart zum Zeitpunkt des Sendens), `projektId`, `titel`, `zeitquelleTicketIds` (string[]), `art`
(`festpreis` | `aufwand`), `positionenJson`, `stundensatzCents`, `schaetzungMinuten`, `summeCents`,
`zahlungsplanJson`, `zahlungszielTage`, `auftragVom`, `auftragWeg`, `startAm`, `fertigBis`, `texteFassung`,
`agbFassung`, `pdfDatei`, `pdfSha256`, `gesendetAm`, `gesendetAn`, `mailStatus`, `mailFehler`, `tokenHash`,
`tokenGueltigBis`, `bestaetigtAm`, `bestaetigtUeber`, `bestaetigtPdfSha256`, `fruehbeginn`, `fruehbeginnFassung`,
`ersetztId`, `ersetztDurchId`, `storniertAm`, `widerrufenAm`, `erstelltVon`, `erstelltAm`, `geaendertAm`.
Indizes: `kundenId`, `projektId`, `nummer` (normaler Index: Entwürfe haben keine Nummer; eindeutig über die Sperre
beim Senden), `tokenHash`.
- **`projektRechnungen`**: Dokument-ID = Stripe-Rechnungs-ID; `kundenId`, `projektId`, `auftragId`, `art`,
`betragCents`, `arbeitsblattIds` (string[]), `aktiv` (bool), `erstelltVon`, `erstelltAm`. Indizes: `kundenId`,
`projektId`, `auftragId`.
Neue Attribute: `customers.kundenart` (`''` | `unternehmen` | `privat`), `financeSettings.steuernummer`.
Stripe-Rechnungen werden weiter nicht gespiegelt.
## 6. Schnittstellen (Portal-Server)
Admin (unter dem bestehenden Finanzen-Router, `requireAppwriteStaff`):
| Methode, Pfad | Zweck |
|---|---|
| GET `/api/admin/finanzen/projektkonten?kunde=` | Projektkonten des Kunden inkl. „Ohne Projekt“ |
| GET `/api/admin/finanzen/projektkonten/:projektId` | Ein Projektkonto mit ABs, Rechnungen, offenen Arbeitsblättern |
| GET, POST `/api/admin/finanzen/auftraege` (`?kunde=`) | ABs lesen, Entwurf anlegen |
| GET, PATCH, DELETE `/api/admin/finanzen/auftraege/:id` | Lesen; Entwurf ändern/löschen |
| GET `/api/admin/finanzen/auftraege/:id/pdf` | Gespeichertes PDF bzw. Vorschau eines Entwurfs |
| POST `/api/admin/finanzen/auftraege/:id/senden` | Nummer, PDF, Ablage, Mail |
| POST `/api/admin/finanzen/auftraege/:id/erinnern` | K31 erneut, neuer Token |
| POST `/api/admin/finanzen/auftraege/:id/neue-fassung` | Kopie als Entwurf |
| POST `/api/admin/finanzen/auftraege/:id/stornieren` | Status `storniert` |
| POST `/api/admin/finanzen/rechnungen` | erweitert: `{ customerId, projektId?, auftragId?, art, items, arbeitsblattIds?, leistungszeitraum?, daysUntilDue, memo, senden, idempotencyKey }`; Positionen für `anzahlung`/`schluss`/`aufwand` baut der Server nach (Minuszeilen nur serverseitig) |
| PATCH `/api/admin/finanzen/einstellungen` | erweitert um `steuernummer` |
Alle Mitarbeiter (`/api/team`, `requireStaffJwt`): GET `/api/team/arbeitsblaetter/abgerechnet?ticketId=` →
`{ worksheetId: rechnungsnummer }` (ohne Beträge).
Kunden: GET `/api/auftraege` (eigene ABs), GET `/api/auftraege/:id/pdf`, POST `/api/auftraege/:id/bestaetigen`
(Portal-Sitzung; in der Kundenansicht gesperrt).
Öffentlich (Token): GET `/api/auftrag/:token`, GET `/api/auftrag/:token/pdf`, POST `/api/auftrag/:token/bestaetigen`
(`{ bestaetigt: true, fruehbeginn }`).
Widerruf: `/api/abo/public/withdraw` akzeptiert `order = projekt` mit `abNummer`.
Kundenart: WOMS schreibt `customers.kundenart` direkt (wie die übrigen Kundenfelder).
## 7. Mails
Nächste freie Kennungen zum Zeitpunkt des Baus prüfen (derzeit K31, K32, T22):
- **K31 Auftragsbestätigung** (Kunde; Anhänge AB-PDF und AGB-PDF; Variante Erinnerung; Variante Privatperson).
- **K32 Auftrag bestätigt** (Kunde; Eingangsbestätigung, Frühbeginn ja/nein).
- **T22 Auftrag bestätigt** (Team).
- Widerruf eines Projektauftrags nutzt die bestehenden Widerrufs-Mails (K04/T09) mit der neuen Vertragsart.
## 8. Rechtsrunde (vor dem Livegang von Plan 2)
Der Nutzer gibt frei; danach lädt Claude die Fassungen in WOMS → Rechtliches hoch:
- Textbausteine der AB (Einleitung, Aufwandsregel, Zahlungsplan, Mitwirkung/Abnahme, AGB-Satz).
- Widerrufsbelehrung für Projektaufträge und Muster-Widerrufsformular (Bauart wie `widerrufsbelehrung-funktionen`).
- Frühbeginn-Erklärung (Wortlaut nach Muster der Funktionen, passende Norm prüfen).
- Datenschutzerklärung: neuer Abschnitt „Auftragsbestätigungen“ (verarbeitete Daten, Bestätigungslink, keine IP,
Aufbewahrung 6 bzw. 8 Jahre) und Ergänzung des Widerrufs-Abschnitts.
- AGB als PDF (Fassung, aus der Website gedruckt).
## 9. Fehlerverhalten
| Fall | Verhalten |
|---|---|
| Stripe nicht erreichbar | Rechnung scheitert mit „Stripe ist gerade nicht erreichbar.“; ABs und Projektkonto (ohne Stripe-Teil, markiert) gehen weiter |
| PDF-Erzeugung scheitert | AB bleibt Entwurf, keine Nummer verbraucht, Meldung im Dialog |
| Mail scheitert | AB ist `versendet`, `mailStatus` „nicht zugestellt“, Knopf „Erneut senden“ |
| Link ungültig/abgelaufen | Seite erklärt es, nennt den Kontaktweg; WOMS kann per „Erinnerung senden“ einen neuen Link schicken |
| Doppelte Bestätigung | folgenlos, die erste zählt |
| Arbeitsblatt schon abgerechnet | 409 mit Rechnungsnummer |
| Schlussrechnung mit offenem Entwurf zur AB | 409 „Erst den Entwurf … senden oder löschen.“ |
| Fehlende Anschrift beim AB-Senden | 409 „Adresse des Kunden unvollständig“ |
| Kundenart leer beim AB-Senden | 400 (der Dialog fragt vorher) |
## 10. Tests
- **Unit** (`node --test`): Projektkonto-Regeln; Positionsbau je Art (Minuszeilen, Tageszeilen, 15-Minuten-Rundung,
Unterstützung, Wochen-Zusammenfassung über 20 Tage); AB-Nummern (Jahreswechsel, Sperre); Token (Hash, Ablauf,
Erneuerung); Frühbeginn-Datum; PDF enthält Pflichtangaben (Text aus dem PDF gelesen) und bei Privatpersonen die
Belehrung; Stripe-Parameter (`custom_fields`, `page_size`, Metadaten) gegen das Fake-Stripe.
- **Server-Ablauf** mit Appwrite-Nachbau, Fake-Stripe und SMTP-Senke (Rezept `kundenportal-testing`): AB anlegen →
senden → bestätigen (Link und Portal) → Anzahlung → Schlussrechnung; Aufwand mit Arbeitsblättern; Storno gibt frei;
Kundenansicht kann nicht bestätigen; Widerruf eines Projektauftrags.
- **WOMS:** Lib-Tests der Ansichtslogik; Bildschirmfotos über das Prüfwerkzeug des Redesigns (Desktop und Handy).
- **Kundenseiten:** Playwright (Handy, Desktop) gegen Fixture-Server.
- **Live:** nur Entwürfe mit dem Testkunden des Nutzers; nichts an echte Kunden.
## 11. Pläne und Livegang
- **Plan 1 – Rechnungen mit Projektkonto:** `projektRechnungen`, Projektkonto, Arten `aufwand`, `festbetrag` und
`frei`, Zusatzfelder, Metadaten, A4, Abzeichen „abgerechnet“, Projekt in der Kunden-Rechnungsliste,
WOMS-Oberfläche (Projektkonto, neuer Rechnungsdialog, Reiter umbenannt). In Plan 1 zeigt *Vereinbart* immer
„keine AB“. Keine Rechtsrunde nötig.
- **Plan 2 – Auftragsbestätigung:** `auftraege`, Kundenart, Steuernummer-Einstellung, PDF, Mails, Bestätigungsseite
und Portal-Abschnitt, Verbraucher-Variante, Widerruf, Arten `anzahlung` und `schluss` (mit Minuszeilen),
WOMS-Dialog und -Liste, Rechtsrunde.
- **Parallel gebaut, gestaffelt live:** Auf Wunsch des Nutzers (06.10.2026) werden beide Pläne gleichzeitig in
parallelen Strängen gebaut (Umsetzungsplan `2026-10-06-auftraege-00-uebersicht.md`). Der AB-Teil (und die Arten
`anzahlung`/`schluss`) hängt hinter dem Schalter `AUFTRAEGE_ENABLED` (`off` | `pilot` | `on`, Vorgabe `off`), so geht
der Rechnungsteil ohne Rechtsrunde live; der Datenschutz-Abschnitt kommt erst mit dem AB-Livegang.
- **Reihenfolge je Plan:** Portal-Server und Kundenseiten zuerst; WOMS-Oberfläche im neuen Design auf Basis der
zusammengeführten Redesign-Stränge Kunden (P4) und Finanzen (P7). Sind die Stränge beim Start der WOMS-Aufgaben noch
nicht zusammengeführt, wird mit dem Nutzer entschieden (warten oder auf `test` bauen und eine Mitnahme-Notiz für das
Redesign hinterlegen).
- **Livegang:** (0) Nutzer hinterlegt die Steuernummer in Stripe; (1) Setup-Skript; (2) Nutzer pusht Portal `main`;
(3) Nutzer pusht WOMS `test`; (4) Prüfung mit Testkunde (Entwürfe, PDF-Ansicht); bei Plan 2 vorher Rechtsrunde und
Upload in Rechtliches.
## Nicht im Umfang
Angebote vor dem Auftrag, Mahnwesen (Stripe-Erinnerungen wie heute), E-Rechnung, Abrechnung des Abo-Mehraufwands
(`hourLedger`), Nachlass-/Rabattzeilen, eigene Archivkopien der Stripe-PDFs, englische ABs, mehrere Währungen,
Projektmarge und Auswertungen, automatische Erinnerungen.
## Quellen
- § 34a UStDV: https://lxgesetze.de/ustdv/34a
- § 14b UStG: https://lxgesetze.de/ustg/14b
- § 312f BGB: https://www.haufe.de/id/norm/buergerliches-gesetzbuch-312f-abschriften-und-bestaetigungen-HI1049155_p312f.html
- Auftragsbestätigung (Inhalt): https://www.lexware.de/wissen/faktura-warenwirtschaft/auftragsbestaetigung-bedeutung-und-inhalt/
- Stripe Rechnungen anpassen: https://docs.stripe.com/invoicing/customize
- Agentur-Abrechnung (Festpreis/Aufwand): https://everhour.com/de/invoices/rechnungssoftware-fuer-digitalagenturen
## Nachträge aus der Planrunde (06.10.2026)
- Kundentexte: Mails, PDF und die öffentliche Bestätigungsseite in Sie-Form; der Abschnitt „Aufträge“ im angemeldeten
Portal in Du-Form wie das übrige Portal.
- Steuernummer-Feld akzeptiert auch eine USt-IdNr. („DE…“).
- Frühbeginn-Erklärung stützt sich auf § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB (Wortlaut in der Rechtsrunde).
- Bei Weg „vor Ort“ und Kundenart Privatperson zeigt der AB-Dialog den Hinweis „zusätzlich ausdrucken und übergeben“
(§ 312f Abs. 1 BGB), bis die Kanzlei-Frage beantwortet ist.
- Team-Mail T22 trägt im Betreff das Präfix „[Aufträge]“ wie die übrigen Team-Mails.
@@ -0,0 +1,296 @@
# Buchungskalender für Unterkünfte (Funktion `unterkunft`): Design
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
Stand 2026-10-07. Basis: Portal `origin/main` 8814c4a (TP2, Webshop, Öffnungszeiten im Pilot). Arbeitszweig
`feature/unterkunft`, Worktree `dev-unterkunft`. Pfade: **P** = Portal-Repo, **W** = WOMS-Repo, **D** = Website Donau Home
(`WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
Auftrag des Inhabers (2026-10-07): Auf der Donau-Home-Seite (MIMI FEWO, erste Kundin) ersetzt ein Kalender die Felder
Anreise/Abreise; der Gast wählt den Zeitraum, gibt Personen, Name, E-Mail, Telefon, Nachricht an. Die Anfrage geht an
die Vermieterin; gibt sie frei, bekommt der Gast eine Mail mit Rechnung und zahlt direkt oder vor Ort. Als Funktion im
Kundenportal, für alle Kunden. Entscheidungen des Inhabers U1–U7 (Abschnitt 13), alle übrigen von Claude (U8 ff.).
---
## 1. Ziel und Abgrenzung
Eine Funktion für Ferienwohnungen, Monteurzimmer und kleine Pensionen: Belegungskalender auf der Website, Anfrage,
Freigabe durch den Vermieter, verbindliche Buchung durch den Gast, Rechnung im Namen des Vermieters, Zahlung online
(Stripe, direkt an den Vermieter) oder vor Ort, Abgleich mit Booking.com/Airbnb per iCal.
Nicht im Umfang: Meldeschein (Bundesmeldegesetz), Kurtaxe/Gästebeitrag, Anzahlungen und Teilzahlungen, Sofortbuchung
ohne Freigabe, saisonale Preise (nur Staffel nach Aufenthaltsdauer), Gutscheine, Channel-Manager-APIs außer iCal,
Mehrsprachigkeit (nur Deutsch).
Begriffe: **Vermieter** = unser Kunde (Mimi). **Gast** = wer auf der Website anfragt. **Unterkunft** = buchbare Einheit
(Wohnung, Zimmer).
## 2. Ablauf
1. **Website (Widget):** Kalender zeigt belegte Nächte der gewählten Unterkunft (bei nur einer Unterkunft keine Auswahl).
Erster Klick Anreise, zweiter Abreise, Spanne markiert, Zusammenfassung „12.–30. Nov. · 18 Nächte“. Belegte Nächte
und Vergangenheit nicht wählbar; ein Zeitraum über eine belegte Nacht hinweg ebenso nicht; Mindestnächte werden
geprüft. Formular: Personen (1 … Höchstzahl), Firma (optional), Name, E-Mail, Telefon (optional), Nachricht
(optional), Datenschutzhinweis. Knopf **„Anfrage senden“** (unverbindlich, U6).
→ Anfrage `neu`, **G16** Eingangsbestätigung an den Gast, **K31** Neue Anfrage an den Vermieter.
2. **Portal, Reiter „Buchungen“ → Anfragen:** Detail mit vorgerechnetem Preis (Posten editierbar: Betrag, Text, Posten
hinzufügen/entfernen), Reservierungsfrist (Standard aus Einstellungen), optionale Nachricht an den Gast.
**Freigeben** → Prüfung unter Sperre, dass der Zeitraum frei ist; Status `freigegeben`, Nächte als Reservierung
gesperrt bis `fristAt`, **G17** an den Gast mit Preis, Stornobedingungen und Buchungslink.
**Ablehnen** (optional mit Text) → `abgelehnt`, **G18**.
3. **Buchungsseite** `https://project.webklar.com/unterkunft/<siteKey>/buchung/<id>?t=<token>` (Abschnitt 6):
Rechnungsadresse, Zahlart „online“/„vor Ort“, alle Pflichtangaben, Knopf **„Zahlungspflichtig buchen“** (U6).
→ Prüfung unter Sperre (Status `freigegeben`, Frist nicht abgelaufen, Zeitraum frei); Status `gebucht`,
Rechnungsnummer vergeben, PDF erzeugen, **G19** Buchungsbestätigung mit PDF an den Gast, **K32** an den Vermieter.
Bei „online“ direkt weiter zu Stripe Checkout; Zahlung → `bezahlt`, **G20**. Bricht der Gast die Zahlung ab, bleibt
die Buchung `gebucht` (unbezahlt); G19 und die Buchungsseite enthalten „Jetzt online bezahlen“.
4. **Frist abgelaufen** (Minuten-Job): `freigegeben` → `abgelaufen`, Nächte frei, **G21**.
5. **Vor Ort bezahlt:** Vermieter setzt „bezahlt (vor Ort)“ mit Datum; keine Mail.
6. **Stornierung** durch den Vermieter (nur `freigegeben`, `gebucht`, `bezahlt`): Grund, bei Online-Zahlung Erstattung
0 … Gesamtbetrag über Stripe; bei vorhandener Rechnung Stornorechnung (Rechnungskorrektur mit eigener Nummer, voller
Betrag negativ); **G22** an den Gast mit Stornorechnung. Stornierungen auf Wunsch des Gastes laufen ebenso über den
Vermieter (der Gast schreibt ihm; keine Selbststornierung im Widget).
Statusmaschine (nur vorwärts, Übergänge idempotent unter Sperre `unterkunft:<unterkunftId>`):
`neu → freigegeben → gebucht → bezahlt`; `neu → abgelehnt`; `freigegeben → abgelaufen`;
`freigegeben|gebucht|bezahlt → storniert`. `zahlart` ∈ {`online`, `vorort`} wird bei `gebucht` gesetzt;
`bezahltVia` ∈ {`stripe`, `vorort`}.
## 3. Datenmodell (Appwrite, `scripts/unterkunft-setup.mjs`, nur Server-Key, Muster `shop-setup.mjs`)
Alle Dokumente tragen `customerId`, `projectId`, `featureId` (portalFeatures-Dokument der Funktion).
- **`ukUnterkuenfte`**: `name`, `beschreibung` (kurz, optional), `maxPersonen` (1–20), `mindestNaechte` (1–90),
`checkIn`/`checkOut` (HH:MM), `sort`, `aktiv`, `icalImporte` (JSON-Array {id, name, url, letzterAbrufAt,
letzterErfolgAt, fehler}), `icalExportToken` (32 Byte, zufällig, erneuerbar).
- **`ukPreise`** (1:1 zur Unterkunft): `staffeln` (JSON [{abNaechten, preisProNachtCent}], aufsteigend, erste ab 1),
`reinigungCent`, `personenInklusive`, `aufpreisPersonNachtCent`, `direktrabattProzent` (0–50, ganze Zahl),
`steuerart` (`kleinunternehmer` | `regel`), `ustUebernachtung` (Standard 7), `ustReinigung` (Standard 7),
`fristTage` (1–14, Standard 3), `stornobedingungen` (Text, Pflicht vor dem Freischalten).
- **`ukSperren`**: `unterkunftId`, `von`, `bis` (Datum, `bis` = Abreisetag exklusiv, also belegte Nächte von..bis-1),
`quelle` (`manuell` | `ical:<importId>` | `anfrage:<anfrageId>`), `notiz` (nur manuell, nur Portal).
Reservierungen und Buchungen werden nicht als Sperre kopiert, sondern direkt aus `ukAnfragen` gelesen (Status
`freigegeben` mit `fristAt` > jetzt, `gebucht`, `bezahlt`); `ukSperren` enthält nur `manuell` und `ical:*`.
- **`ukAnfragen`**: `nummer` (kurz, z. B. `A-7K2Q`), `unterkunftId`, `von`, `bis`, `personen`, `status`, `gast`
(JSON {name, firma, email, telefon, nachricht}), `rechnungsadresse` (JSON {name, firma, strasse, plz, ort, land}),
`posten` (JSON [{text, menge, einzelCent, ustProzent}], Schnappschuss bei Freigabe), `summen` (JSON {netto, ust je
Satz, brutto}), `nachrichtVermieter`, `fristAt`, `tokenHash` (sha256 des Buchungslink-Tokens), `zahlart`,
`bezahltVia`, `bezahltAt`, `stripeSessionId`, `stripePaymentIntentId`, `erstattetCent`, `stornoGrund`,
`stornobedingungenText` (Schnappschuss bei Freigabe), `rechtstexteFassung`, `ipHash` (nur bis Freigabe/Ablehnung,
danach entfernt), Zeitstempel je Status.
- **`ukRechnungen`**: `anfrageId`, `art` (`rechnung` | `storno`), `nummer`, `datum`, `fileId` (Bucket
`uk-rechnungen`), `aussteller` (JSON-Schnappschuss der Firmendaten), `bruttoCent`.
- **`ukEinstellungen`** (1 je Funktion): `firma`, `inhaber`, `strasse`, `plz`, `ort`, `email`, `telefon`,
`steuernummer`/`ustId` (eins Pflicht), `iban`/`bic`/`bank` (optional, für „vor Ort/Überweisung“-Hinweis), `rechnungsPraefix`
(2–8 Zeichen A–Z0–9, Standard `RE`), `rechnungZaehler` (JSON {jahr, n}), `akzentfarbe`, `stripeAccountId`,
`stripeStatus` (Muster Webshop), `onlineZahlung` (an/aus; aus, solange Stripe nicht aktiv).
Rechnungsnummern: `<Präfix>-<Jahr>-<n vierstellig>`, lückenlos je Funktion und Jahr, vergeben unter Sperre
`uk-rechnungsnr:<featureId>` (keyedLock) mit Lesen–Erhöhen–Schreiben des Zählers vor dem PDF; schlägt das PDF fehl, wird
es neu erzeugt (Nummer bleibt). Storno-Rechnungen ziehen aus demselben Zähler.
## 4. Preisrechnung (`server/services/unterkunft/preis.js`, rein, ohne I/O)
`berechnePreis({ preise, von, bis, personen })` → `{ naechte, posten, summen }`:
- `naechte` = Tage zwischen `von` und `bis`; Staffel = höchste `abNaechten` ≤ `naechte` für **alle** Nächte.
- Posten: „Übernachtung, N Nächte à X €“ (ust Übernachtung); falls `personen` > `personenInklusive`: „Zusatzperson(en),
k × N Nächte à Y €“; „Endreinigung“ (falls > 0, ust Reinigung); „Direktbuchungsrabatt P %“ als negativer Posten auf
Übernachtung + Zusatzpersonen (nicht auf Reinigung), mit ust Übernachtung.
- Beträge sind Bruttobeträge in Cent. Bei `regel` wird die enthaltene USt je Satz aus der Summe der Posten dieses Satzes
herausgerechnet und kaufmännisch auf Cent gerundet; bei `kleinunternehmer` keine USt.
- Mimis Änderungen ersetzen die Posten vollständig; `summen` werden immer serverseitig aus den Posten neu berechnet
(`summiere(posten, steuerart)`), nie aus dem Browser übernommen.
## 5. Verfügbarkeit und iCal (`server/services/unterkunft/belegung.js`, `ical.js`)
- `belegteNaechte(unterkunftId, von, bis)`: Vereinigung aus `ukSperren` und aktiven Anfragen (Abschnitt 3).
`istFrei(...)` wird bei Freigabe und Buchung **unter der Sperre** erneut geprüft (U9). Eine eigene Anfrage zählt bei
ihrer eigenen Buchung nicht als belegt.
- **Import** (Job alle 15 Minuten, je Import höchstens 1 MB, Zeitlimit 10 s, nur `https:`, keine privaten/lokalen
Ziele — Prüfung der aufgelösten IP gegen private Netze): VEVENT mit `DTSTART`/`DTEND` (DATE oder DATE-TIME →
Datum), ohne `DTEND` = 1 Nacht, `STATUS:CANCELLED` ignorieren, nur Zeitraum heute−1 Tag bis +18 Monate. Erfolgreicher
Abruf ersetzt alle Sperren `ical:<importId>` (erst neue anlegen, dann alte löschen, damit nie eine Lücke entsteht). Fehler: alter Stand
bleibt, `fehler` gesetzt; ab 6 Stunden ohne Erfolg **K33** (höchstens einmal je 24 h und Import). Parser selbst
geschrieben (Zeilenfaltung, Escapes), keine Abhängigkeit.
- **Export** `GET /unterkunft/ical/<icalExportToken>.ics`: VCALENDAR mit einem VEVENT „Belegt“ je gebuchter/bezahlter
Buchung, Reservierung und manueller Sperre (nicht die importierten, sonst Echo), `UID` stabil, keine Gastdaten.
Bei der Booking.com-Anleitung im Portal: Link kopieren, bei Booking.com „Kalender synchronisieren“ eintragen.
## 6. Website-Widget und öffentliche Schnittstellen
Einbau (Muster Termine/Shop): `<div data-webklar-unterkunft="<siteKey>"></div>` +
`<script src="https://project.webklar.com/embed/unterkunft.js" defer></script>`. Shadow DOM, Stile über CSS-Variablen
(`--wk-uk-akzent`, `--wk-uk-text`, `--wk-uk-flaeche`, `--wk-uk-linie`, `--wk-uk-radius`, `--wk-uk-schrift`), damit jede
Seite das Widget anpassen kann. Layout: zwei Spalten (Kalender links, Formular rechts), unter 760 px untereinander.
Kalender: ein Monat sichtbar (zwei ab 1100 px Breite), Pfeile, Tastatur (Pfeiltasten, Enter), `aria-label` je Tag mit
Datum und Status, belegte Nächte durchgestrichen/grau, Anreise an einem Tag, an dem nur eine Abreise liegt, ist möglich
(Nächte-Logik).
Öffentliche Routen (`server/routes/publicUnterkunft.js`, Antworten nie mit IDs außer Unterkunft-IDs, keine Gastdaten):
- `GET /api/public/unterkunft/:siteKey` → Unterkünfte (id, name, maxPersonen, mindestNaechte, checkIn/Out),
Akzentfarbe, Datenschutz-Link-Ziel, Anbietername.
- `GET /api/public/unterkunft/:siteKey/belegung?unterkunft=<id>&von=&bis=` (höchstens 18 Monate) → belegte Nächte als
Bereiche. Cache 60 s.
- `POST /api/public/unterkunft/:siteKey/anfrage` → Prüfung (Pflichtfelder, Längen, E-Mail, Zeitraum frei und gültig,
Personen), Honeypot-Feld + Mindest-Ausfüllzeit, Rate-Limit je IP-Hash und Site (Muster Termine). Antwort: Anfragenummer.
- `GET /api/public/unterkunft/:siteKey/buchung/:id?t=` und `POST …/buchung/:id` (Token prüfen gegen `tokenHash`,
zeitkonstanter Vergleich; nach `abgelaufen`/`abgelehnt`/`storniert` nur Statusanzeige) → Buchungsseite.
- `POST …/buchung/:id/zahlung?t=` → Stripe Checkout Session auf dem verbundenen Konto (Direct Charge, Muster Webshop:
`mode: payment`, ein Posten „Aufenthalt <Unterkunft> <von>–<bis>, Rechnung <Nr>“, Betrag = brutto, `expires_at` 30 min,
`metadata` {ukAnfrageId}, `success_url` zurück zur Buchungsseite). Abschluss über den bestehenden Connect-Webhook
(Weiterleitung nach `metadata`) **und** beim Laden der Buchungsseite (Muster Webshop „Danke-Seite schließt selbst ab“).
**Buchungsseite** (Standalone-Seite im Portal-Design mit Akzentfarbe des Vermieters, § 312j BGB und Art. 246a EGBGB,
unmittelbar über dem Knopf): Anbieter (Firma, Anschrift, Kontakt), Unterkunft, An-/Abreise mit Check-in/-out-Zeiten,
Personen, alle Posten, Gesamtpreis inkl. USt bzw. Kleinunternehmer-Hinweis, Zahlarten, Stornobedingungen (Schnappschuss),
Hinweis „Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Nr. 9 BGB).“, Datenschutzhinweis des Vermieters, Frist.
Pflichtfelder Rechnungsadresse (Name/Firma, Straße, PLZ, Ort, Land; Vorbelegung aus der Anfrage). Korrektur bis zum Knopf
möglich. Knopf **„Zahlungspflichtig buchen“**; „online“ nur wählbar, wenn `onlineZahlung` an.
## 7. Portal für den Vermieter (Funktionen → „Buchungen“, Hash `#buchungen`)
Nach dem Muster des Webshop-Bereichs; nur sichtbar, wenn die Funktion für das Projekt aktiv ist (Einrichtung: schon in
`setup` nutzbar zum Vorbereiten, Widget erst nach Freischaltung öffentlich).
- **Anfragen:** Filter offen (neu, freigegeben) / gebucht / alle; Liste mit Nummer, Zeitraum, Nächten, Gast, Status,
Betrag. Detail: Gastangaben, Kalenderausschnitt, Posten-Editor mit Live-Summe, Frist, Nachricht, Freigeben/Ablehnen;
bei Buchungen Rechnung herunterladen, „bezahlt (vor Ort)“, Stornieren (mit Erstattungsbetrag bei Online-Zahlung).
- **Kalender:** Monatsansicht je Unterkunft, Farben nach Quelle (Direkt gebucht, reserviert, Booking/iCal, manuell);
Klick-Klick auf freie Nächte → manuelle Sperre mit Notiz; Klick auf manuelle Sperre → löschen.
- **Unterkünfte & Preise:** Anlegen/Bearbeiten/Sortieren/Deaktivieren, Preis-Editor mit Beispielrechnung („7 Nächte, 2
Personen = …“), iCal-Importe (Name + URL, Status letzter Abruf), Export-Link mit Kopieren und Anleitung für Booking.com
und Airbnb, „Link erneuern“.
- **Rechnungen:** Liste (Nummer, Datum, Gast, Betrag, Art), PDF-Download, Jahres-ZIP-Download.
- **Einstellungen:** Firmendaten, Steuer, Rechnungspräfix (änderbar nur vor der ersten Rechnung), Bankverbindung,
Akzentfarbe, Stripe verbinden (Wiederverwendung `shop/connect.js`, ein Stripe-Konto je Kunde: hat der Kunde bereits ein
Shop-Konto, wird es angeboten), Online-Zahlung an/aus, Mustertext Stornobedingungen zum Übernehmen.
- Pflicht vor dem Freischalten (`freischaltbar`): mindestens eine aktive Unterkunft mit Preisen und Stornobedingungen,
Firmendaten vollständig, Steuernummer oder USt-ID.
## 8. Rechnung (PDF, `server/services/unterkunft/rechnungPdf.js`)
Mit der vorhandenen PDF-Bibliothek des Portals (Plan prüft, welche; keine neue Abhängigkeit, wenn vermeidbar).
Pflichtangaben § 14 Abs. 4 UStG: Name/Anschrift Aussteller und Empfänger, Steuernummer oder USt-ID, Rechnungsdatum,
Rechnungsnummer, Leistung („Beherbergung <Unterkunft>, <Anschrift der Unterkunft = Firmenanschrift, sofern nicht
abweichend>“), Leistungszeitraum (An–Abreise), Posten, Entgelt netto je Steuersatz, Steuersatz und Steuerbetrag bzw.
„Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“, Gesamtbetrag. Zusätzlich Zahlart und Status („bezahlt am …“ wird
nicht nachträglich ins PDF geschrieben; die Rechnung bleibt unverändert). Storno-Rechnung verweist auf die
Ursprungsrechnung. Kein WEBklar-Hinweis auf der Rechnung (U10), sie ist die des Vermieters.
## 9. Bestellung der Funktion bei WEBklar (Register-Eintrag `unterkunft`)
Muster Webshop: `product: 'unterkunftSetup'` (19900 Cent, Lookup-Key `webklar_unterkunft_einrichtung_19900`),
`priceLabel '199 €'`, `setupEstimate 'meist innerhalb von 1–2 Wochen'`, `requiresWartungsAbo: false`,
`ticketFreischaltung: true`, `returnHash 'buchungen'`, `returnProjekt: true`. Haken: `verfuegbar` (Schalter
`UNTERKUNFT_ENABLED` aus/pilot/an + `UNTERKUNFT_PILOT_EMAILS`), `bestellpruefung` (Anfrage angenommen, Muster Webshop
`anfrage.js`), `unterlagenBereit` (Leistungsbeschreibung + AVV Buchungskalender), `belehrung` (gemeinsame),
`freischaltbar` (Abschnitt 7). Anfrageformular der Funktion: Art der Unterkunft, Zahl der Einheiten, Booking.com/Airbnb
genutzt ja/nein, Nachricht → **K34** an den Kunden, **T22** an das Team. Zustimmungen wie Webshop: unterlagen,
verantwortung („Ich vermiete in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten bin ich
verantwortlich.“), stripe (nur wenn Online-Zahlung genutzt werden soll; sonst entfällt), beginn, information.
Ticket „Buchungskalender einbauen – <Projekt>“ mit siteKey, Snippet, Schritten (Kalender-Bereich anlegen, Snippet,
CSS-Variablen an die Seite anpassen, Datenschutz-Absatz einfügen, Testanfrage), Einbauanleitung
`docs/funktionen/unterkunft-einbau.md`. Für Kunden, die die Funktion als Teil ihres Projekts bekommen (Mimi), schaltet
das Team sie wie bei den anderen Funktionen manuell frei (vorhandener Weg „ohne Zahlung freischalten“ im WOMS; der Plan
prüft, ob es ihn gibt, sonst Skript `scripts/unterkunft-freischalten.mjs`).
## 10. WOMS
Wie Webshop: Anfragen der Funktion in der Kunden-Funktionsansicht annehmen/ablehnen; Anzeige des Funktionsstatus;
Rechtliches-Karten `leistungsbeschreibung-unterkunft` und `avv-unterkunft` (Sortnummern nach den belegten, der Plan
prüft `setup-legal.mjs`). Keine Verwaltung der Buchungen im WOMS (Datenhoheit beim Kunden).
## 11. Mails (Vorlagen im Stil der bestehenden, `server/mail-templates`)
Gast (Absender WEBklar-Versandadresse, Anzeigename = Firma des Vermieters, Reply-To = E-Mail des Vermieters):
G16 Anfrage eingegangen · G17 Ihre Anfrage ist frei – jetzt buchen (Preis, Frist, Stornobedingungen, Link) ·
G18 Leider nicht möglich · G19 Buchungsbestätigung (alle Angaben, Stornobedingungen, Rechnungs-PDF, ggf. Bezahl-Link;
dauerhafter Datenträger § 312f BGB) · G20 Zahlung eingegangen · G21 Reservierung abgelaufen · G22 Stornierung
(Stornorechnung-PDF, Erstattungsbetrag).
Vermieter: K31 Neue Buchungsanfrage · K32 Neue Buchung · K33 Abgleich fehlgeschlagen · K34 Ihre Anfrage zum
Buchungskalender. Team: T22 Neue Anfrage Buchungskalender. Nummern vor dem Bau gegen `main` prüfen (Stand: K30, G15,
T21 belegt).
## 12. Datenschutz, Aufbewahrung, Rechtstexte
- Rollen: Vermieter ist Verantwortlicher für Gastdaten, WEBklar Auftragsverarbeiter → **AVV Buchungskalender** (Muster
AVV Webshop; Unterauftragsverarbeiter Hetzner, Stripe nur als eigener Vertragspartner des Vermieters).
- Fristen: `ipHash` weg bei Freigabe/Ablehnung bzw. nach 30 Tagen; Anfragen ohne Buchung (abgelehnt, abgelaufen, neu
älter als 60 Tage) nach 6 Monaten anonymisiert; Gastdaten gebuchter Aufenthalte 3 Jahre nach Jahresende der Abreise
anonymisiert, Rechnungs-PDFs und Rechnungsadresse bleiben 8 Jahre (§ 147 AO, Buchungsbelege) solange die Funktion
läuft; bei Abschaltung: Jahres-ZIP-Download angeboten, Hinweis-Mail, Löschung 90 Tage danach (Muster Webshop).
Täglicher Job im bestehenden Aufräum-Muster (`retention.js`).
- Datenschutz WEBklar: neuer Abschnitt **§ 6f** (6d Webshop, 6e Öffnungszeiten; Plan prüft). Muster-Absatz für die
Website des Vermieters `docs/funktionen/unterkunft-datenschutz-absatz.md`. Mustertext Stornobedingungen (mit Hinweis
„Vorlage, keine Rechtsberatung“).
- WEBklar-Texte (Fassung `2026-10-07`): Leistungsbeschreibung Buchungskalender, AVV Buchungskalender, Kanzlei-Fragen
`docs/rechtliches/unterkunft-kanzlei-fragen.md` (u. a. Button-Lösung über Mail-Link, USt-Satz Endreinigung, Rolle bei
Rechnungserstellung im Namen des Vermieters § 14 Abs. 2 S. 4 UStG — Rechnung durch Dritten im Auftrag, Vermieter muss
zustimmen → Zustimmung beim Bestellen und Einstellungen-Haken).
- Upload der Texte in WOMS Rechtliches macht Claude selbst (Feedback des Inhabers), sobald der Inhaber die Texte
freigibt.
## 13. Entscheidungen
Inhaber (2026-10-07): **U1** allgemeine Funktion für alle Kunden, Mimi zuerst. **U2** Stripe Connect wie Webshop, Geld
direkt an den Vermieter, PDF-Rechnung in dessen Namen, Zahlung online oder vor Ort. **U3** Preise im Portal hinterlegt,
vorgerechnet, vor Freigabe änderbar. **U4** iCal-Abgleich in beide Richtungen + manuelle Sperren. **U5** mehrere
Unterkünfte je Website. **U6** Anfrage unverbindlich, Buchung erst per „Zahlungspflichtig buchen“ nach Freigabe,
Reservierung bis Frist (Standard 3 Tage). **U7** 199 € einmalig auf Anfrage, keine laufenden Kosten. Bau in parallelen
Strängen.
Claude: **U8** eigenes Modul statt Terminplaner-Erweiterung oder Fremdsystem. **U9** Freigabe und Buchung prüfen
Verfügbarkeit unter Sperre erneut. **U10** keine WEBklar-Werbung auf der Rechnung. **U11** Staffelpreis gilt für alle
Nächte. **U12** Rabatt nicht auf Reinigung. **U13** Stornierung nur durch den Vermieter. **U14** Zahlungsabbruch lässt
die Buchung bestehen (unbezahlt). **U15** ein Stripe-Konto je Kunde für Shop und Buchungen. **U16** Widget ohne
Fremdbibliothek, Shadow DOM, CSS-Variablen.
## 14. Website Donau Home (D)
Abschnitt `#anfrage`: das bisherige Formular wird durch das Widget ersetzt (Kalender links unter Überschrift und
Einleitung, Formular rechts; Telefon/E-Mail unter dem Kalender). CSS-Variablen in Palette A (Eiche & Leinen) und
Schrift Figtree; Widget-Kalender optisch an den Zeichenstil angelehnt (Rahmen, Akzent Eiche). Bis zur Freischaltung
zeigt die Vorschau das Widget über `UNTERKUNFT_ENABLED=pilot` mit Mimis siteKey. Einleitungstext anpassen („Wählen Sie
Ihren Zeitraum im Kalender …“). Kein Push durch Claude.
## 15. Tests und Livegang
- Unit: `preis.js` (Staffeln, Zusatzpersonen, Rabatt, USt-Rundung, Kleinunternehmer), `ical.js` (Parser inkl. Faltung,
DATE/DATE-TIME, CANCELLED, Export), Statusmaschine, Rechnungsnummern unter Parallelität, Belegung/Überschneidung,
öffentliche Routen (Validierung, Rate-Limit, Token), Aufbewahrung. Muster der bestehenden Tests (Appwrite-Mock),
Node-Läufe mit `--max-old-space-size=1024` und Timeout.
- UI: Playwright-Durchlauf (Muster `test/ui/shopTestServer.mjs`): Anfrage im Widget → Freigabe im Portal → Buchungsseite
→ „vor Ort“ → Rechnung; Online-Zahlung gegen Stripe-Attrappe.
- Livegang (Notiz `docs/funktionen/unterkunft-livegang.md`): Setup-Skript, Stripe-Preis anlegen, Connect-Webhook-Events
prüfen (vorhandener Endpunkt), compose/.env `UNTERKUNFT_ENABLED=pilot`, `UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,
<Mimi>`, Push durch den Inhaber (`feature/unterkunft:main`, nie unter eigenem Namen), Rechtstexte hochladen, Pilot mit
Mimi, danach `on`.
## 16. Parallele Stränge (für den Plan)
0. **Kern zuerst** (seriell, klein): Schema + Setup-Skript, Schalter, Register-Eintrag, `preis.js`, `belegung.js`,
Statusmaschine mit festen Funktionsschnittstellen und Attrappen.
Danach gleichzeitig:
1. **Ablauf + Server:** öffentliche Routen, Portal-API, Stripe, Jobs (Frist, iCal, Aufbewahrung), Rechnungsnummern.
2. **Portal-Oberfläche + WOMS.**
3. **Widget + Buchungsseite + Donau-Home-Einbau.**
4. **Mails, Rechnungs-PDF, Rechtstexte, Einbau- und Livegang-Notiz.**
Abschluss: Zusammenführen, Gesamttests, Review, UI-Durchlauf.
## 17. Nachträge aus der Code-Prüfung (2026-10-07, vor dem Plan)
- **N1 (ersetzt U16):** Widget nach dem Muster `shop.js`/`termine.js` statt Shadow DOM: eigenes `<style id="wku-style">` mit
`@layer wku`, Klassenpräfix `.wku-`, Variablen `--wku-akzent`, `--wku-akzent-text`, `--wku-hintergrund`, `--wku-text`,
`--wku-rand`, `--wku-radius`, `--wku-schrift`, `--wku-schrift-titel`. Öffentliche API unter `/api/public/unterkunft/v1/`.
- **N2 (ersetzt den Bucket `uk-rechnungen`):** Keine Datei-Ablage. `ukRechnungen.inhalt` speichert den vollständigen,
unveränderlichen Schnappschuss (Aussteller, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug); das PDF
wird bei jedem Abruf daraus deterministisch erzeugt. Eigener minimaler PDF-Writer (Helvetica, WinAnsi inkl. Umlaute und
€), keine neue Abhängigkeit.
- **N3:** Die Mail-Warteschlange (`termine/outbox.js`) bekommt ein optionales Feld `anhang` (JSON-Verweis
`{art:'ukRechnung', rechnungId}`), `sendGuestMail` einen Parameter `attachments`; das PDF entsteht beim Versand.
- **N4 (konkretisiert U15):** `ukEinstellungen.stripeAccountId` ist eigen; hat das Projekt ein Webshop-Konto
(`shopConfig.stripeAccountId`), bietet das Portal „vorhandenes Stripe-Konto verwenden“ an. Metadaten `wkUk:'1'`;
`account.updated` aktualisiert auch `ukEinstellungen`; `erstatten` bekommt einen Parameter für das Metadatum `webklar`.
- **N5:** Datenschutz-Abschnitt bleibt § 6f (6e ist für Öffnungszeiten reserviert, noch nicht live).
- **N6:** Kein Textform-Nachweis (nicht in `TEXTFORM_KEYS`); Mimi wird im WOMS über „Freischalten“ (`action:'activate'`)
ohne Zahlung freigeschaltet.
- **N7:** Kein Jahres-ZIP der Rechnungen (YAGNI); Einzel-PDF-Download je Rechnung. Bei Abschaltung Hinweis-Mail mit der Bitte, die PDFs herunterzuladen.
@@ -0,0 +1,412 @@
# Kundenansicht neu: weiß, schlank, Funktionen im Abo
Stand 15.10.2026. Brainstorming mit dem Inhaber (Kenso) in dieser Sitzung. Basis ist `origin/main` d33a483.
Bestandsaufnahme (nur gelesen) unter `/root/reports/kundenansicht-neu-2026-10-15/bestand/`:
- `ansichten.md`: Karte aller 18 Ansichten, Navigation, Design heute
- `bestaetigungen.md`: jedes Häkchen mit Datei:Zeile, Server-Prüfung, Nachweis
- `funktionen.md`: Katalog, Zustände, Einstieg, Reiter je Funktion
- `woms-design.md`: Tokens und Regeln des weißen Ticketsystems
- `recht.md`: Mindestanforderungen mit Quellen, Fragen an die Kanzlei
Navigations-Entwürfe (Workflow mit 5 Entwürfen, 2 Prüfern, 3 Nachschärfungen) liegen unter `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/`. Gewählt wurde `final-2-frei.html`, die „Fahrstuhl-Zeile“.
Kennzeichnung:
- **[Inhaber]** hat Kenso entschieden.
- **[Claude]** ist eine Festlegung von mir, die er gesehen und nicht verworfen hat.
- **[Bauer]** ist dem Umsetzer überlassen.
---
## 1. Absicht und Warum
Die Kundenansicht (`project.webklar.com`, `public/dashboard.html` + `app.js` + Funktionsmodule) ist über Monate gewachsen. Heute ist sie dunkel und lila, hat 18 Ansichten und viele Erklärsätze. Beim Buchen einer Funktion stehen bis zu sechs Häkchen mit Paragrafen-Text und die ganze Widerrufsbelehrung als Scrolltext. Der Inhaber sagt dazu: „bei den Häkchen beim Funktionen buchen, viel zu viel Text“.
Die Kunden sind fast nur kleine Betriebe (Friseur, Bäckerei, Ferienwohnung, Physio), Privatkunden kaum. Viele nutzen das Portal 1–2× im Monat als installierte Web-App auf dem Handy.
Das Ziel hat drei Teile:
1. Die Kundenansicht bekommt den Stil des neuen weißen Ticketsystems (WOMS, live seit 06.10.2026): minimal, aufgeräumt, wenig Text.
2. Die Funktionen werden nicht mehr einzeln verkauft. Sie stecken im Abo.
3. Bestätigungen werden auf das rechtlich Nötige reduziert. Ein Betrieb soll nichts „durchklicken“ müssen, und trotzdem bleibt alles rechtlich sauber.
Die Handy-Navigation soll laut Inhaber „zeitlos, modern, mit Wow-Effekt“ sein.
## 2. Ziele, Nicht-Ziele, Anti-Ziele
**Ziele**
- Eine weiße Kundenansicht, die zum Ticketsystem passt, auf Handy und Desktop.
- Eine Handy-Leiste mit vier Wörtern und federndem schwarzem Balken. Die Unterseiten fahren als „Etage“ hoch.
- Funktionen mit den Reitern „Meine“ und „Entdecken“, ohne Preise, mit dem Knopf „Einschalten“.
- Ein Betrieb setzt beim Buchen und Einschalten **kein** Häkchen, ein Privatkunde höchstens eins.
- Überall „du“, deutlich weniger Erklärtext.
- Zuerst eine Vorschau mit Testdaten, dann Livegang in Paketen.
**Nicht-Ziele**
- Keine neue Funktion und keine neue Geschäftslogik außer dem Abo-Modell der Funktionen.
- Kein Umbau der Gäste- und Käuferseiten der Kunden (`termin.html`, `unterkunft-seite.html`, `shop-seite.html`, `embed/*`). Ausnahme: Sie lesen Texte oder Zustände, die sich durch dieses Projekt ändern.
- Kein Umbau der Mails im Aussehen. Mailtexte, die Preise oder Zahlung für Funktionen nennen, werden aber inhaltlich angepasst.
- Kein Dunkelmodus und kein Umschalter. Das Ticketsystem hat auch keinen.
- Keine Änderung der Abo-Preise (Hosting 20 €, Wartung 50 €).
**Anti-Ziele.** Das Projekt gilt als gescheitert, auch wenn es technisch läuft, wenn
- ein Laie (Inhaberin, 60, Brille vergessen) Chat, „Rechnung zahlen“, eine Funktion oder „Kündigen“ nicht findet;
- „Verträge hier kündigen“ oder „Vertrag widerrufen“ mehr als 2 Taps entfernt sind;
- zwischen Kerninfo und „Zahlungspflichtig bestellen“ ein Akkordeon, Sheet oder Scrollen liegt;
- das Einschalten einer Funktion versteckt Kosten auslöst;
- Hashes, die Mails, Stripe oder Links schon nutzen, nicht mehr funktionieren (`#news`, `#termine`, `#webshop`, `#buchungen`, `#zeiten`, `#chat`, `#rechnungen`, `#vertraege`, `#wartung`, `#abo`, `#gutschrift`, `#passwort` …);
- der Nachweis einer Zustimmung (Wortlaut, Fassung, Zeit) schlechter wird als heute.
## 3. Entscheidungen des Inhabers
| Nr. | Entscheidung |
|---|---|
| E1 | Bei den Häkchen, besonders beim Buchen einer Funktion, steht viel zu viel Text. Er soll auf das rechtlich Nötige schrumpfen. |
| E2 | Die Kunden sind fast nur Betriebe. Privat kommt kaum vor. |
| E3 | Die **Kundenart** (Betrieb/privat) wählt das Team **beim Anlegen des Leads** im Ticketsystem. Das Portal fragt den Kunden nicht. |
| E4 | Ist die Kundenart leer, gilt der Kunde als **privat** und sieht das eine Häkchen. |
| E5 | Aussehen weiß wie das Ticketsystem, Schwarz als Akzent. Das Lila fällt weg. |
| E6 | Die **Preise der Funktionen fallen weg**. Die Funktionen stecken im Abo: Das **Hosting-Abo** enthält die **Analyse**. Das **Wartungs-Abo** enthält **alle anderen** Funktionen (News, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten) und als Obermenge auch die Analyse [Claude, bestätigt]. |
| E7 | Die Einrichtung einer Funktion geht als Arbeitszeit vom **Stundenkonto** des Abos ab. |
| E8 | Das Einschalten löst **nie automatisch Mehrkosten** aus. Reicht das Guthaben nicht, meldet sich WEBklar vorher und klärt es mit dem Kunden [Claude, bestätigt]. |
| E9 | Will ein Kunde eine Funktion **schon beim Projektstart**, baut WEBklar sie gleich ein und rechnet sie über die **Projektkosten** ab, nicht über das Stundenkonto. |
| E10 | Einzeln gekaufte Funktionen laufen weiter wie bisher [Claude, bestätigt]. |
| E11 | Handy-Leiste **Variante 2 „Fahrstuhl-Zeile“** (schwarzer Balken, siehe 5.2). |
| E12 | Neue Gruppierung: Website · Funktionen · Chat · Verträge. Rechnungen werden ein Reiter unter Verträge. Konto liegt am Profilbild oben rechts. Desktop wie im Ticketsystem mit Menü links. Die Erklärsätze unter den Überschriften fallen weg [Claude, bestätigt]. |
| E13 | „Entdecken“ zeigt je Funktion eine kurze Beschreibung, ein Bild und den Knopf **„Einschalten“**. |
| E14 | Tippt ein Kunde mit Hosting-Abo bei einer Wartungs-Funktion auf „Einschalten“, landet er direkt beim Wechsel ins Wartungs-Abo. Danach schaltet sich die Funktion ein. |
| E15 | Anrede überall **„du“**, auch bei Verträgen, Abo und Übernahmegutschrift. |
| E16 | Erst eine **Vorschau mit Testdaten** wie beim Ticketsystem, danach Livegang in Paketen. |
| E17 | Reicht das Stundenkonto für eine Einrichtung nicht, gibt es einen **Vorschuss**. Die Einrichtung geht voll ab, das Konto darf dafür ins Minus und wird mit den nächsten Monatsgutschriften ausgeglichen. Eine Extra-Rechnung gibt es nicht. Steht das Konto bei Vertragsende noch im Minus, trägt WEBklar den Rest. Abgezogen wird die **echte Arbeitszeit** [Claude, die Teilfrage blieb unbeantwortet]. E17 ersetzt die Absprache aus E8. |
| E18 | Die Kundenart **aller 42 Bestandseinträge** wird auf **Betrieb** (`unternehmen`) gesetzt. Das sind 6 Kunden, 31 Leads und 5 verlorene, Stand 15.10. Das passiert als Schritt vor dem Umschalten. |
| E19 | Die neue Ansicht ist zuerst **nur für das Team** sichtbar (Pilot-Schalter). Danach schalten alle Kunden auf einmal um. |
| E20 | Rechtstexte gehen nach der **Freigabe durch den Inhaber** live. Kanzlei-Fragen werden gesammelt, halten den Livegang aber nicht auf. |
| E21 | Eine beim Projektstart eingebaute Funktion (E9) läuft **nur mit dem passenden Abo**, wie alle anderen auch. Die Einrichtung läuft über die Projektkosten. |
## 4. Rahmen und Randbedingungen
**Code und Laufzeit**
- Code: `knso/Webklar-Kundenbereich`, Express + Vanilla JS in `public/`, Node 20 (prod), Hash-Router, eine Seite mit Ansichten. Kein Framework einführen.
- Deploy: Ein Push auf Gitea `main` startet den Webhook. `build/` wird nie direkt bearbeitet.
- Der Push auf `main` ist im Auto-Modus für Claude gesperrt. Der Inhaber pusht, außer er gibt im Einzelfall frei.
- Server-Speicher: Node-Tests immer mit `--max-old-space-size=1024`, mit Timeout und unter `systemd-run --scope -p MemoryMax=…`. Am 04.10. haben Testläufe den Server per OOM abgeschossen.
- Daten: Appwrite `woms-database`. Die Kundenart gibt es schon als Attribut `customers.kundenart` (`'' | 'unternehmen' | 'privat'`, `server/services/auftraege/schema.js:53`). Das Ticketsystem bearbeitet sie heute auf der Kunden-Detailseite (`src/lib/kundenFelder.js`, `CustomerDetailPage.jsx:257`), nicht im Dialog „Neuer Lead“ (`CustomersPage.jsx:162`).
**Bestand, der weiterlaufen muss**
- Die Kundenansicht der Mitarbeiter (Ansichtsmodus aus WOMS) sperrt alle Bestell- und Bestätigungswege.
- PWA mit Offline-Seite, iOS-Statusleiste `black-translucent` → wird für Weiß angepasst [Bauer], Android `interactive-widget`, Chat-Composer über `visualViewport`.
- Funktionsflags: `TERMINE_ENABLED`, `SHOP_ENABLED`, `UNTERKUNFT_ENABLED`, `ZEITEN_ENABLED` (off/pilot/on), `ANALYSE_ENABLED`, `AUFTRAEGE_ENABLED`. Eine Funktion, die per Flag aus ist, erscheint auch unter „Entdecken“ nicht. Das gilt wie heute.
**Rechtlich fest, unabhängig vom Design** (aus `recht.md` § 5)
- Bei jeder kostenpflichtigen Bestellung stehen Kerninfo und Knopf auf demselben Bildschirm.
- Der Knopf heißt wörtlich „Zahlungspflichtig bestellen“ (mit Betrag, wo es ihn heute gibt). Keine Geste und kein Icon statt Text.
- „Verträge hier kündigen“ ist immer erreichbar, auch ausgeloggt. `kuendigen.html` bleibt schlicht: keine Halteangebote, kein „Bist du sicher?“.
- „Vertrag widerrufen“ ist hervorgehoben, solange eine Frist läuft. `widerrufen.html` bleibt.
- Impressum und Datenschutz bleiben im Konto erreichbar.
- Belehrung, Vertragstexte und PDFs gehen weiter nach jeder Bestellung per Mail raus (§ 312f, § 312i Abs. 1 Nr. 4).
**Nachweise.** `featureConsents` speichert weiter den kompletten Wortlaut dessen, was auf dem Bildschirm stand: Linkzeile, Häkchen falls vorhanden, Knopftext, Fassungen, Hash, Zeit, IP, Browser, Konto. Wo Abo, Wechsel und Zusatzspeicher heute nur die Version speichern, darf das so bleiben. Besser ist es, auf denselben Wortlaut-Nachweis umzustellen [Bauer].
**Rechtliches hochladen.** Neue Fassungen von Rechtstexten lädt Claude selbst in WOMS „Rechtliches“ hoch. Dafür nutzt Claude den kurzlebigen Storage-Key `/root/.appwrite-rechtliches.key`, den der Inhaber bereitstellt.
## 5. Was gebaut wird
### 5.1 Aussehen
Die Tokens und Regeln des Ticketsystems werden übernommen. Quelle ist `woms-design.md` bzw. `ticket.webklar.com` `src/styles/global.css:7-51`:
- Weiß `#fff`, Text `#1a1f36`, Zweittext `#5e6678`, Tertiär `#8a93a3` nur für Nebensächliches.
- Haarlinien `#ebeef1` / `#d8dde3`, Hover `#f6f8fa`, Fläche `#f8f9fb`.
- Akzent Schwarz `#111`, höchstens ein schwarzer Knopf pro Seitenkopf.
- Status-Pillen getönt und ohne Rand. Systemschrift, Gewicht 800 entfällt, Zahlen `tabular-nums`.
- Radien 6 und 12 px, Ring-Schatten statt Rahmen bei Feldern und Zweitknöpfen.
- Keine Karten und Kästen, keine Verläufe, kein Glow, kein Punktraster, kein Glas als Deko.
- Dialoge schließen nur per × und Esc und erscheinen am Handy als Bottom-Sheet.
- Ladezustände als Text („Wird geladen …“).
Die Ersetzungsregeln des Ticketsystems gelten auch hier:
- Karte → Abschnitt
- Kachelraster → Liste
- farbiger Streifen → Badge
- Icon-Knopf → Textknopf
Das gilt für **alle** Kundenansichten, die Login-Seite, `kuendigen.html`, `widerrufen.html`, `abo-bedingungen.html`/Rechtliches-Seiten, `offline.html` und die Funktionsmodule (Termine, Webshop, Buchungskalender, Zeiten, News, Analyse) [Claude: Login auch weiß]. `auftrag.html` ist heute schon hell und wird angeglichen.
Die CSS-Dateien sind heute über viele Dateien verstreut und haben 24 Schriftgrößen und 15 Radien. Daraus wird ein gemeinsamer Satz Tokens mit Komponenten. Wie die Dateien geschnitten werden, entscheidet der Bauer.
### 5.2 Navigation
**Handy (bis 880 px, wie heute `appLayout`), Variante „Fahrstuhl-Zeile“.** Referenz-Demo: `handy-leiste/final-2-frei.html`.
- **Leiste unten:**
- Vier Wörter ohne Icons: **Website · Funktionen · Chat · Verträge**, 15 px/600, Zweittext-Farbe.
- Ein schwarzer Balken (Radius 6, 36 px hoch) gleitet federnd unter das aktive Wort. Das Wort darauf ist weiß. Technik der Demo: Overlay-Kopie der Wörter im Balken mit Gegenbewegung; Fallback ohne Overlay, wenn die Breiten nicht passen.
- Tap-Ziele mindestens 44 px. Safe-Area unten.
- Reicht die Breite nicht, schaltet die Leiste auf 14 px um (Prüfung per Messung, wie in der Demo).
- **Zähler:** Rote Zahl hochgestellt am Wort: Chat für ungelesene Nachrichten, Verträge für offene Rechnungen plus Rückfragen. Ein „!“ für eine Rückfrage hat Vorrang [Bauer]. Der Zähler federt beim Erscheinen und verschwindet beim Öffnen. `aria-label` nennt die Zahl.
- **Unterseite („Etage 2“):** Öffnet sich eine Funktion (z. B. Terminplaner) oder eine Detailseite, fährt die Zeile eine Etage hoch.
- Dort steht **„Zurück“** und daneben die **Reiter der Unterseite**, mit derselben Balken-Mechanik.
- Die Seite selbst schiebt sich als Blatt von unten über die Hauptseite.
- Ein Punkt am „Zurück“ zeigt Neues im Chat.
- Hat die Unterseite keine Reiter, füllt „Zurück“ die Zeile.
- Hinweis der Prüfer: Das Muster „unten Ebene, oben Ansicht“ darf nicht brechen. Unterseiten-Reiter stehen **immer unten in Etage 2**. Reiter einer Hauptseite (Meine/Entdecken, Rechnungen/Abo/…) stehen **immer oben** als Unterstrich-Reiter. Wer Etage 2 für alle Reiter einheitlich lösen will, entscheidet das mit Begründung [Bauer].
- **Kopf:** Die große Überschrift (28/34, 700) klappt beim Scrollen ein und erscheint dann klein in der Kopfzeile. Rechts sitzt das Profilbild (DiceBear wie heute, 44-px-Tap-Fläche).
- **Konto-Sheet** über das Profilbild, Reihenfolge:
- Name und E-Mail
- Passwort ändern
- Datenschutz
- Impressum
- App installieren (nur wenn sinnvoll, wie heute)
- **Verträge kündigen**
- **Vertrag widerrufen**: immer da, wie heute. Während einer laufenden Frist hervorgehoben mit der Pille „Frist läuft“ (C3).
- Abmelden
- **Widerrufsstreifen:** Läuft eine Widerrufsfrist, steht über der Leiste ein schmaler Streifen „Widerrufsfrist bis TT.MM. · Widerrufen“. Er ist nicht wegtippbar, auf allen Hauptseiten sichtbar und blendet sich bei offener Tastatur aus. Er erscheint nur bei Kundenart privat oder leer und für jede Frist, die der Server kennt (C3).
- **Tastatur offen:** Leiste und Streifen fahren weg, der Chat-Composer sitzt über der Tastatur. Die heutige `visualViewport`-Logik bleibt.
- **Zurück-Geste:** Reiterwechsel mit `replaceState`, Etage 2, Sheet und Seiten mit `pushState`. Zurück schließt der Reihe nach Sheet, Tastatur, Etage 2 und geht dann zu Website. Das verhält sich wie heute (`APP_PARENT`), nur mit der neuen Mechanik.
- **Querformat:** Die Leiste wird niedriger und die große Überschrift entfällt [Bauer: Maße].
- **Bewegung reduziert:** Keine Federn, nur kurzes Überblenden.
- **Wow-Moment** (zum Abnehmen): Der Balken zieht federnd mit Überschwingen, die Schrift kippt dabei von grau auf weiß, und die Unterseite fährt als Blatt mit der Leiste eine Etage hoch.
**Desktop (über 880 px)**
- Menü links wie im Ticketsystem (232 px, Haarlinie rechts, Wörter ohne Icons, aktiver Eintrag schwarz) mit denselben vier Bereichen und Zählern.
- Unterseiten zeigen eine Brotkrume („Funktionen / Terminplaner“) statt Etage 2. Oben rechts sitzt das Profilbild mit dem Konto als Popover.
- Inhalt maximal ~1160 px, Abstände wie `.page` im Ticketsystem.
### 5.3 Gruppierung der Bereiche
| Bereich | Inhalt | Reiter |
|---|---|---|
| **Website** (heute „Projekte“) | Live-Vorschau, Status, Projektdetails, letzte Änderungen, Arbeiten am Projekt, Dateien. Bei mehreren Projekten eine Liste, Detail als Etage 2. | – |
| **Funktionen** | siehe 5.4 | Meine · Entdecken |
| **Chat** | Verlauf und Composer (Bild, Kamera, Datei, Sprachnachricht wie heute) | – |
| **Verträge** | Rechnungen (Liste, Zahlen/PDF, Aufträge falls an), Abo (Mein Vertrag, Laufzeit, Wechsel, Bedingungen), Stunden (Stundenkonto + Verlauf), E-Mail (Zusatzspeicher). Darunter fest: „Verträge hier kündigen“. Ohne Abo: Abo-Auswahl Hosting/Wartung. | Rechnungen · Abo · Stunden · E-Mail |
| **Konto** (Profilbild) | siehe 5.2 | – |
Weitere Regeln:
- **Sprung zum offenen Posten:** Ist eine Rechnung offen, öffnet Verträge auf dem Reiter Rechnungen. Website und Funktionen zeigen oben eine ruhige Zeile „Rechnung offen · RE-… · 50,00 € · Zahlen“ [Claude, aus Entwurf „Das gesetzte Heft“].
- **Übernahmegutschrift:** Sie wird ein Eintrag unter Verträge → Abo, wie heute über die Verträge erreichbar [Bauer: genaue Stelle].
- **Hashes:** Alle bisherigen Hashes bleiben gültig und führen an die passende neue Stelle. Zum Beispiel öffnet `#rechnungen` Verträge → Rechnungen, `#stunden` Verträge → Stunden, `#gutschrift` die Übernahmegutschrift. Neue Hashes für die Reiter entscheidet der Bauer.
- **PWA-Shortcuts:** Sie werden angepasst (Chat, Rechnungen, Verträge).
### 5.4 Funktionen: „Meine“ und „Entdecken“
**Meine** zeigt alles, was für eine Website des Kunden nicht `off` ist, also gebucht, in Einrichtung, aktiv, abgeschaltet oder angefragt. Dazu kommen begonnene Entwürfe (Terminplaner-Fragebogen fortsetzen).
- Darstellung als Liste mit Haarlinien: Name, eine Statuszeile, Status-Pille.
- Ein Tap öffnet die Funktion als Etage 2 mit ihren eigenen Reitern. Die Reiter der Funktionen bleiben inhaltlich, bekommen aber die gemeinsame Balken- bzw. Unterstrich-Mechanik statt der vier eigenen Kopien.
- Ist „Meine“ leer, öffnet Funktionen direkt auf „Entdecken“.
- Der Zustand „angefragt“ (Webshop, Buchungskalender) wird sichtbar. Heute fehlt er auf der Kachel, `anfrageStatus` steht schon in den Daten.
**Entdecken** zeigt alle verfügbaren Funktionen, die noch nicht unter „Meine“ stehen, je Funktion:
- **ein Bild:** echte Aufnahme der Funktion auf einer Beispiel-Website, mit Testdaten erstellt [Claude]. Format und Größe entscheidet der Bauer; ruhig und ohne Rahmen.
- **eine kurze Beschreibung:** höchstens zwei Sätze.
- **eine Zeile, was dabei ist:** „Im Wartungs-Abo enthalten“ bzw. „Im Hosting-Abo enthalten“.
- **den Knopf „Einschalten“:**
- Mit passendem Abo: Der Einschalt-Ablauf startet (5.5).
- Hosting-Abo, aber Wartungs-Funktion: direkt zur Wechselseite ins Wartungs-Abo. Nach dem Wechsel schaltet sich die Funktion ein. Dafür merkt sich der Wechsel die gewünschte Funktion und startet ihren Einschalt-Ablauf [Bauer: Technik].
- Kein Abo: zur Abo-Auswahl, mit vorausgewähltem passendem Abo [Claude].
- **Keine Preise** für Funktionen, nirgends in der Kundenansicht.
### 5.5 Funktionen im Abo: Einschalten
**Ablauf** für einen Kunden mit passendem Abo:
1. Er tippt in „Entdecken“ oder auf der Funktionsseite auf **„Einschalten“**.
2. Bei Funktionen mit Fragebogen oder Anfrage kommt erst dieser Schritt, wie heute: Terminplaner-Fragebogen, Webshop- und Buchungskalender-Anfrage mit Freigabe durch das Team.
3. Eine kurze Bestätigungsseite (oder ein Sheet, weil nichts kostet [Bauer]) zeigt:
- Leistung: „Einrichtung durch WEBklar“
- Dauer: z. B. „meist in 1–2 Wochen“
- „Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht. Extrakosten entstehen keine.“ (E17; bei der Analyse entfällt der Satz, siehe C6)
- Laufzeit: „läuft, solange dein Wartungs-Abo läuft“
- Eine Linkzeile: „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung …).“
- Den Knopf **„Einschalten“**.
4. Danach entsteht wie heute ein Einrichtungs-Ticket in WOMS. Der Zustand geht auf `setup`, und `runActivation` schaltet auf `active`, wenn das Ticket geschlossen ist. **Neu:** Arbeitsblätter auf diesem Ticket stehen standardmäßig auf `countsToPlan=true` und laufen damit aufs Stundenkonto (`hourLedger.js`). Heute ist der Schalter „+ Abo“ manuell und standardmäßig aus, und die Ticket-Texte (`termine/ticketDetails.js:38`, `shop/ticketDetails.js:57`, `unterkunft/ticketDetails.js:49`) sagen „nicht anrechnen“. Beides wird umgestellt (C6).
5. Kein Stripe und kein „Zahlungspflichtig bestellen“. Begründung: Es entsteht keine neue Zahlungspflicht, die Leistung ist Teil des bestehenden Abos. Betriebe setzen kein Häkchen. **Ausnahme Privatkunde (oder leere Kundenart) mit laufender Widerrufsfrist des Abos:** Er sieht das eine Häkchen „Bitte beginnt sofort …“. Ohne Häkchen kann er erst nach Fristende einschalten, und der Knopf nennt das Datum (C4).
Weitere Regeln:
- **Stundenkonto zu knapp (E17, Vorschuss):** Das Einschalten wird immer angenommen, und die Einrichtung startet ohne Rückfrage. `hourLedger.js` rechnet heute so: Was kein Guthaben findet, ist Mehraufwand und wird separat abgerechnet. Für Arbeitsblätter auf Einrichtungs-Tickets gilt künftig eine andere Regel:
- Das Minus ist ein **Vorschuss**, den künftige Gutschriften zuerst ausgleichen (FIFO auch rückwirkend).
- Er wird **nie** als Mehraufwand ausgewiesen.
- Ein Restminus bei Vertragsende verfällt zugunsten des Kunden.
- Normale Arbeit außerhalb von Einrichtungen bleibt beim heutigen Mehraufwand.
- Das Stundenkonto im Portal zeigt den Vorschuss verständlich, etwa „Vorschuss für Einrichtung: 2,5 Std., wird mit den nächsten Monaten verrechnet“. WOMS zeigt ihn im Ticket und beim Kunden.
- Wie Ledger, Anzeige und Kennzeichnung der Einrichtungs-Blätter umgesetzt werden, entscheidet der Bauer.
- **Gleich beim Projektstart (E9, E21):** Das Team schaltet die Funktion in WOMS ohne Kundenklick ein (das gibt es heute schon: Admin-Aktivierung ohne Zahlung). Die Einrichtungs-Arbeitsblätter zählen dann **nicht** aufs Stundenkonto (`countsToPlan=false`), weil sie über die Projektkosten laufen (C7). Auch diese Funktion läuft nur mit dem passenden Abo und endet mit ihm (C12). Die Admin-Aktivierung prüft deshalb, ob ein passendes Abo besteht, oder warnt zumindest [Bauer].
- **Abo-Ende:** Endet das Abo, das die Funktion enthält, schaltet die Funktion ab. Es gelten die bisherigen Lösch- und Behaltefristen (`disabledReason: abo_ende`) [Claude]. Den kostenpflichtigen „Behalten“-Weg der Öffnungszeiten (30 €) gibt es nicht mehr.
- **Bestand (E10):** Einzeln gekaufte Funktionen bleiben unverändert aktiv, auch ohne passendes Abo. Ihr Nachweis und ihre Rechnungen bleiben. Neue Einzelkäufe gibt es nicht mehr.
- **Analyse:**
- Sie ist im Hosting- und im Wartungs-Abo enthalten.
- Sie bleibt hinter `ANALYSE_ENABLED`, bis ihr eigener Livegang (L5) erledigt ist.
- Ihre Aktivierung bekommt dasselbe schlanke Muster. Die Aussage „Meine Website richtet sich nicht gezielt an Kinder unter 16“ bleibt als eine kurze Zeile mit Häkchen, weil sie eine Tatsachenangabe für die Einwilligungslogik ist, kein Rechtstext [Claude; Kanzlei-Frage].
### 5.6 Bestätigungen schlank
Für alles, was **kostet** (Abo bestellen, Wechsel, Zusatzspeicher erhöhen), gilt dasselbe Muster. Vorbild ist die Buchungsseite im Mockup dieser Sitzung.
1. **Block „Was du bestellst“** direkt über dem Knopf: Leistung, Preis (mit „keine USt, § 19 UStG“), Abrechnung bzw. heute fällig, Beginn, Mindestlaufzeit und Kündigung, Zahlungsweg.
2. **Eine Linkzeile:** „Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung …). Alles kommt danach auch als PDF per Mail.“ Datenschutz als Link.
3. **Nur für Privatkunden (oder leere Kundenart, E4)** ein Häkchen, nicht vorausgewählt: „Bitte beginnt sofort. Mir ist klar: … erlischt mein Widerrufsrecht / zahle ich den bis dahin erbrachten Teil.“ Den genauen Wortlaut je Fall legt die Rechtsrunde fest. Abo: Wertersatz; Einrichtung: Erlöschen + Wertersatz. Ohne Häkchen:
- Abo: Beginn nach 14 Tagen. Das ist der heutige Weg `nach_frist`.
- Wechsel: nicht möglich ohne Häkchen, weil er sofort wirkt. Der Knopf bleibt dann gesperrt, mit einer kurzen Zeile dazu.
4. **Knopf** „Zahlungspflichtig bestellen“, ggf. mit Betrag.
5. **Unter dem Knopf:** „Du bestellst als Betrieb“ bzw. „… privat“, und „Stimmt nicht? Schreib uns im Chat“ [Claude]. Ändern kann es nur das Team.
**Zuordnung der heutigen Häkchen** (Datei:Zeile in `bestaetigungen.md`):
| Heute | Neu |
|---|---|
| A1 Abo „gelesen und stimme zu … Belehrung zur Kenntnis“ | Linkzeile |
| A2 Abo Radio Sofort / nach Frist | Betrieb: Beginn sofort ohne Auswahl. Privat: das eine Häkchen; ohne Häkchen nach Frist |
| B1 Wechsel AGB/AVV | Linkzeile |
| B2 Wechsel Sofortbeginn (heute Pflicht für alle) | nur noch Privat |
| C1 Zusatzspeicher Regeln + Belehrung | Linkzeile, kein Häkchen (WEBklar verzichtet laut § 10 auf Wertersatz) |
| C3 Warnbox beim Verkleinern | bleibt als eine Zeile |
| D2 Übernahmegutschrift „Ich versichere …“ | Satz über dem Knopf „Mit dem Absenden bestätigst du, dass die Angaben stimmen …“ + Datenschutz-Link; Bedingungen als Link statt Klappe [Claude] |
| F Funktionen `unterlagen`, `information`, `beginn`, `leistung`, `stripe`, `verantwortung` | entfallen mit dem Einschalt-Ablauf (5.5). Pflichten wie „Ich verkaufe in eigenem Namen und bin für Rechtstexte verantwortlich“ (Webshop) und „Zahlung über mein Stripe“ wandern in die Leistungsbeschreibung, dazu ein Satz auf der Einschalt-Seite |
| F `gesundheit` (Terminplaner) | wird zur Branchen-Frage im Fragebogen. Gesundheitsbereich → Terminplaner nicht verfügbar, mit freundlichem Hinweis |
| F `rechnung` (Buchungskalender) + G5 Einstellungen | **bleibt als einziges Fach-Häkchen**, nur noch einmal: in den Einstellungen (G5), weil es den Betrieb freischaltet. Der Bestell-Haken entfällt |
| G2 Webshop-Anfrage „Ich verkaufe als Unternehmer“ | entfällt (Kundenart). Privat → Webshop nicht verfügbar [Claude] |
| G2 „keine ausgeschlossenen Waren“ | Satz über „Anfrage senden“ mit der Liste. Das Team prüft bei der Freigabe [Claude] |
| G3 Webshop „Texte bestätigen“ | bleibt (Betriebsfreigabe, kein Vertrag), Text auf einen Satz gekürzt |
| G6 Öffnungszeiten WhatsApp-Hinweis | bleibt, gekürzt |
| G1 Terminplaner-Fragebogen „Weiter“ + „Preise geprüft“ | bleibt (Einrichtung, kein Rechtstext), Texte gekürzt |
| H Analyse AVV + Zielgruppe | Linkzeile + kurze Zielgruppen-Zeile mit Häkchen (5.5) |
| I Auftragsbestätigung | bleibt wie gebaut (Häkchen als Beweis + Frühbeginn nur privat), nur im neuen Aussehen |
| E1/E2 Kündigen/Widerrufen | unverändert im Ablauf, nur im neuen Aussehen und mit „du“ |
**Belehrung.** Der Volltext der Widerrufsbelehrung steht nicht mehr als Scrollfeld auf der Seite. Er ist per Link erreichbar und liegt als PDF in der Bestätigungsmail (Kanzlei-Frage 4 in `recht.md`).
### 5.7 Sprache und Text
- **Anrede:** Überall „du“, in der Kundenansicht und auf den öffentlichen Formularseiten (Kündigen, Widerrufen, Login) [Claude: auch öffentliche Seiten].
- Die Rechtsdokumente (Abo-Bedingungen, Leistungsbeschreibungen, AVV, Datenschutz) bleiben im bisherigen Stil, außer die Rechtsrunde stellt sie um.
- Mails mit „Sie“ bleiben, bis zu einer eigenen Mail-Runde [Claude].
- **Erklärsätze und Hinweise:** Die Erklärsätze unter den Überschriften fallen weg. Hinweise erscheinen nur, wo der Kunde etwas tun muss oder etwas schiefging.
- **Tonfall:** kurze Sätze, keine Paragrafen in der Oberfläche, Knöpfe mit Verb zuerst.
### 5.8 Ticketsystem (WOMS)
- **Dialog „Neuer Lead“** (`CustomersPage.jsx:162`) bekommt das Pflichtfeld **Kundenart** (Betrieb/privat) mit `KUNDENART_OPTIONEN` aus `kundenFelder.js`. Gibt es einen Dialog „Neuer Kunde“, gilt dort dasselbe [Claude].
- **Kunden-Detailseite:** Sie zeigt die Kundenart weiter editierbar.
- **Funktionen-Bereich in WOMS:**
- Er zeigt keine Preise mehr für Funktionen.
- Die Admin-Aktivierung kann „im Projekt enthalten“ (zählt nicht aufs Stundenkonto, E9) von „aus dem Abo“ unterscheiden [Bauer: Form].
- Die Textform-Zustimmung per Chat, E-Mail oder WhatsApp für die Team-Aktivierung bleibt für die AVV-Fälle, wird aber an das Abo-Modell angepasst [Bauer, Rechtsrunde].
- **Einrichtungs-Ticket:** Es zeigt den Kontostand des Kunden und, sobald es entsteht, den Vorschuss (E17). Neue Arbeitsblätter stehen dort standardmäßig auf „aufs Abo anrechnen“ (C6) [Bauer: Form].
- **Stil:** Es gilt das weiße Design des Ticketsystems.
### 5.9 Rechtsrunde
Diese Texte ändern sich und brauchen neue Fassungen:
- **Abo-Bedingungen Fassung 6:**
- Funktionen sind enthalten (Hosting: Analyse; Wartung: alle).
- Einrichtung zählt aufs Stundenkonto, Mehraufwand nur nach Absprache.
- Funktionen enden mit dem Abo.
- Bestand bleibt.
- Dazu ein Übergang für laufende Abos der Fassung 5: Die Änderung ist für Kunden nur vorteilhaft. Ob sie informiert werden müssen, klärt die Rechtsrunde.
- **Leistungsbeschreibungen je Funktion:** Preise raus, Pflichten aus den alten Häkchen rein.
- **AVV je Funktion:** Prüfen, ob sie Anlage zu den Abo-Bedingungen werden.
- **Datenschutz:** Prüfen, ob die Stellen zu Zahlung/Stripe bei Funktionen und zu gespeicherten Zustimmungen noch stimmen.
- **Widerrufsbelehrung Funktionen:** wird für das Einschalten nicht mehr gebraucht, bleibt für Bestand.
Ablauf wie bei früheren Rechtsrunden:
1. Claude entwirft.
2. Der Inhaber entscheidet die Punkte.
3. Die Kanzlei-Fragen werden gesammelt (`recht.md` § 4 plus neue).
4. Claude lädt die Fassungen in WOMS „Rechtliches“ hoch.
Die rechtsabhängigen Teile (5.5, 5.6, Preise weg) gehen erst nach der Rechtsrunde live, also sobald der Inhaber die Entwürfe freigegeben hat (E20). Auf die Kanzlei wird nicht gewartet, ihre Fragen bleiben gesammelt.
Neue Punkte für Fassung 6:
- die Vorschuss-Regel des Stundenkontos (E17)
- „Funktionen laufen nur mit passendem Abo“, auch beim Projektstart (E21)
- Analyse im Hosting-Abo (C10)
### 5.10 Vorschau und Livegang
- **Vorschau mit Testdaten** wie beim Ticketsystem:
- Eine Vorschau-Instanz der Kundenansicht mit festem Testdatensatz und den Szenarien, umschaltbar ohne echte Kundendaten: Betrieb mit Wartung, Hosting-Kunde, ohne Abo, privat in Widerrufsfrist, offene Rechnung, Chat ungelesen, Funktionen in allen Zuständen.
- Erreichbar unter einer `*.project.webklar.com`-Adresse hinter dem Portal-Login.
- Vorlage ist die Testdaten-Vorschau des Ticketsystems (Service Worker mit Fake-Backend, Umschalter „Daten/Fall“) oder der statische Fixture-Server aus `kundenportal-testing`. Welches, entscheidet der Bauer.
- Keine echten Mails, kein echtes Stripe, keine Prod-Daten.
- Deploy der Vorschau nur per explizitem `cp`, ohne `--delete` und ohne Ziel aus Parametern ohne Guard (Vorfall 05.10.).
- **Pilot-Schalter (E19):** Die neue Ansicht liegt hinter einem eigenen Schalter `off | pilot | on`, nach dem Muster von `ZEITEN_ENABLED` mit Pilot-E-Mail-Liste. Name und Technik entscheidet der Bauer.
- Bei `pilot` sehen nur das Team, die Pilot-Adressen und die Mitarbeiter-Kundenansicht die neue Oberfläche. Alle anderen Kunden behalten die alte, inklusive heutiger Preise und Bestellseiten.
- Kein gemischter Zwischenstand: Ein Kunde sieht entweder ganz alt oder ganz neu.
- **Pakete** sind interne Bauabschnitte. Sie werden einzeln gebaut, geprüft und in den Pilot gepusht, nicht einzeln für Kunden freigeschaltet. Den Schnitt macht der Bauer, z. B.:
1. Fundament mit Tokens, Komponenten, Kopf, Leiste und Menü
2. Website, Chat, Verträge
3. Funktionen-Rahmen mit Meine/Entdecken
4. die Funktionsmodule
5. Abo-Modell und schlanke Bestätigungen
6. WOMS-Änderungen inklusive Kundenart-Pflichtfeld
- **Umschalten für alle:**
1. Alle Pakete sind fertig, und der Inhaber hat Vorschau, Pilot und Fassung 6 freigegeben.
2. Vorher: Kundenart aller Bestandseinträge auf Betrieb setzen (E18), Fassung 6 hochladen, WOMS-Pflichtfeld live.
3. Dann `on` für alle.
4. Danach räumt ein eigenes Paket die alte Oberfläche weg.
- **Einzelkäufe bis zum Umschalten:** Sie laufen in der alten Oberfläche weiter wie heute [Claude, die Teilfrage blieb unbeantwortet]. Wer kurz vorher eine Funktion kauft, gehört zum Bestand (E10).
- **Vor jedem Paket** gibt es eine Screenshot-Prüfung auf Handy, Tablet und Desktop, wie bei P1–P9 im Ticketsystem.
## 6. Dem Bauer überlassen
- Schnitt der CSS/JS-Dateien, Komponenten, Namen, neue Hash-Namen für Reiter (alte bleiben gültig).
- Exakte Maße, Kurven und Dauern im Rahmen der Demo `final-2-frei.html` und der Ticketsystem-Tokens.
- Technik der Vorschau (Fake-Backend), Testwerkzeug, Gates.
- Wie „Stundenkonto reicht nicht“ ermittelt und angezeigt wird (E8).
- Wie WOMS „im Projekt enthalten“ markiert (E9).
- Ob Abo, Wechsel und Zusatzspeicher auf den Wortlaut-Nachweis wie `featureConsents` umgestellt werden (empfohlen).
- Paketschnitt und Reihenfolge, solange die rechtsabhängigen Teile nach der Rechtsrunde kommen.
- Die Bilder für „Entdecken“ (Aufnahmen mit Testdaten), Format.
## 7. Festlegungen nach dem Builder-Check [Claude]
Drei Prüfer haben das Dokument gelesen (Oberfläche, Server/Abo, WOMS/Recht). Die Fragen liegen in `/root/reports/kundenansicht-neu-2026-10-15/builder-check.json`. Was sich ohne den Inhaber vernünftig entscheiden ließ, steht hier:
- **C1 Mehrere Websites.** Funktionen gelten je Website (`portalFeatures`).
- Hat ein Kunde mehr als eine Website, zeigt „Meine“ eine Zeile je Funktion und Website, mit dem Websitenamen als zweiter Zeile.
- „Entdecken“ zeigt eine Funktion, solange mindestens eine Website sie noch nicht hat.
- „Einschalten“ fragt dann zuerst nach der Website. Bei nur einer Website entfällt das.
- Die drei heutigen Übergabewege (`selection`, `analyseSel`, `dataset.wunschProjekt`) werden zu einem.
- **C2 Viele Reiter in Etage 2.** Die Reiterzeilen von Terminplaner, Webshop und Buchungskalender bekommen Kurzlabels, z. B. „Team“ statt „Team & Plätze“ und „Preise“ statt „Unterkünfte & Preise“. Passt die Zeile trotzdem nicht, scrollt sie waagerecht, der Balken wandert mit, und der aktive Reiter wird in den sichtbaren Bereich gerückt. Die Overlay-Technik braucht dafür einen Rückfall, der beim Scrollen trägt. Die Labels legt der Bauer fest, die Vorschau prüft sie.
- **C3 Widerruf sichtbar.**
- Der Eintrag „Vertrag widerrufen“ im Konto bleibt wie heute **immer** da und wird während einer Frist hervorgehoben.
- Der Streifen erscheint nur bei Kundenart privat oder leer, und zwar für jede Frist, die der Server kennt: Abo, Wechsel, Speicher-Plus, Bestands-Funktionen, Projektaufträge falls an. Er nennt das früheste Fristende. Dafür liefert der Server eine Sammelangabe der Fristen.
- Die Verarbeitung eines Widerrufs (`withdrawOrder`, `widerrufen.html`) bleibt in der Logik unverändert.
- Nachweise speichern ab jetzt die Kundenart zum Zeitpunkt des Klicks.
- **C4 Privatkunde in der Widerrufsfrist schaltet eine Funktion ein.** Er braucht das eine Verlangen-Häkchen, ohne Häkchen erst nach Fristende (siehe 5.5 Schritt 5). Für Betriebe gibt es kein Häkchen, auch nicht beim Wechsel. Der Server speichert dann die Kundenart „unternehmen“ als Grund statt `earlyStartConsentAt`, und `planSwitch.js:351` wird entsprechend angepasst.
- **C5 Gemerkte Funktion beim Wechsel.** Die gewünschte Funktion wird am Wechsel-Vorgang gespeichert (serverseitig).
- Erst wenn der Wechsel wirksam ist (Zahlung bestätigt, wie heute `planSwitch`), öffnet das Portal den **Einschalt-Ablauf** der Funktion mit Fragebogen, Anfrage oder Bestätigung. Es schaltet die Funktion also nicht sofort frei.
- Scheitert der Wechsel, passiert nichts.
- Ein Widerruf des Wechsels beendet alle Funktionen, die nur im Wartungs-Abo stecken und über das Abo eingeschaltet wurden. Die Analyse bleibt, weil Hosting sie enthält.
- **C6 Stundenkonto im Ticket.**
- Einrichtungs-Tickets aus „Einschalten“ stehen standardmäßig auf „aufs Abo anrechnen“.
- Team-Aktivierungen „im Projekt enthalten“ (E9) stehen standardmäßig auf „nicht anrechnen“.
- Die **Analyse-Einrichtung bleibt für den Kunden gratis**, wie es ihre Leistungsbeschreibung heute sagt, und das Hosting-Abo hat ohnehin kein Stundenkonto. Ihre Einschalt-Seite nennt deshalb kein Stundenkonto.
- **C7 Projektkosten (E9).** Dafür gibt es keine neue Markierung. Arbeitsblätter mit „nicht anrechnen“ sind wie heute als Projektarbeit abrechenbar (Rechnungsdialog WOMS), nach Aufwand oder im Projektangebot enthalten, wie das Team es mit dem Kunden vereinbart.
- **C8 Eingefrorene Rechtstext-Fassungen** (`public/rechtliches/*-2026-10-0x.html` + PDFs mit sha256, `vertragsunterlagen/v3`, `v4`) bleiben unberührt, Text und Aussehen. Neue Fassungen erscheinen im neuen Stil. `abo-bedingungen.html` bekommt den neuen Stil mit Fassung 6.
- **C9 Entdecken und Schalter.** Eine Funktion, die per Flag aus ist, erscheint nicht. Teaser „Kommt bald“ fallen weg. Pilot-Funktionen sehen nur Pilot-Kunden.
- **C10 Analyse L5** ist unabhängig. Sie darf vorher mit den heutigen Texten live gehen (nur Wartungs-Abo). Fassung 6 und Paket 5 erweitern sie dann auf Hosting und passen ihre Texte an (Abo-Ergänzung Analyse, Eignungsprüfung `analyse.js:45,60`).
- **C11 Fassung 6 und Bestands-Abos.**
- Fassung 6 gilt für neue Abschlüsse.
- Laufende Abos (v3 bis v5) behalten ihren Vertrag.
- Beim Einschalten einer Funktion gelten deren Leistungsbeschreibung und AVV über die Linkzeile und den Klick. Der Nachweis kommt in `featureConsents`.
- Ob Bestandskunden zusätzlich eine Info-Mail bekommen, entscheidet die Rechtsrunde. Das ist eine neue Kanzlei-Frage.
- Der harte Verweis auf „-v5“ (`products.js:32,51`) wird zu einer Liste der Fassungen.
- **C12 Abo-Ende.**
- Über das Abo eingeschaltete Funktionen schalten zum Abo-Ende ab, wie heute der Terminplaner.
- Je Funktion gelten die bestehenden Fristen und Löschjobs. Webshop und Buchungskalender schließen für Käufer und Gäste, offene Bestellungen und Buchungen bleiben bearbeitbar, gelöscht wird nach den bestehenden 90 Tagen. News und Öffnungszeiten folgen ihrer 30-Tage-Frist.
- Ein neues passendes Abo innerhalb der Frist holt die Funktion zurück.
- Die Mails K02, K22, K29 und K30 werden angepasst. Den Satz zum 30-€-Behalten gibt es nicht mehr.
- **Einzeln gekaufte Funktionen behalten genau ihre bisherigen Regeln.** Der gekaufte Terminplaner endet also weiter mit dem Wartungs-Abo, wie sein Vertrag sagt. Die anderen gekauften Funktionen laufen weiter.
- **C13 Kundenart in WOMS.**
- Pflichtfeld in „Neuer Lead“, „Neuer Kunde“ (`customerAdmin.js:110`) und „Zum festen Kunden machen“.
- In der Kundenliste ein Filter „Kundenart fehlt“.
- Das Portal bekommt die Kundenart über `/api/auth/me`. Das Attribut `customers.kundenart` gibt es live (geprüft 15.10.: 42 Einträge, alle leer).
- **C14 Wortlaut der Bestellseiten.** Abo, Wechsel, Zusatzspeicher, Gutschrift, Kündigen und Widerrufen bekommen bis Paket 5 nur das neue Aussehen, nicht den neuen Text. Jede Textänderung dort zieht eine neue `termsVersion` nach (`config.js:853`). „Du“ und das schlanke Muster kommen dort mit Fassung 6.
- **C15 Webshop für Privatkunden** ist nicht verfügbar. Er bekommt einen freundlichen Hinweis mit Chat-Link.
## 8. Fragen an den Inhaber nach dem Builder-Check (beantwortet 16.10.)
| Frage | Antwort | Eingearbeitet |
|---|---|---|
| Q1 Stundenkonto reicht nicht | a) Vorschuss | E17, 5.5 |
| Q2 Kundenart Bestand | alle auf Betrieb | E18, 5.10 |
| Q3 Livegang | erst Team, dann alle auf einmal | E19, 5.10 |
| Q4 Rechtsrunde | live nach Freigabe durch den Inhaber | E20, 5.9 |
| Q5 Funktion beim Projektstart | nur mit Abo | E21, 5.5, C12 |
Zwei Teilfragen blieben offen. Ich habe sie so entschieden, dass sie leicht zu kippen sind: Es zählt die echte Arbeitszeit statt einer Pauschale (E17), und Einzelkäufe laufen bis zum Umschalten weiter (5.10).
+188 -2
View File
@@ -14,9 +14,11 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"feiertagejs": "1.5.1",
"fontkit": "^2.0.4",
"jose": "^6.0.11",
"node-appwrite": "^14.1.0",
"nodemailer": "^9.0.3",
"pdfkit": "^0.20.2",
"sharp": "^0.34.5",
"stripe": "^19.3.0"
},
@@ -571,6 +573,39 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -596,6 +631,26 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
@@ -620,6 +675,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -658,6 +722,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -744,6 +817,12 @@
"node": ">=8"
}
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -876,6 +955,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/feiertagejs": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/feiertagejs/-/feiertagejs-1.5.1.tgz",
@@ -885,6 +970,12 @@
"node": ">=8.0.0"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"license": "MIT"
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -903,6 +994,23 @@
"node": ">= 0.8"
}
},
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1059,6 +1167,25 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1191,6 +1318,12 @@
"node": ">= 0.8"
}
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1206,6 +1339,28 @@
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
"node_modules/pdfkit": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.20.2.tgz",
"integrity": "sha512-Q/w03ICAQyXfHNfTsg1udp0ADerdBN0s7a6XSPL7J7Ro6ABnafoBOCDBstIO9U02mvzHEV8HrvFIw5iV5ejIRA==",
"license": "MIT",
"dependencies": {
"@noble/ciphers": "^1.3.0",
"@noble/hashes": "^1.8.0",
"fflate": "^0.8.3",
"fontkit": "^2.0.4",
"linebreak": "^1.1.0",
"png-js": "^2.0.0"
}
},
"node_modules/png-js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz",
"integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==",
"dependencies": {
"fflate": "^0.8.2"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1258,6 +1413,12 @@
"node": ">= 0.8"
}
},
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1492,6 +1653,12 @@
}
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1505,8 +1672,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
"license": "0BSD"
},
"node_modules/type-is": {
"version": "1.6.18",
@@ -1521,6 +1687,26 @@
"node": ">= 0.6"
}
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+2
View File
@@ -21,9 +21,11 @@
"dotenv": "^16.4.7",
"express": "^4.21.2",
"feiertagejs": "1.5.1",
"fontkit": "^2.0.4",
"jose": "^6.0.11",
"node-appwrite": "^14.1.0",
"nodemailer": "^9.0.3",
"pdfkit": "^0.20.2",
"sharp": "^0.34.5",
"stripe": "^19.3.0"
}
+36 -12
View File
@@ -9,11 +9,18 @@ import { analyseOf, pickAnalyseProject, analyseSel, bannerColor, BANNER_PREVIEW_
import { headerHtml, tabsHtml, overviewHtml, rankCardHtml, skeletonHtml, errorHtml, settingsHtml, chartTipText } from './analyse-cards.js'
import { HEAT_DEVICES, deviceHtml, heatCaption } from './geraete.js'
import { ZU_VERTRAEGE } from './vertraege-link.js'
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
const PRESET_COLORS = ['#111111', '#1d4ed8', '#047857', '#b91c1c', '#7c3aed', '#c2410c']
const HEX_RE = /^#[0-9a-f]{6}$/i
const nf = new Intl.NumberFormat('de-DE')
const TABS = ['uebersicht', 'heatmap', 'einstellungen']
// Abo-Modell: die Reiter zeichnet die Etage (kn:reiter, public/fn-reiter.js); Reihenfolge und Wortlaut wie tabsHtml
const ANALYSE_REITER = [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }]
const reiterLabel = (key) => ANALYSE_REITER.find((r) => r.key === key)?.label || ANALYSE_REITER[0].label
// Abo-Modell, Texte A1 bis A3
const ZIELGRUPPE = 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.'
const KEIN_ABO_ANALYSE = 'Die Analyse ist im Hosting- und im Wartungs-Abo enthalten.'
const RANGES = [7, 30, 90]
const CLOCK = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
@@ -21,10 +28,14 @@ const showError = (root) => (err) => { root.innerHTML = `<p class="muted">${esc(
export async function initAnalyse(root, { projectId } = {}) {
if (!root) return
if (projectId) analyseSel.projectId = projectId
const { projects = [] } = await request('/api/funktionen')
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
if (root.dataset) delete root.dataset.wunschProjekt
if (wunsch) analyseSel.projectId = wunsch
const { projects = [], modell } = await request('/api/funktionen')
const abo = modell === 'abo' // strikt: nur dann Reiter in der Etage und die Texte A1 bis A3
const usable = projects.filter((p) => !analyseOf(p).comingSoon)
if (!usable.length) {
if (abo) meldeReiter(root, { eintraege: [] })
root.innerHTML = '<div class="an"><section class="an-card an-state"><p class="an-empty">Die Analyse ist für deine Websites noch nicht freigeschaltet.</p></section></div>'
return
}
@@ -33,7 +44,10 @@ export async function initAnalyse(root, { projectId } = {}) {
const a = analyseOf(project)
const active = a.state === 'active'
const tab = TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht'
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${active ? tabsHtml(tab) : ''}<div id="an-stage"${active ? ` role="tabpanel" aria-labelledby="an-tab-${tab}"` : ''}></div></div>`
if (abo && !active) meldeReiter(root, { eintraege: [] }) // ohne Cockpit keine Reiter; das Cockpit meldet seine gleich nach dem Zeichnen
const leiste = active && !abo ? tabsHtml(tab) : ''
const panel = !active ? '' : abo ? ` role="tabpanel" aria-label="${reiterLabel(tab)}"` : ` role="tabpanel" aria-labelledby="an-tab-${tab}"`
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${leiste}<div id="an-stage"${panel}></div></div>`
const stage = root.querySelector('#an-stage')
const reload = () => initAnalyse(root).catch(showError(root))
// Website wechseln: der Reiter bleibt (analyseSel.tab)
@@ -42,11 +56,11 @@ export async function initAnalyse(root, { projectId } = {}) {
analyseSel.projectId = ev.target.value
reload()
})
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload })
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload, abo, root })
else if (a.state === 'disabled') stage.innerHTML = '<section class="an-card an-state"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></section>'
else if (a.state === 'unknown') stage.innerHTML = '<section class="an-card an-state"><p>Die Daten konnten gerade nicht geladen werden.</p></section>'
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
else renderAnalyseActivation(stage, project, reload)
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(abo && a.reason === 'kein_abo' ? KEIN_ABO_ANALYSE : a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
else renderAnalyseActivation(stage, project, reload, { modell })
}
// Kopfleiste (analyse-cards.js); Zeitraum nur in der Uebersicht, "Stand" = Uhrzeit des letzten Ladens, today = "Heute bisher" aus der Statistik
@@ -73,7 +87,8 @@ function controllerBlock(controller) {
return `<div class="an-ctl" id="an-ctl"><p>Auf der Datenschutz-Zusatzseite Deiner Website erscheinen als Verantwortlicher:</p><p><strong>${controllerLines(controller).map(esc).join(' · ')}</strong></p><p>Stimmt etwas nicht? Schreib uns im Chat, bevor Du einschaltest.</p></div>`
}
export function renderAnalyseActivation(stage, project, reload) {
export function renderAnalyseActivation(stage, project, reload, { modell } = {}) {
const abo = modell === 'abo' // Abo-Modell: Linkzeile statt des langen Zustimmungssatzes, Zielgruppen-Häkchen, audienceConfirmed
const a = analyseOf(project)
const legal = a.legal || {}
const controller = a.controller || { ok: false }
@@ -91,7 +106,10 @@ export function renderAnalyseActivation(stage, project, reload) {
<p class="an-hint hidden" id="an-hint">Diese Farbe ist für weiße Schrift zu hell. Der Banner nutzt dann Schwarz (#111111).</p>
<div id="an-preview"></div>
${controllerBlock(controller)}
<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>
${abo
? `<p class="an-hint" id="an-legal">Es gelten <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung</a> und <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitung</a> (Fassung ${esc(a.avvVersion)}).</p>
<label class="fn-consent"><input type="checkbox" id="an-zielgruppe"><span>${ZIELGRUPPE}</span></label>`
: `<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>`}
<p class="error hidden" id="an-error" role="alert"></p>
<div class="abo-actions"><button type="button" class="abo-btn" id="an-on" disabled>Analyse einschalten</button></div>
</div>`
@@ -104,7 +122,7 @@ export function renderAnalyseActivation(stage, project, reload) {
paint()
input.addEventListener('input', () => { color = input.value; paint() })
stage.querySelectorAll('.an-preset').forEach((b) => b.addEventListener('click', () => { color = b.dataset.color; input.value = color; paint() }))
const box = stage.querySelector('#an-avv')
const box = stage.querySelector(abo ? '#an-zielgruppe' : '#an-avv')
const button = stage.querySelector('#an-on')
const error = stage.querySelector('#an-error')
box.addEventListener('change', () => { button.disabled = !box.checked || controller.ok !== true })
@@ -113,7 +131,7 @@ export function renderAnalyseActivation(stage, project, reload) {
button.disabled = true
error.classList.add('hidden')
try {
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion } } })
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion }, ...(abo ? { audienceConfirmed: true } : {}) } })
reload()
} catch (err) {
error.textContent = err.message
@@ -171,7 +189,7 @@ function wireChart(stage) {
svg.addEventListener('pointercancel', hideChartTip)
}
function mountCockpit({ an, stage, project, usable, reload }) {
function mountCockpit({ an, stage, project, usable, reload, abo = false, root = null }) {
const pid = project.projectId
const state = { stats: null, range: 0, stamp: '', rank: { seen: {}, who: {} } }
const tabNow = () => (TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht')
@@ -195,7 +213,12 @@ function mountCockpit({ an, stage, project, usable, reload }) {
b.setAttribute('aria-selected', String(on))
b.tabIndex = on ? 0 : -1
})
stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
if (abo) stage.setAttribute('aria-label', reiterLabel(tab))
else stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
}
// Abo-Modell: den aktiven Reiter an die Etage melden (bei = pickTab); der Teaser der Uebersicht ruft ebenfalls pickTab
function melde() {
if (abo) meldeReiter(root, { eintraege: reiterEintraege('analyse', ANALYSE_REITER), aktiv: tabNow(), bei: pickTab })
}
// Anfrage je Zeitraum: Uebersicht und Heatmap teilen sich eine laufende Anfrage. Sie schreibt nichts in state; uebernommen
// (adopt) wird die Antwort erst nach current(mine), sonst ueberschriebe eine verspaetete den Stand des neueren Zeitraums.
@@ -276,6 +299,7 @@ function mountCockpit({ an, stage, project, usable, reload }) {
}
function showTab() {
paintTabs()
melde()
paintHead()
const tab = tabNow()
if (tab === 'heatmap') showHeat()
+28 -4
View File
@@ -139,7 +139,7 @@ function projectState(project) {
return ['kb-state-off', 'Noch keine Website']
}
const FEATURE_LABELS = { preview: 'Vorschau', chat: 'Chat', invoices: 'Rechnungen', news: 'News-Pinnwand', termine: 'Terminplaner', shop: 'Webshop' }
const FEATURE_LABELS = { preview: 'Vorschau', chat: 'Chat', invoices: 'Rechnungen', news: 'News-Pinnwand', termine: 'Terminplaner', shop: 'Webshop', unterkunft: 'Buchungskalender', zeiten: 'Öffnungszeiten' }
function renderProjectCard(project, features) {
const li = document.createElement('li')
@@ -584,6 +584,13 @@ function renderViewAsBanner(me) {
}
/* ------------------------------------------------------------- Rechnungen --- */
// @rechnungsnummer-anfang
/** Rechnungsnummer mit dem Projekt darunter (Stripe-Metadaten projektName, Feld `projekt`); beides maskiert. */
function invoiceNumberHtml(i) {
const project = i.projekt ? `<span class="inv-project">${escapeHtml(i.projekt)}</span>` : ''
return `${escapeHtml(i.number)}${project}`
}
// @rechnungsnummer-ende
async function loadCustomerInvoices() {
const body = document.getElementById('invoices-body')
if (!body) return
@@ -615,7 +622,7 @@ async function loadCustomerInvoices() {
<tbody>
${invoices.map((i) => `
<tr class="${Number(i.balance) > 0 ? 'is-open' : 'is-paid'}">
<td class="inv-no">${escapeHtml(i.number)}</td>
<td class="inv-no">${invoiceNumberHtml(i)}</td>
<td class="inv-date">${fmtDateShort(i.date)}</td>
<td class="inv-amount">${fmtEuro(i.amount)}</td>
<td class="inv-open">${fmtEuro(i.balance)}</td>
@@ -632,6 +639,19 @@ async function loadCustomerInvoices() {
}
}
// Abschnitt "Aufträge" im Reiter Rechnungen (public/auftrag-portal.js): Fehler lassen den Abschnitt weg und stören die Rechnungen nicht.
function loadCustomerOrders() {
return import('/auftrag-portal.js')
.then((m) => m.initAuftraege({
section: document.getElementById('orders-section'),
body: document.getElementById('orders-body'),
invoicesTitle: document.getElementById('invoices-title'),
request: api,
viewAs: chatState.viewAs,
}))
.catch(() => {})
}
/* -------------------------------------------------------------- Aktivität --- */
// Abschnitt "Was wir zuletzt erledigt haben" unter Projekte: je Auftrag eine Karte mit Status, Fortschrittsleiste
// und Arbeiten (Bausteine der Projektdetails; Texte kommen aufbereitet vom Server). Zuerst die drei neuesten,
@@ -1910,7 +1930,7 @@ function initPasswordView() {
// "projekt" (Projektdetails als eigene Seite) gibt es ebenfalls nur in der App-Ansicht.
// "passwort" oeffnet das Konto-Menue (Profilbild). #fortschritt fuehrt zu Projekte und scrollt zum Abschnitt
// "Was wir zuletzt erledigt haben" (#pj-activity); hinter dem Hash darf ein Query stehen (#news?bestellt=1, Rueckkehr von Stripe).
const VIEW_HASHES = { projects: '', funktionen: 'funktionen', news: 'news', analyse: 'analyse', termine: 'termine', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort', webshop: 'webshop' }
const VIEW_HASHES = { projects: '', funktionen: 'funktionen', news: 'news', analyse: 'analyse', termine: 'termine', invoices: 'rechnungen', chat: 'chat', vertraege: 'vertraege', stunden: 'stunden', 'bestellen-hosting': 'bestellen-hosting', 'bestellen-wartung': 'bestellen-wartung', wechsel: 'wechsel', takeover: 'gutschrift', projekt: 'projekt', passwort: 'passwort', webshop: 'webshop', buchungen: 'buchungen', zeiten: 'zeiten' }
const HASH_ALIASES = { wartung: 'vertraege', abo: 'vertraege' }
const viewLoaders = { chat: initChat }
// Funktionen und News laden bei jedem Oeffnen neu (Zustand aendert sich durch Bestellung und Freischaltung); Modul erst dann laden
@@ -1920,6 +1940,9 @@ const viewRefreshers = {
analyse: () => import('/analyse.js').then((m) => m.initAnalyse(document.getElementById('analyse-body'))),
termine: () => import('/termine.js').then((m) => m.initTermine(document.getElementById('termine-body'))),
webshop: () => import('/shop.js').then((m) => m.initShop(document.getElementById('webshop-body'))),
buchungen: () => import('/buchungen.js').then((m) => m.initBuchungen(document.getElementById('buchungen-body'))),
// baut sich bei gleichem Projekt und Stand nicht neu auf, solange der Editor steht (ungespeicherte Eingaben bleiben; zeiten.js, initZeiten)
zeiten: () => import('/zeiten.js').then((m) => m.initZeiten(document.getElementById('zeiten-body'))),
}
const loadedViews = new Set()
@@ -2061,6 +2084,7 @@ async function initDashboardPage() {
if (aboParam) window.history.replaceState(null, '', `/dashboard.html${window.location.hash}`)
loadContracts({ afterCheckout: aboParam === 'success' })
loadCustomerInvoices()
loadCustomerOrders()
activityScrollPending = currentHash() === 'fortschritt'
loadCustomerActivity().then(scrollToActivity)
@@ -2112,7 +2136,7 @@ const appLayout = window.matchMedia('(max-width: 880px), (pointer: coarse) and (
const coarsePointer = window.matchMedia('(pointer: coarse)')
// Unterseite -> Tab, zu dem sie gehoert (und Text neben dem Zurueck-Pfeil).
// "passwort" setzt onAppViewShown: die Ansicht, aus der das Konto-Menue geoeffnet wurde.
const APP_PARENT = { news: 'funktionen', analyse: 'funktionen', termine: 'funktionen', stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects', webshop: 'funktionen' }
const APP_PARENT = { news: 'funktionen', analyse: 'funktionen', termine: 'funktionen', stunden: 'vertraege', 'bestellen-hosting': 'vertraege', 'bestellen-wartung': 'vertraege', wechsel: 'vertraege', takeover: 'vertraege', projekt: 'projects', webshop: 'funktionen', buchungen: 'funktionen', zeiten: 'funktionen' }
const APP_PARENT_LABEL = { vertraege: 'Verträge', projects: 'Projekte', funktionen: 'Funktionen', invoices: 'Rechnungen', chat: 'Chat' }
const appShell = { view: 'projects', scroll: {}, fromHistory: false, sideCards: [], installPrompt: null }
const TEXT_INPUT = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
+260
View File
@@ -0,0 +1,260 @@
// Reine Ansichtslogik der Auftragsbestätigung (AB) im Kundenportal: Token, Beträge, Daten, Zustände, Status, AB-Nummer
// und alle Seitentexte. Ohne Importe, ohne DOM, ohne fetch; jede Funktion verträgt fehlende Felder.
// Die Anrede wählt der Aufrufer über ANREDE_SIE (öffentliche Seite, Widerrufsseite) oder ANREDE_DU (angemeldetes Portal).
export const TOKEN_MUSTER = /^[A-Za-z0-9_-]{43}$/
export const KONTAKT = { email: 'support@webklar.com', telefon: '0176 23726355' }
export const BESTAETIGUNG_FALLBACK = 'Ich bestätige den Auftrag zu diesen Bedingungen.'
export const ANREDE_SIE = 'sie'
export const ANREDE_DU = 'du'
/** Texte, die in beiden Anreden gleich lauten (weder Du- noch Sie-Formen). */
export const TEXTE_GEMEINSAM = {
titelOffen: 'Auftragsbestätigung',
leistung: 'Leistungsumfang',
aufwand: 'Abrechnung nach Aufwand',
zahlung: 'Zahlung',
termine: 'Termine',
unterlagen: 'Unterlagen',
spalten: ['Beschreibung', 'Menge', 'Einzelpreis', 'Summe'],
gesamtpreis: 'Gesamtpreis',
linkPdf: 'Auftragsbestätigung als PDF ansehen',
linkAgb: 'Allgemeine Geschäftsbedingungen (PDF)',
freiwillig: '(freiwillig)',
knopf: 'Auftrag bestätigen',
sendet: 'Wird gesendet …',
ansichtsmodus: 'Im Ansichtsmodus kann der Auftrag nicht bestätigt werden. Die Bestätigung gibt der Kunde selbst ab.',
erneut: 'Erneut versuchen',
kartePdf: 'PDF ansehen',
karteDetails: 'Details ansehen',
karteDetailsBestaetigen: 'Details ansehen und bestätigen',
}
/** Öffentliche Seite und Widerrufsseite (Sie-Form); enthält auch die Schlüssel, die das Portal in Du überschreibt. */
export const TEXTE_SIE = {
laedt: 'Ihre Auftragsbestätigung wird geladen …',
geladen: 'Ihre Auftragsbestätigung wurde geladen.',
titelBestaetigt: 'Ihr Auftrag ist bestätigt.',
intro: 'Bitte prüfen Sie die Angaben. Mit einem Klick bestätigen Sie den Auftrag.',
kontaktVor: 'Sie erreichen uns unter',
keinJs: 'Bitte aktivieren Sie JavaScript, um Ihren Auftrag zu bestätigen.',
abNummerUngueltig: 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfen Sie die Nummer oder lassen Sie das Feld leer.',
formular: 'Ihre Bestätigung',
speicherHinweis: 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.',
fruehbeginnWunsch: 'Wir beginnen auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist.',
fehler: {
keine: 'Keine Verbindung. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.',
haken: 'Bitte setzen Sie das Häkchen, um den Auftrag zu bestätigen.',
anmeldung: 'Ihre Anmeldung ist abgelaufen. Bitte laden Sie die Seite neu und melden Sie sich erneut an.',
gesperrt: 'Das Bestätigen ist gerade nicht möglich. Bitte laden Sie die Seite neu.',
zuviele: 'Zu viele Anfragen. Bitte versuchen Sie es in einigen Minuten noch einmal.',
sonst: 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es später noch einmal.',
},
}
/** Angemeldetes Portal (Du-Form): nur die Schlüssel, die anders lauten. */
export const TEXTE_DU = {
formular: 'Deine Bestätigung',
speicherHinweis: 'Deine Bestätigung wird mit Datum und Uhrzeit gespeichert.',
fruehbeginnWunsch: 'Wir beginnen auf deinen Wunsch schon vor Ablauf der Widerrufsfrist.',
fehler: {
keine: 'Keine Verbindung. Bitte prüfe deine Internetverbindung und versuche es erneut.',
haken: 'Bitte setze das Häkchen, um den Auftrag zu bestätigen.',
anmeldung: 'Deine Anmeldung ist abgelaufen. Bitte lade die Seite neu und melde dich erneut an.',
gesperrt: 'Das Bestätigen ist gerade nicht möglich. Bitte lade die Seite neu.',
zuviele: 'Zu viele Anfragen. Bitte versuche es in einigen Minuten noch einmal.',
sonst: 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuche es später noch einmal.',
},
}
/** Texte der gewählten Anrede (ohne Angabe: Sie). */
export const texte = (anrede = ANREDE_SIE) => ({ ...TEXTE_GEMEINSAM, ...(anrede === ANREDE_DU ? TEXTE_DU : TEXTE_SIE) })
/* ------------------------------------------------------------------ Token und Links --- */
/** Token aus der Adresse: zuerst das Fragment (so verlinkt K31), dann die Suche (Rückfall). '' = ungültig. */
export function tokenAusAdresse(suche = '', hash = '') {
for (const roh of [hash, suche]) {
const teil = String(roh ?? '').replace(/^[#?]/, '')
if (!teil) continue
const werte = new URLSearchParams(teil).getAll('t')
if (werte.length === 1 && TOKEN_MUSTER.test(werte[0])) return werte[0]
}
return ''
}
export const pdfPfad = (token) => `/api/auftrag/${encodeURIComponent(token)}/pdf`
/** AGB-Link: nur ein PDF unter /rechtliches/, sonst ''. */
export const agbPfad = (agbUrl) => (typeof agbUrl === 'string' && /^\/rechtliches\/[A-Za-z0-9][A-Za-z0-9._-]*\.pdf$/.test(agbUrl) ? agbUrl : '')
/* ------------------------------------------------------------------ Beträge und Zeiten --- */
const EURO = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })
const BERLIN_TAG = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' })
const BERLIN_ZEIT = new Intl.DateTimeFormat('de-DE', {
timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
})
/** Ganze Cent als deutscher Euro-Betrag (negativ mit dem Minuszeichen), sonst '–'. */
export function euroDe(cents) {
if (!Number.isSafeInteger(cents)) return '–'
const text = EURO.format(cents / 100)
return text.startsWith('-') ? `\u{2212}${text.slice(1)}` : text
}
/** 'YYYY-MM-DD' oder ISO-Zeitpunkt (Berliner Tag) oder 'TT.MM.JJJJ' als 'TT.MM.JJJJ', sonst ''. */
export function datumDe(wert) {
if (typeof wert !== 'string') return ''
if (/^\d{2}\.\d{2}\.\d{4}$/.test(wert)) return wert
if (/^\d{4}-\d{2}-\d{2}$/.test(wert)) return `${wert.slice(8, 10)}.${wert.slice(5, 7)}.${wert.slice(0, 4)}`
if (/^\d{4}-\d{2}-\d{2}T/.test(wert)) {
const zeit = new Date(wert)
if (Number.isNaN(zeit.getTime())) return ''
return datumDe(BERLIN_TAG.format(zeit))
}
return ''
}
/** 'TT.MM.JJJJ, HH:MM Uhr' in Berliner Zeit, sonst ''. */
export function zeitpunktDe(iso) {
if (typeof iso !== 'string' || !iso) return ''
const zeit = new Date(iso)
if (Number.isNaN(zeit.getTime())) return ''
const teile = Object.fromEntries(BERLIN_ZEIT.formatToParts(zeit).map((teil) => [teil.type, teil.value]))
return `${teile.day}.${teile.month}.${teile.year}, ${teile.hour}:${teile.minute} Uhr`
}
/* ------------------------------------------------------------------ Zustand der AB --- */
export const zeigtFruehbeginn = (a) => typeof a?.texte?.fruehbeginn === 'string' && a.texte.fruehbeginn.trim() !== ''
export const darfBestaetigen = (a) => !a?.bestaetigtAm && (a?.status === 'versendet' || a?.status === 'abgerechnet')
const STATUS = {
versendet: { text: 'Versendet', ton: 'muted' },
bestaetigt: { text: 'Bestätigt', ton: 'ok' },
abgerechnet: { text: 'Abgerechnet', ton: 'ok' },
storniert: { text: 'Storniert', ton: 'muted' },
ersetzt: { text: 'Ersetzt', ton: 'muted' },
widerrufen: { text: 'Widerrufen', ton: 'rot' },
}
export function statusAnzeige(a) {
if (a?.kannBestaetigen === true && !a.bestaetigtAm) return { text: 'Bitte bestätigen', ton: 'warn' }
const eintrag = STATUS[a?.status]
return eintrag ? { ...eintrag } : { text: '–', ton: 'muted' }
}
export function kartenMeta(a) {
const teile = []
const datum = datumDe(a?.datum)
if (datum) teile.push(`vom ${datum}`)
if (a?.art === 'aufwand') teile.push('nach Aufwand')
else if (Number.isSafeInteger(a?.summeCents)) teile.push(euroDe(a.summeCents))
return teile.join(' · ')
}
/** Kopie: zuerst die AB, die noch bestätigt werden kann, dann nach Nummer absteigend (Zeichenvergleich). */
export function sortiereAuftraege(liste) {
if (!Array.isArray(liste)) return []
return [...liste].sort((x, y) => {
const a = x?.kannBestaetigen === true ? 0 : 1
const b = y?.kannBestaetigen === true ? 0 : 1
if (a !== b) return a - b
const nx = String(x?.nummer ?? '')
const ny = String(y?.nummer ?? '')
return nx < ny ? 1 : nx > ny ? -1 : 0
})
}
/* ------------------------------------------------------------------ Inhalte --- */
export function zusammenfassung(a) {
const auftrag = a ?? {}
const aufwand = auftrag.art === 'aufwand'
const kopf = [
{ label: 'Nummer', wert: auftrag.nummer || '' },
{ label: 'Datum', wert: datumDe(auftrag.datum) },
{ label: 'Projekt', wert: auftrag.titel || '' },
].filter((zeile) => zeile.wert)
const positionen = (Array.isArray(auftrag.positionen) ? auftrag.positionen : []).map((p) => ({
beschreibung: String(p?.beschreibung ?? ''),
menge: String(p?.menge ?? ''),
einzelpreis: euroDe(p?.einzelpreisCents),
summe: euroDe(p?.summeCents),
}))
const termine = [
{ label: 'Start', wert: datumDe(auftrag.startAm) },
{ label: 'Voraussichtliche Fertigstellung', wert: datumDe(auftrag.fertigBis) },
].filter((zeile) => zeile.wert)
const hinweis = typeof auftrag.texte?.kleinunternehmer === 'string' ? auftrag.texte.kleinunternehmer.trim() : ''
return {
kopf,
art: aufwand ? 'aufwand' : 'festpreis',
positionen,
summe: !aufwand && Number.isSafeInteger(auftrag.summeCents) ? euroDe(auftrag.summeCents) : '',
aufwand: aufwand ? String(auftrag.aufwandText ?? '') : '',
zahlung: String(auftrag.zahlungsplanText ?? ''),
termine,
hinweis,
}
}
export function bestaetigtZeilen(a, anrede = ANREDE_SIE) {
if (!a?.bestaetigtAm) return []
const zeitpunkt = zeitpunktDe(a.bestaetigtAm)
const zeilen = [zeitpunkt ? `Bestätigt am ${zeitpunkt}` : 'Bestätigt']
if (a.kundenart === 'privat') {
if (a.fruehbeginn === true) zeilen.push(texte(anrede).fruehbeginnWunsch)
else if (datumDe(a.fruehesterBeginn)) zeilen.push(`Wir beginnen frühestens am ${datumDe(a.fruehesterBeginn)}.`)
}
return zeilen
}
/* ------------------------------------------------------------------ Antworten des Servers --- */
const BEENDET_GRUENDE = [['ersetzt', /ersetzt/i], ['storniert', /storniert/i], ['widerrufen', /widerrufen/i]]
/** Zustand der öffentlichen Seite aus Status und Antwort (nur feste eigene Texte, nie Servertext). */
export function zustandAusAntwort(status, body) {
if (status === 404) {
return {
zustand: 'ungueltig', titel: 'Dieser Link ist ungültig oder abgelaufen.',
text: 'Bitte melden Sie sich bei uns. Wir senden Ihnen gern einen neuen Link.',
}
}
if (status === 410) {
const meldung = typeof body?.error === 'string' ? body.error : ''
const grund = BEENDET_GRUENDE.find(([, muster]) => muster.test(meldung))?.[0] || ''
return {
zustand: 'beendet',
titel: grund ? `Diese Auftragsbestätigung wurde ${grund}.` : 'Diese Auftragsbestätigung ist nicht mehr gültig.',
text: grund === 'ersetzt'
? 'Bitte verwenden Sie den Link in der neuesten Auftragsbestätigung, die wir Ihnen geschickt haben.'
: 'Bei Fragen melden Sie sich gern bei uns.',
}
}
if (status === 429) return { zustand: 'limit', titel: 'Zu viele Anfragen', text: 'Bitte versuchen Sie es in einigen Minuten noch einmal.' }
return {
zustand: 'fehler', titel: 'Das hat leider nicht geklappt',
text: 'Ihre Auftragsbestätigung konnte gerade nicht geladen werden. Bitte versuchen Sie es in einigen Minuten noch einmal.',
}
}
/** Meldung beim Bestätigen nach Status (feste eigene Texte der gewählten Anrede). */
export function fehlerMeldung(status, anrede = ANREDE_SIE) {
const fehler = texte(anrede).fehler
if (status === 0) return fehler.keine
if (status === 400) return fehler.haken
if (status === 401) return fehler.anmeldung
if (status === 403 || status === 409) return fehler.gesperrt
if (status === 429) return fehler.zuviele
return fehler.sonst
}
/** Freiwillige AB-Nummer der Widerrufsseite: 'AB-JJJJ-NNNN' oder ''. */
export function abNummerNormalisiert(eingabe) {
const text = String(eingabe ?? '').trim().toUpperCase().replace(/[\s\-\u{2010}-\u{2015}\u{2212}]+/gu, '-')
return /^AB-\d{4}-\d{4}$/.test(text) ? text : ''
}
+126
View File
@@ -0,0 +1,126 @@
/* Auftragsbestätigung im Kundenportal: DOM-Bausteine (Zusammenfassung, Bestätigt-Block, Unterlagen, Bestätigen-Formular).
Kundentext und Servertext kommen ausschließlich über textContent in die Seite (h), nie über innerHTML.
Alle Texte stehen in auftrag-ansicht.js; die Anrede (Sie auf der öffentlichen Seite, Du im Portal) wählt der Aufrufer. */
import { ANREDE_SIE, BESTAETIGUNG_FALLBACK, bestaetigtZeilen, fehlerMeldung, texte, zeigtFruehbeginn, zusammenfassung } from './auftrag-ansicht.js'
/** Element bauen: Texte nur als Textknoten, Attribute mit on… als Ereignis, true setzt ein Attribut ohne Wert, false/null/undefined lassen es weg. */
export function h(tag, attrs, ...kinder) {
const el = document.createElement(tag)
for (const [name, wert] of Object.entries(attrs || {})) {
if (wert === undefined || wert === null || wert === false) continue
if (name === 'class') el.className = wert
else if (name.startsWith('on')) el.addEventListener(name.slice(2), wert)
else el.setAttribute(name, wert === true ? '' : wert)
}
for (const kind of kinder.flat()) if (kind !== undefined && kind !== null && kind !== false) el.append(kind)
return el
}
const zeile = (label, wert) => h('div', { class: 'ab-zeile' }, h('dt', null, label), h('dd', null, wert))
const liste = (zeilen) => h('dl', { class: 'ab-liste' }, zeilen.map((z) => zeile(z.label, z.wert)))
/** Zusammenfassung der AB (Kopf, Leistung bzw. Aufwand, Zahlung, Termine); ebene = Überschriftenebene der Abschnitte. */
export function renderZusammenfassung(auftrag, { ebene = 2 } = {}) {
const t = texte(ANREDE_SIE)
const z = zusammenfassung(auftrag)
const kopfzeile = (text) => h(`h${ebene}`, { class: 'ab-h2' }, text)
const abschnitt = (name, titel, ...inhalt) => h('section', { class: 'ab-abschnitt', 'data-abschnitt': name }, kopfzeile(titel), ...inhalt)
const hinweis = z.hinweis ? h('p', { class: 'ab-hinweis', 'data-hinweis': true }, z.hinweis) : null
const teile = []
if (z.kopf.length) teile.push(liste(z.kopf))
if (z.art === 'aufwand') {
teile.push(abschnitt('aufwand', t.aufwand, h('p', { class: 'ab-text', 'data-aufwand': true }, z.aufwand), hinweis))
} else {
const tabelle = z.positionen.length
? h('table', { class: 'ab-tabelle', 'data-positionen': true },
h('thead', null, h('tr', null, t.spalten.map((spalte) => h('th', { scope: 'col' }, spalte)))),
h('tbody', null, z.positionen.map((p) => h('tr', { 'data-position': true },
[p.beschreibung, p.menge, p.einzelpreis, p.summe].map((wert, i) => h('td', { 'data-label': t.spalten[i] }, wert))))))
: null
const gesamt = z.summe ? h('p', { class: 'ab-gesamt', 'data-gesamt': true }, h('span', null, t.gesamtpreis), ' ', h('strong', null, z.summe)) : null
teile.push(abschnitt('leistung', t.leistung, tabelle, gesamt, hinweis))
}
if (z.zahlung) teile.push(abschnitt('zahlung', t.zahlung, h('p', { class: 'ab-text', 'data-zahlung': true }, z.zahlung)))
if (z.termine.length) teile.push(abschnitt('termine', t.termine, liste(z.termine)))
return h('div', { class: 'ab-zusammenfassung', 'data-art': z.art }, teile)
}
/** Block „bestätigt“ (Zeitpunkt, bei Privatpersonen Zeile zum Beginn); null ohne Bestätigung. */
export function renderBestaetigtBlock(auftrag, { anrede = ANREDE_SIE } = {}) {
const zeilen = bestaetigtZeilen(auftrag, anrede)
if (!zeilen.length) return null
return h('div', { class: 'ab-bestaetigt', 'data-bestaetigt': true, role: 'status', tabindex: '-1' },
zeilen.map((text, i) => h('p', null, i === 0 ? h('strong', null, text) : text)))
}
/** Links auf die AB als PDF und die AGB (neuer Tab); null ohne Ziele. */
export function renderDokumente({ pdfHref = '', agbHref = '' } = {}) {
const t = texte(ANREDE_SIE)
const link = (art, href, text) => h('li', null, h('a', { class: 'ab-link', 'data-link': art, href, target: '_blank', rel: 'noopener noreferrer' }, text))
const eintraege = [pdfHref ? link('pdf', pdfHref, t.linkPdf) : null, agbHref ? link('agb', agbHref, t.linkAgb) : null].filter(Boolean)
return eintraege.length ? h('ul', { class: 'ab-links' }, eintraege) : null
}
/**
* Formular zum Bestätigen: ein Häkchen, bei Privatpersonen ein zweites, freiwilliges (Frühbeginn). Sendet genau einmal:
* senden(koerper) → { ok: true, auftrag } | { ok: false, status, body }; nachErfolg(auftrag); nachEnde(status, body) bei 404/410.
* gesperrt (Text) = Ansichtsmodus: Häkchen und Knopf gesperrt, Hinweis sichtbar.
*/
export function renderBestaetigung(auftrag, { senden, nachErfolg, nachEnde, gesperrt = '', anrede = ANREDE_SIE } = {}) {
const t = texte(anrede)
let busy = false
const haken1 = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung', required: true, disabled: Boolean(gesperrt) })
const haken2 = zeigtFruehbeginn(auftrag) ? h('input', { type: 'checkbox', 'data-feld': 'fruehbeginn', disabled: Boolean(gesperrt) }) : null
const fehler = h('p', { class: 'ab-fehler', 'data-fehler': true, role: 'alert', hidden: true })
const knopf = h('button', { type: 'submit', class: 'ab-knopf', 'data-aktion': 'bestaetigen', disabled: true }, t.knopf)
const aktualisiere = () => { knopf.disabled = Boolean(gesperrt) || busy || !haken1.checked }
const form = h('form', {
class: 'ab-bestaetigen',
novalidate: true,
onsubmit: async (ev) => {
ev.preventDefault()
if (busy || gesperrt || !haken1.checked) return
busy = true
fehler.hidden = true
knopf.disabled = true
knopf.setAttribute('aria-busy', 'true')
knopf.textContent = t.sendet
let ergebnis
try {
ergebnis = await senden({ bestaetigt: true, fruehbeginn: Boolean(haken2?.checked) })
} catch {
ergebnis = { ok: false, status: 0, body: {} }
}
knopf.removeAttribute('aria-busy')
knopf.textContent = t.knopf
if (ergebnis?.ok) {
nachErfolg?.(ergebnis.auftrag)
return
}
const status = ergebnis?.status ?? 0
if (status === 404 || status === 410) {
nachEnde?.(status, ergebnis?.body)
return
}
busy = false
aktualisiere()
fehler.textContent = fehlerMeldung(status, anrede)
fehler.hidden = false
},
},
h('fieldset', { class: 'ab-feldgruppe' },
h('legend', null, t.formular),
h('label', { class: 'ab-haken' }, haken1, h('span', null, auftrag?.texte?.bestaetigung || BESTAETIGUNG_FALLBACK)),
haken2 ? h('label', { class: 'ab-haken' }, haken2, h('span', null, auftrag.texte.fruehbeginn, ' ', h('small', { class: 'ab-frei' }, t.freiwillig))) : null),
fehler,
knopf,
h('p', { class: 'ab-notiz' }, t.speicherHinweis),
gesperrt ? h('p', { class: 'ab-gesperrt', 'data-gesperrt': true }, gesperrt) : null)
haken1.addEventListener('change', aktualisiere)
aktualisiere()
return form
}
+84
View File
@@ -0,0 +1,84 @@
/* Abschnitt „Aufträge“ im Reiter Rechnungen des Kundenportals (geladen von app.js, loadCustomerOrders; request ist api aus app.js).
Kundentext kommt nur über textContent in die Seite. Alle Texte stehen in auftrag-ansicht.js; das Portal spricht in der Du-Form. */
import {
ANREDE_DU, agbPfad, darfBestaetigen, kartenMeta, sortiereAuftraege, statusAnzeige, texte,
} from './auftrag-ansicht.js'
import {
h, renderBestaetigtBlock, renderBestaetigung, renderDokumente, renderZusammenfassung,
} from './auftrag-dom.js'
export async function initAuftraege({ section, body, invoicesTitle, request, viewAs = false } = {}) {
if (!section || !body || typeof request !== 'function') return { sichtbar: false }
const t = texte(ANREDE_DU)
let eintraege = []
/** Bestätigen darf, wer der Server dafür freigibt; in der Kundenansicht zeigt die Oberfläche die offene AB mit gesperrtem Formular. */
const wirksam = (e) => darfBestaetigen(e) && (viewAs || e.kannBestaetigen === true)
async function ladeListe() {
const daten = await request('/api/auftraege')
if (daten?.aktiv !== true || !Array.isArray(daten.auftraege)) return []
return daten.auftraege.filter((e) => e && typeof e.id === 'string' && e.id !== '')
}
function karte(auftrag, offeneId) {
const kann = wirksam(auftrag)
const status = statusAnzeige({ ...auftrag, kannBestaetigen: kann })
const pfad = `/api/auftraege/${encodeURIComponent(auftrag.id)}`
async function senden(koerper) {
try {
const antwort = await request(`${pfad}/bestaetigen`, { method: 'POST', body: JSON.stringify(koerper) })
return antwort?.auftrag ? { ok: true, auftrag: antwort.auftrag } : { ok: false, status: 0, body: {} }
} catch (err) {
return { ok: false, status: Number(err?.status) || 0, body: { error: String(err?.message ?? '') } }
}
}
function nachErfolg(neu) {
eintraege = eintraege.map((alt) => (alt.id === auftrag.id ? { ...alt, ...neu, id: alt.id, kannBestaetigen: false } : alt))
zeichne(auftrag.id)
body.querySelector(`[data-auftrag="${CSS.escape(auftrag.id)}"] [data-bestaetigt]`)?.focus()
}
async function nachEnde() {
try {
eintraege = await ladeListe()
zeichne('')
} catch {
/* die Anzeige bleibt stehen */
}
}
return h('article', { class: 'ab-karte', 'data-auftrag': auftrag.id, 'data-status': auftrag.status || '' },
h('div', { class: 'ab-karte-kopf' },
h('div', null,
h('p', { class: 'ab-karte-nummer' }, h('strong', null, auftrag.nummer || '')),
auftrag.titel ? h('p', { class: 'ab-karte-titel' }, auftrag.titel) : null,
h('p', { class: 'ab-karte-meta' }, kartenMeta(auftrag))),
h('span', { class: `ab-pille ab-pille-${status.ton}` }, status.text)),
h('div', { class: 'ab-karte-aktionen' },
h('a', { class: 'ab-link', 'data-link': 'pdf', href: `${pfad}/pdf`, target: '_blank', rel: 'noopener noreferrer' }, t.kartePdf)),
h('details', { class: 'ab-details', open: kann || auftrag.id === offeneId },
h('summary', null, kann ? t.karteDetailsBestaetigen : t.karteDetails),
renderZusammenfassung(auftrag, { ebene: 4 }),
renderDokumente({ agbHref: agbPfad(auftrag.agbUrl) }),
renderBestaetigtBlock(auftrag, { anrede: ANREDE_DU }),
kann
? renderBestaetigung(auftrag, { senden, nachErfolg, nachEnde, gesperrt: viewAs ? t.ansichtsmodus : '', anrede: ANREDE_DU })
: null))
}
function zeichne(offeneId) {
body.replaceChildren(...sortiereAuftraege(eintraege.map((e) => ({ ...e, kannBestaetigen: wirksam(e) }))).map((e) => karte(eintraege.find((x) => x.id === e.id), offeneId)))
}
try {
eintraege = await ladeListe()
} catch {
return { sichtbar: false }
}
if (!eintraege.length) return { sichtbar: false }
zeichne('')
section.classList.remove('hidden')
invoicesTitle?.classList.remove('hidden')
return { sichtbar: true }
}
+59
View File
@@ -0,0 +1,59 @@
/* Widerrufsseite: Vertragsart „Projektauftrag“ mit freiwilliger Nummer der Auftragsbestätigung (Feld abNummer).
Rechtsfunktion: dieses Modul darf die anderen Vertragsarten nie stören. Fällt es aus, funktioniert der Widerruf wie bisher.
Das Feld ist nur bei „Projektauftrag“ sichtbar; eine eingegebene, aber unpassende Nummer wird gemeldet und stoppt das Absenden,
ein leeres Feld lässt den Widerruf durch (der Widerruf bleibt formfrei). Die Meldung steht in auftrag-ansicht.js. */
import { ANREDE_DU, abNummerNormalisiert, texte } from './auftrag-ansicht.js'
export function initProjektWiderruf(form) {
const wrap = form?.querySelector('#ab-nummer-wrap')
const feld = form?.querySelector('#abNummer')
const fehler = form?.querySelector('#form-error')
if (!form || !wrap || !feld || !fehler) return
const gewaehlt = () => form.querySelector('[name="order"]:checked')?.value
let eigeneMeldung = false
const meldungWeg = () => {
if (!eigeneMeldung) return
eigeneMeldung = false
fehler.textContent = ''
fehler.classList.add('hidden')
}
const zeige = () => {
const projekt = gewaehlt() === 'projekt'
wrap.classList.toggle('hidden', !projekt)
if (!projekt) {
feld.value = ''
meldungWeg()
}
}
form.addEventListener('change', zeige)
form.addEventListener('click', (ev) => {
if (!ev.target?.closest?.('button[type="submit"], input[type="submit"]') || gewaehlt() !== 'projekt') return
const eingabe = feld.value.trim()
if (!eingabe) {
feld.value = ''
meldungWeg()
return
}
const nummer = abNummerNormalisiert(eingabe)
if (!nummer) {
ev.preventDefault()
// Neue Seite (data-anrede="du"): die Meldung in du; ohne das Attribut bleibt der Satz aus auftrag-ansicht.js
fehler.textContent = form.getAttribute('data-anrede') === ANREDE_DU
? 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfe die Nummer oder lass das Feld leer.'
: texte().abNummerUngueltig
fehler.classList.remove('hidden')
eigeneMeldung = true
feld.focus()
return
}
feld.value = nummer
meldungWeg()
})
zeige()
}
if (typeof document !== 'undefined' && document.getElementById('decl-form') && document.getElementById('ab-nummer-wrap')) {
initProjektWiderruf(document.getElementById('decl-form'))
}
+161
View File
@@ -0,0 +1,161 @@
/* Bestätigungsseite für Auftragsbestätigungen (auftrag.html) und Abschnitt „Aufträge“ im Portal; Farben wie die Mailvorlagen.
Die Datei wird auch vom Dashboard geladen: jede Regel ist an body.ab-seite bzw. .ab-portal gebunden, Ausnahmen sind
.ab-rechnungen-titel und .inv-project am Ende. */
/* ------------------------------------------------------------------ Farben --- */
body.ab-seite {
--ab-grund: #f1f0ec; --ab-karte: #ffffff; --ab-text: #111111; --ab-weich: #4a4843; --ab-linie: #e8e6e0; --ab-box: #f7f6f3;
--ab-knopf: #111111; --ab-knopf-text: #ffffff; --ab-fokus: #5a2a91;
--ab-warn-bg: #efe6fb; --ab-warn-text: #5a2a91; --ab-ok-bg: #e4f2e6; --ab-ok-text: #1d5b2c;
--ab-rot-bg: #fbe4e1; --ab-rot-text: #8c1d18; --ab-muted-bg: #ecebe6; --ab-muted-text: #4a4843; --ab-fehler: #b3261e;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
body.ab-seite {
--ab-grund: #161615; --ab-karte: #1f1f1e; --ab-text: #f2f1ee; --ab-weich: #a8a6a0; --ab-linie: #3a3a37; --ab-box: #2a2a28;
--ab-knopf: #f2f1ee; --ab-knopf-text: #161615; --ab-fokus: #d9c2f5;
--ab-warn-bg: #2d2140; --ab-warn-text: #d9c2f5; --ab-ok-bg: #1e3322; --ab-ok-text: #a9dcb3;
--ab-rot-bg: #3d1f1c; --ab-rot-text: #f4b5ae; --ab-muted-bg: #2d2d2b; --ab-muted-text: #a8a6a0; --ab-fehler: #ff8a80;
}
}
/* ------------------------------------------------------------------ Grundlayout --- */
body.ab-seite {
margin: 0; min-height: 100dvh; display: flex; flex-direction: column;
background: var(--ab-grund); color: var(--ab-text);
font: 16px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-text-size-adjust: 100%;
}
.ab-seite .hidden { display: none !important; }
.ab-seite .ab-innen { max-width: 40rem; margin: 0 auto; padding: 0 16px; box-sizing: border-box; width: 100%; }
.ab-seite .ab-kopf { padding: max(20px, env(safe-area-inset-top)) 0 12px; }
.ab-seite .ab-kopf .ab-innen { display: flex; align-items: center; gap: 10px; }
.ab-seite .ab-logo { width: 36px; height: 36px; border-radius: 10px; display: block; }
.ab-seite .ab-marke { font-weight: 800; letter-spacing: 0.08em; font-size: 15px; }
.ab-seite .ab-haupt { flex: 1 0 auto; padding-bottom: 24px; }
.ab-seite .ab-wurzel {
background: var(--ab-karte); border: 1px solid var(--ab-linie); border-radius: 16px; padding: 28px 24px;
overflow-wrap: anywhere; word-break: normal;
}
.ab-seite .ab-laden { margin: 0; color: var(--ab-weich); }
.ab-seite a { color: inherit; text-underline-offset: 2px; }
.ab-seite .ab-vh {
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.ab-seite .ab-fuss { flex-shrink: 0; padding: 8px 0 max(24px, env(safe-area-inset-bottom)); font-size: 13px; color: var(--ab-weich); }
.ab-seite .ab-fuss .ab-innen { display: flex; gap: 16px; flex-wrap: wrap; }
.ab-seite .ab-fuss a { color: var(--ab-weich); display: inline-flex; align-items: center; min-height: 44px; }
/* ------------------------------------------------------------------ Inhalt --- */
.ab-seite .ab-pille, .ab-portal .ab-pille {
display: inline-block; margin: 0 0 12px; padding: 4px 12px; border-radius: 999px; font-size: 12px; font-weight: 700; line-height: 1.4;
background: var(--ab-muted-bg); color: var(--ab-muted-text);
}
.ab-seite .ab-pille-warn, .ab-portal .ab-pille-warn { background: var(--ab-warn-bg); color: var(--ab-warn-text); }
.ab-seite .ab-pille-ok, .ab-portal .ab-pille-ok { background: var(--ab-ok-bg); color: var(--ab-ok-text); }
.ab-seite .ab-pille-rot, .ab-portal .ab-pille-rot { background: var(--ab-rot-bg); color: var(--ab-rot-text); }
.ab-seite .ab-h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 12px; }
.ab-seite .ab-h1:focus { outline: none; }
.ab-seite .ab-intro, .ab-seite .ab-text { margin: 0 0 16px; }
.ab-seite .ab-text, .ab-portal .ab-text { white-space: pre-line; }
.ab-seite .ab-kontakt { margin: 0 0 16px; color: var(--ab-weich); }
.ab-seite .ab-h2, .ab-portal .ab-h2 {
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; color: var(--ab-weich); margin: 24px 0 8px;
}
.ab-seite .ab-liste, .ab-portal .ab-liste { margin: 0 0 8px; padding: 0; }
.ab-seite .ab-zeile, .ab-portal .ab-zeile {
display: grid; grid-template-columns: 9rem 1fr; gap: 4px 12px; padding: 8px 0; border-top: 1px solid var(--ab-linie);
}
.ab-seite .ab-zeile dt, .ab-portal .ab-zeile dt { color: var(--ab-weich); }
.ab-seite .ab-zeile dd, .ab-portal .ab-zeile dd { margin: 0; }
.ab-seite .ab-gesamt, .ab-portal .ab-gesamt { display: flex; justify-content: space-between; gap: 12px; margin: 12px 0 8px; padding-top: 12px; border-top: 2px solid var(--ab-text); }
.ab-seite .ab-gesamt strong, .ab-portal .ab-gesamt strong { font-weight: 800; }
.ab-seite .ab-hinweis, .ab-portal .ab-hinweis { margin: 4px 0 0; font-size: 0.85rem; color: var(--ab-weich); }
.ab-seite .ab-links, .ab-portal .ab-links { list-style: none; margin: 0 0 16px; padding: 0; }
.ab-seite .ab-links a, .ab-portal .ab-links a { display: flex; align-items: center; min-height: 44px; line-height: 1.35; padding: 4px 0; font-weight: 600; }
.ab-seite .ab-bestaetigt, .ab-portal .ab-bestaetigt {
background: var(--ab-ok-bg); color: var(--ab-ok-text); border-radius: 12px; padding: 12px 16px; margin: 0 0 16px;
}
.ab-seite .ab-bestaetigt p, .ab-portal .ab-bestaetigt p { margin: 2px 0; }
.ab-seite .ab-bestaetigt:focus-visible, .ab-portal .ab-bestaetigt:focus-visible { outline: 3px solid var(--ab-fokus); outline-offset: 2px; }
/* ------------------------------------------------------------------ Tabelle --- */
.ab-seite .ab-tabelle, .ab-portal .ab-tabelle { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
.ab-seite .ab-tabelle th, .ab-portal .ab-tabelle th { text-align: left; font-weight: 600; color: var(--ab-weich); padding: 6px 8px 6px 0; border-bottom: 1px solid var(--ab-linie); }
.ab-seite .ab-tabelle td, .ab-portal .ab-tabelle td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--ab-linie); vertical-align: top; }
.ab-seite .ab-tabelle th:nth-child(n+2), .ab-seite .ab-tabelle td:nth-child(n+2),
.ab-portal .ab-tabelle th:nth-child(n+2), .ab-portal .ab-tabelle td:nth-child(n+2) { text-align: right; white-space: nowrap; }
@media (max-width: 640px) {
.ab-seite .ab-tabelle thead, .ab-portal .ab-tabelle thead {
position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.ab-seite .ab-tabelle, .ab-seite .ab-tabelle tbody, .ab-seite .ab-tabelle tr, .ab-seite .ab-tabelle td,
.ab-portal .ab-tabelle, .ab-portal .ab-tabelle tbody, .ab-portal .ab-tabelle tr, .ab-portal .ab-tabelle td { display: block; }
.ab-seite .ab-tabelle tr, .ab-portal .ab-tabelle tr { padding: 8px 0; border-bottom: 1px solid var(--ab-linie); }
.ab-seite .ab-tabelle td, .ab-portal .ab-tabelle td {
display: flex; justify-content: space-between; gap: 12px; border: 0; padding: 2px 0; text-align: right; white-space: normal;
}
.ab-seite .ab-tabelle td:first-child, .ab-portal .ab-tabelle td:first-child { display: block; text-align: left; font-weight: 700; }
.ab-seite .ab-tabelle td:nth-child(n+2), .ab-portal .ab-tabelle td:nth-child(n+2) { white-space: normal; }
.ab-seite .ab-tabelle td:nth-child(n+2)::before, .ab-portal .ab-tabelle td:nth-child(n+2)::before { content: attr(data-label); color: var(--ab-weich); text-align: left; }
.ab-seite .ab-zeile, .ab-portal .ab-zeile { grid-template-columns: 1fr; gap: 0; }
.ab-seite .ab-wurzel { padding: 22px 18px; }
}
/* ------------------------------------------------------------------ Formular --- */
.ab-seite .ab-bestaetigen, .ab-portal .ab-bestaetigen { margin-top: 24px; padding-top: 8px; border-top: 1px solid var(--ab-linie); }
.ab-seite .ab-feldgruppe, .ab-portal .ab-feldgruppe { border: 0; margin: 0; padding: 0; min-width: 0; }
.ab-seite .ab-feldgruppe legend, .ab-portal .ab-feldgruppe legend { font-weight: 700; padding: 0; margin: 12px 0 4px; }
.ab-seite .ab-haken, .ab-portal .ab-haken { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; min-height: 44px; font-size: 15px; cursor: pointer; }
.ab-seite .ab-haken input, .ab-portal .ab-haken input { flex: 0 0 22px; width: 22px; height: 22px; margin: 1px 0 0; accent-color: var(--ab-knopf); }
.ab-seite .ab-frei, .ab-portal .ab-frei { color: var(--ab-weich); font-size: 0.85em; }
.ab-seite .ab-fehler, .ab-portal .ab-fehler { margin: 8px 0; color: var(--ab-fehler); font-weight: 700; }
.ab-seite .ab-fehler[hidden], .ab-portal .ab-fehler[hidden] { display: none; }
.ab-seite .ab-knopf, .ab-portal .ab-knopf {
display: inline-block; min-height: 48px; padding: 0 24px; border: 0; border-radius: 10px; cursor: pointer;
background: var(--ab-knopf); color: var(--ab-knopf-text); font: inherit; font-weight: 700;
}
.ab-seite .ab-knopf:disabled, .ab-portal .ab-knopf:disabled { opacity: 0.45; cursor: default; }
.ab-seite .ab-knopf:focus-visible, .ab-portal .ab-knopf:focus-visible,
.ab-seite .ab-link:focus-visible, .ab-portal .ab-link:focus-visible { outline: 3px solid var(--ab-fokus); outline-offset: 2px; }
.ab-seite .ab-notiz, .ab-portal .ab-notiz, .ab-seite .ab-gesperrt, .ab-portal .ab-gesperrt { margin: 12px 0 0; font-size: 0.85rem; color: var(--ab-weich); }
@media (max-width: 640px) {
.ab-seite .ab-knopf, .ab-portal .ab-knopf { width: 100%; }
}
/* ------------------------------------------------------------------ Portal (Reiter Rechnungen, dunkel) --- */
.ab-portal {
--ab-karte: var(--kb-surface); --ab-text: #ffffff; --ab-weich: var(--kb-muted); --ab-linie: var(--kb-border); --ab-box: rgba(255, 255, 255, 0.04);
--ab-knopf: var(--kb-purple); --ab-knopf-text: #ffffff; --ab-fokus: #d4b4ff;
--ab-warn-bg: rgba(157, 61, 254, 0.16); --ab-warn-text: #d8b4fe; --ab-ok-bg: rgba(74, 222, 128, 0.1); --ab-ok-text: #86efac;
--ab-rot-bg: rgba(248, 113, 113, 0.12); --ab-rot-text: #fca5a5; --ab-muted-bg: rgba(255, 255, 255, 0.06); --ab-muted-text: var(--kb-muted);
--ab-fehler: #fca5a5;
margin: 0 0 28px; color: var(--ab-text);
}
.ab-portal h2 { font-size: 20px; margin: 0 0 6px; }
.ab-portal .ab-karte {
margin: 12px 0 0; padding: 16px 18px; border-radius: 16px; background: var(--ab-karte); border: 1px solid var(--ab-linie); overflow-wrap: anywhere;
}
.ab-portal .ab-karte-kopf { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.ab-portal .ab-karte-kopf .ab-pille { margin: 0; flex: none; }
.ab-portal .ab-karte-kopf p { margin: 0; }
.ab-portal .ab-karte-titel { margin-top: 2px; }
.ab-portal .ab-karte-meta { margin-top: 2px; font-size: 13px; color: var(--ab-weich); }
.ab-portal .ab-karte-aktionen { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.ab-portal .ab-karte-aktionen a {
display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--kb-border-strong);
color: var(--ab-text); text-decoration: none; font-weight: 600; font-size: 14px;
}
.ab-portal .ab-details > summary { cursor: pointer; display: flex; align-items: center; min-height: 44px; color: #d4b4ff; font-weight: 700; }
.ab-portal .ab-details[open] > summary { margin-bottom: 4px; }
.ab-portal .ab-knopf { border-radius: 999px; }
.ab-rechnungen-titel { font-size: 20px; margin: 0 0 10px; }
.inv-project { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 400; color: var(--kb-muted, rgba(255, 255, 255, 0.6)); overflow-wrap: anywhere; }
/* ------------------------------------------------------------------ Installierte App --- */
.ab-app-back { display: none; }
@media (display-mode: standalone) {
.ab-app-back {
display: inline-block; margin: calc(env(safe-area-inset-top) + 12px) 16px 0; font-size: 15px; font-weight: 600; color: var(--ab-text); text-decoration: none;
}
.ab-seite .ab-kopf { padding-top: 12px; }
}
+42
View File
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f1f0ec" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#161615" media="(prefers-color-scheme: dark)">
<title>Auftragsbestätigung – WEBklar</title>
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/auftrag.css">
<noscript><style>.ab-laden{display:none}</style></noscript>
</head>
<body class="ab-seite">
<!-- Bestätigungsseite für eine Auftragsbestätigung (Link aus der Mail K31: /auftrag.html#t=<Token>, Rückfall ?t=<Token>).
Der Token steht nur in der Adresse und im Speicher der Seite: keine Cookies, kein Browserspeicher, keine Fremdanfragen,
nichts im Protokoll (auftrag.js). Ohne Anmeldung erreichbar, nicht für Suchmaschinen. -->
<a class="ab-app-back" href="/dashboard.html">‹ Zurück zum Kundenbereich</a>
<header class="ab-kopf">
<div class="ab-innen"><img class="ab-logo" src="/webklar-logo.png" width="36" height="36" alt=""><span class="ab-marke">WEBklar</span></div>
</header>
<main class="ab-haupt">
<div class="ab-innen">
<div class="ab-wurzel" id="ab-wurzel" data-zustand="laedt" aria-busy="true">
<noscript><p>Bitte aktivieren Sie JavaScript, um Ihren Auftrag zu bestätigen.</p></noscript>
<p class="ab-laden">Ihre Auftragsbestätigung wird geladen …</p>
</div>
</div>
</main>
<div class="ab-vh" id="ab-ansage" aria-live="polite"></div>
<footer class="ab-fuss">
<div class="ab-innen">
<a href="/datenschutz.html" target="_blank" rel="noopener noreferrer">Datenschutz</a>
<a href="https://webklar.com/impressum" target="_blank" rel="noopener noreferrer">Impressum</a>
</div>
</footer>
<script type="module" src="/auftrag.js"></script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
/*! WEBklar Aufträge: öffentliche Bestätigungsseite (auftrag.html#t=<Token>, Rückfall ?t=). Link aus der Mail K31.
* Der Token steht nur in der Adresse, in Links und im Speicher der Seite, nie in Text, Ansage, Fehlermeldung oder Protokoll.
* Keine Cookies (credentials: 'omit'), kein Browserspeicher, keine Drittanfragen. Alle Texte stehen in auftrag-ansicht.js (Sie-Form)
* und kommen nur über textContent in die Seite. */
import {
KONTAKT, agbPfad, darfBestaetigen, pdfPfad, statusAnzeige, texte, tokenAusAdresse, zustandAusAntwort,
} from './auftrag-ansicht.js'
import { h, renderBestaetigtBlock, renderBestaetigung, renderDokumente, renderZusammenfassung } from './auftrag-dom.js'
/** Einzige Anfrage der Seite: { status, body }; jeder Fehler des Netzes ist Status 0 (nie die Meldung des Browsers weiterreichen). */
async function anfrage(token, aktion, { method = 'GET', body } = {}) {
try {
const antwort = await fetch(`/api/auftrag/${encodeURIComponent(token)}${aktion ? `/${aktion}` : ''}`, {
method,
credentials: 'omit',
headers: body ? { 'Content-Type': 'application/json' } : {},
body: body ? JSON.stringify(body) : undefined,
})
const daten = await antwort.json().catch(() => ({}))
return { status: antwort.status, body: daten !== null && typeof daten === 'object' ? daten : {} }
} catch {
return { status: 0, body: {} }
}
}
export async function initAuftragsseite({ root, ansage, suche = '', hash = '' }) {
const t = texte()
const token = tokenAusAdresse(suche, hash)
const sage = (text) => { ansage.textContent = text }
const zeige = (zustand, ...kinder) => {
root.dataset.zustand = zustand
root.setAttribute('aria-busy', zustand === 'laedt' ? 'true' : 'false')
root.replaceChildren(...kinder.filter(Boolean))
}
const kontakt = () => h('p', { class: 'ab-kontakt' },
t.kontaktVor, ' ',
h('a', { href: `mailto:${KONTAKT.email}` }, KONTAKT.email), ' oder ',
h('a', { href: `tel:${KONTAKT.telefon.replace(/\D/g, '')}` }, KONTAKT.telefon), '.')
function zeigeZustand({ zustand, titel, text }) {
zeige(zustand,
h('h1', { class: 'ab-h1', 'data-titel': true, tabindex: '-1' }, titel),
h('p', { class: 'ab-text' }, text),
zustand === 'limit' ? null : kontakt(),
zustand === 'limit' || zustand === 'fehler' ? h('button', { type: 'button', class: 'ab-knopf', 'data-aktion': 'erneut', onclick: () => lade() }, t.erneut) : null)
sage(titel)
}
async function senden(koerper) {
const { status, body } = await anfrage(token, 'bestaetigen', { method: 'POST', body: koerper })
if (status === 200 && body.auftrag) return { ok: true, auftrag: body.auftrag }
return { ok: false, status: status === 200 ? 0 : status, body }
}
function zeigeAuftrag(auftrag) {
const bestaetigt = Boolean(auftrag.bestaetigtAm)
const offen = darfBestaetigen(auftrag)
const status = statusAnzeige({ ...auftrag, kannBestaetigen: offen })
zeige(bestaetigt ? 'bestaetigt' : 'offen',
h('p', { class: `ab-pille ab-pille-${status.ton}` }, status.text),
h('h1', { class: 'ab-h1', 'data-titel': true, tabindex: '-1' }, bestaetigt ? t.titelBestaetigt : `${t.titelOffen} ${auftrag.nummer || ''}`.trim()),
bestaetigt ? null : h('p', { class: 'ab-intro' }, t.intro),
renderBestaetigtBlock(auftrag),
renderZusammenfassung(auftrag),
h('section', { class: 'ab-abschnitt' },
h('h2', { class: 'ab-h2' }, t.unterlagen),
renderDokumente({ pdfHref: pdfPfad(token), agbHref: agbPfad(auftrag.agbUrl) })),
offen
? renderBestaetigung(auftrag, {
senden,
nachErfolg: (neu) => {
zeigeAuftrag(neu)
sage(t.titelBestaetigt)
root.querySelector('[data-bestaetigt]')?.focus()
},
nachEnde: (code, antwortBody) => zeigeZustand(zustandAusAntwort(code, antwortBody)),
})
: null)
}
async function lade() {
if (!token) {
zeigeZustand(zustandAusAntwort(404, {}))
return
}
zeige('laedt', h('p', { class: 'ab-laden' }, t.laedt))
const { status, body } = await anfrage(token, '')
if (status === 200 && body.auftrag) {
zeigeAuftrag(body.auftrag)
sage(t.geladen)
} else {
zeigeZustand(zustandAusAntwort(status === 200 ? 0 : status, body))
}
}
await lade()
}
if (typeof document !== 'undefined' && document.getElementById('ab-wurzel')) {
initAuftragsseite({
root: document.getElementById('ab-wurzel'),
ansage: document.getElementById('ab-ansage'),
suche: window.location.search,
hash: window.location.hash,
})
}
+144
View File
@@ -0,0 +1,144 @@
/* Buchungskalender im Kundenbereich: die Anfrage der Funktion (Zustand off), Muster shop-anfrage.js.
POST /api/unterkunft/anfrage-funktion?projectId= { art, einheiten, portale, nachricht } => { anfrage } (portale: Liste aus 'booking' | 'airbnb'),
POST /api/unterkunft/anfrage-funktion/zurueckziehen?projectId=. Status im Stand (GET /status): anfrage:{ status:'offen'|'abgelehnt'|'angenommen',
at, begruendung?, anfrage:{ art, einheiten, portale, nachricht } }. Geprueft wird auf dem Server (Feldfehler am Feld). Text nur ueber textContent (h). */
import { allgemeinerFehler, datumText, feld, h, hakenFeld, request, setzeFehler, zeigeFehler } from './shop-dom.js'
import { fehlerFuerFormular } from './buchungen-fehler.js'
export const ARTEN = [['ferienwohnung', 'Ferienwohnung oder Ferienhaus'], ['monteurzimmer', 'Monteurwohnung'], ['pension', 'Pension oder Gästezimmer'], ['sonstiges', 'Sonstiges']]
export const PORTALE = [['booking', 'Booking.com'], ['airbnb', 'Airbnb']]
export const KALENDER_PREIS = '199 €'
const text = (wert) => (typeof wert === 'string' ? wert : '')
/** Der Server liest 'monteur' als Alias, gibt es aber in der Anfrage so zurueck; die Auswahl kennt nur 'monteurzimmer'. */
const artWert = (a) => (a === 'monteur' ? 'monteurzimmer' : a)
const labelVon = (liste, key) => liste.find(([k]) => k === key)?.[1] || ''
const enc = encodeURIComponent
/** Angaben der Anfrage als Zeilen [{ name, wert }] (Definitionsliste und Gruppe der Einschalt-Seite); ohne Objekt leer. */
export function anfrageZeilen(a) {
if (a === null || typeof a !== 'object') return []
const portale = (Array.isArray(a.portale) ? a.portale : []).map((p) => labelVon(PORTALE, p)).filter(Boolean).join(', ')
return [
['Art der Unterkunft', labelVon(ARTEN, artWert(a.art))],
['Anzahl Einheiten', Number.isInteger(a.einheiten) ? String(a.einheiten) : ''],
['Portale', portale || 'keine'],
['Nachricht', text(a.nachricht)],
].filter(([, wert]) => wert !== '').map(([name, wert]) => ({ name, wert }))
}
/** Angaben der Anfrage als Definitionsliste (nur Text). */
export const anfrageListe = (a) => h('dl', { class: 'sh-fakten' }, anfrageZeilen(a).map(({ name, wert }) => h('div', null, h('dt', null, name), h('dd', null, wert))))
/**
* Formular „Buchungskalender anfragen“ in root; nachSenden(anfrage) wird nach dem Speichern aufgerufen (die Ansicht zeichnet sich neu).
* modell === 'abo': die Stichpunkte nennen „Im Wartungs-Abo enthalten.“ statt des Preises und „einschalten“ statt „bestellen“.
*/
export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis = KALENDER_PREIS, nachSenden, modell } = {}) {
const abo = modell === 'abo'
const v = vorgabe && typeof vorgabe === 'object' ? vorgabe : {}
const art = h('select', null, [h('option', { value: '' }, 'Bitte wählen …'), ...ARTEN.map(([w, n]) => h('option', { value: w }, n))])
art.value = ARTEN.some(([k]) => k === artWert(v.art)) ? artWert(v.art) : ''
const einheiten = h('input', { type: 'text', inputmode: 'numeric', maxlength: 3, autocomplete: 'off' })
einheiten.value = Number.isInteger(v.einheiten) ? String(v.einheiten) : ''
const portale = PORTALE.map(([wert]) => {
const box = h('input', { type: 'checkbox', value: wert })
box.checked = Array.isArray(v.portale) && v.portale.includes(wert)
return box
})
const nachricht = h('textarea', { rows: 3, maxlength: 1000 })
nachricht.value = text(v.nachricht)
const absenden = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'anfrage-senden' }, 'Anfrage senden')
const form = h('form', { class: 'sh-form', novalidate: true, 'aria-label': 'Buchungskalender anfragen' },
feld('art', 'Welche Art von Unterkunft vermietest du?', art),
feld('einheiten', 'Wie viele Einheiten (Wohnungen, Zimmer) vermietest du?', einheiten),
h('fieldset', { class: 'sh-gruppe' },
h('legend', null, 'Nutzt du Booking.com oder Airbnb?'),
...PORTALE.map(([wert, name], i) => hakenFeld(`portale.${wert}`, portale[i], name)),
h('p', { class: 'sh-fehler', 'data-fehler': 'portale', role: 'alert' })),
feld('nachricht', 'Noch etwas, das wir wissen sollten? (freiwillig)', nachricht),
allgemeinerFehler(),
h('div', { class: 'sh-aktionen' }, absenden))
let laeuft = false
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
if (laeuft) return
laeuft = true
absenden.disabled = true
setzeFehler(form, {})
const zahl = einheiten.value.trim()
try {
const antwort = await request(`/api/unterkunft/anfrage-funktion?projectId=${enc(projectId)}`, {
method: 'POST',
body: {
art: art.value,
einheiten: /^\d{1,3}$/.test(zahl) ? Number(zahl) : zahl,
portale: portale.filter((b) => b.checked).map((b) => b.value),
nachricht: nachricht.value.trim(),
},
})
if (typeof nachSenden === 'function') await nachSenden(antwort?.anfrage ?? null)
return
} catch (err) {
zeigeFehler(form, fehlerFuerFormular(err))
if (err?.status === 409 && typeof nachSenden === 'function') {
try { await nachSenden(null, { meldung: fehlerFuerFormular(err).message }) } catch { /* der Fehler steht schon am Formular */ }
return
}
}
laeuft = false
absenden.disabled = false
})
const erklaerung = h('div', { class: 'sh-erklaerung' },
h('h3', null, 'Buchungskalender anfragen'),
h('p', null, 'Mit dem Buchungskalender zeigst du auf deiner Website, wann deine Unterkunft frei ist. Gäste fragen direkt an, du gibst frei, und die Rechnung entsteht automatisch.'),
h('ul', null,
h('li', null, abo ? 'Im Wartungs-Abo enthalten.' : `${preis} einmalig für den Einbau in deine Website.`),
h('li', null, 'Belegungskalender mit Abgleich zu Booking.com und Airbnb (über iCal).'),
h('li', null, 'Rechnung in deinem Namen und optional Online-Zahlung auf dein eigenes Stripe-Konto.'),
h('li', null, abo ? 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du einschalten.' : 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du bestellen.')))
const karte = h('section', { class: 'kb-panel sh-anfrage' }, erklaerung, form)
root.replaceChildren(karte)
return karte
}
/** Karte einer Anfrage im Stand offen (mit „Anfrage zurückziehen“) oder abgelehnt (Begründung, „Neue Anfrage stellen“). */
export function anfrageKarte(ansicht, { projectId, neu, neuLaden } = {}) {
const offen = ansicht?.status === 'offen'
const meldung = h('p', { class: 'sh-fehler', role: 'alert', 'data-meldung': 'anfrage' })
const datum = datumText(ansicht?.at)
let aktion
if (offen) {
aktion = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueckziehen' }, 'Anfrage zurückziehen')
aktion.addEventListener('click', async () => {
if (aktion.disabled) return
aktion.disabled = true
meldung.textContent = ''
try {
await request(`/api/unterkunft/anfrage-funktion/zurueckziehen?projectId=${enc(projectId)}`, { method: 'POST' })
if (typeof neuLaden === 'function') await neuLaden()
return
} catch (err) {
meldung.textContent = err.message
if (err?.status === 409 && typeof neuLaden === 'function') { try { await neuLaden({ meldung: err.message }) } catch { /* Meldung bleibt */ } return }
}
aktion.disabled = false
})
} else {
aktion = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'neue-anfrage', onclick: () => { if (typeof neu === 'function') neu() } }, 'Neue Anfrage stellen')
}
return h('section', { class: 'kb-panel sh-info sh-anfrage-karte', 'data-anfrage': offen ? 'offen' : 'abgelehnt' },
h('span', { class: `sh-pille ${offen ? 'sh-pille-wait' : 'sh-pille-bad'}` }, offen ? 'In Prüfung' : 'Abgelehnt'),
h('h3', null, offen ? 'Deine Anfrage wird geprüft' : 'Deine Anfrage wurde leider abgelehnt'),
offen
? h('p', null, `${datum ? `Du hast deine Anfrage am ${datum} Uhr gesendet. ` : ''}Wir melden uns, sobald wir sie angesehen haben. Bis dahin kannst du sie zurückziehen und eine neue stellen.`)
: h('p', null, 'Unsere Begründung:'),
!offen ? h('blockquote', { class: 'sh-begruendung' }, text(ansicht?.begruendung) || 'Dazu liegt keine Begründung vor.') : null,
ansicht?.anfrage ? h('div', { class: 'sh-zusammenfassung' }, h('h4', null, 'Deine Angaben'), anfrageListe(ansicht.anfrage)) : null,
meldung,
h('div', { class: 'sh-aktionen' }, aktion, h('button', { type: 'button', class: 'kb-btn-outline', 'data-view': 'chat' }, 'Frage dazu? Chat öffnen')))
}
+34
View File
@@ -0,0 +1,34 @@
/* Buchungskalender: Fehler der Schnittstelle fuers Formular. Die Schnittstelle antwortet { fehler: '<code>', felder?: {feld: text} };
request() (shop-dom.js) legt den Code in err.fehler und die ganze Antwort in err.body. Fuer zeigeFehler() daher vorher umformen:
zeigeFehler(form, fehlerFuerFormular(err)). */
const CODES = {
zeitraum_belegt: 'Der Zeitraum ist bereits belegt.',
zeitraum_ungueltig: 'Bitte prüfe den Zeitraum.',
zustimmung_fehlt: 'Bitte erteile zuerst in den Einstellungen die Zustimmung zur Rechnungsstellung in deinem Namen.',
stripe_nicht_aktiv: 'Online-Zahlung geht erst, wenn dein Stripe-Konto freigeschaltet ist.',
erstattung_fehlgeschlagen: 'Die Stornierung wurde durchgeführt, aber die Erstattung ist fehlgeschlagen. Bitte erstatte den Betrag im Stripe-Dashboard oder versuch es später noch einmal.',
frist_abgelaufen: 'Die Frist für diese Anfrage ist abgelaufen.',
preise_unvollstaendig: 'Die Preise sind noch nicht vollständig. Trage im Reiter „Unterkünfte“ unter „Preise“ alle Pflichtpreise ein und speichere sie.',
einstellungen_unvollstaendig: 'Deine Einstellungen sind noch nicht vollständig. Ergänze im Reiter „Einstellungen“ Rechnungsangaben und Zustimmung und speichere sie.',
status_ungueltig: 'Das geht beim aktuellen Stand der Anfrage nicht mehr. Lade die Seite neu und prüfe den Status.',
limit_erreicht: 'Du hast die Höchstzahl erreicht. Entferne zuerst einen vorhandenen Eintrag, dann kannst du einen neuen anlegen.',
kein_konto: 'Es ist noch kein Stripe-Konto verbunden. Richte zuerst die Online-Zahlung im Reiter „Shop“ ein.',
nicht_aktiv: 'Der Buchungskalender ist bei dir gerade nicht aktiv. Schalte ihn unter „Funktionen“ ein oder wende dich an uns.',
praefix_gesperrt: 'Das Rechnungspräfix kann nach der ersten Rechnung nicht mehr geändert werden. Lass das Feld unverändert.',
sperre_aus_ical: 'Diese Sperre stammt aus einem Kalenderimport und lässt sich hier nicht ändern. Ändere sie im Ursprungskalender oder entferne den Import.',
online_aus: 'Online-Zahlung ist bei dir ausgeschaltet. Schalte sie in den Einstellungen ein oder wähle „Vor Ort bezahlen“.',
zahlung_fehlgeschlagen: 'Die Zahlung konnte nicht gestartet werden. Versuche es in ein paar Minuten noch einmal.',
hat_anfragen: 'Zu dieser Unterkunft gibt es schon Anfragen, deshalb kannst du sie nicht löschen. Schalte sie stattdessen auf inaktiv.',
konto_vorhanden: 'Es ist schon ein anderes Stripe-Konto hinterlegt. Wende dich an uns, wenn du es wechseln möchtest.',
zu_viele_personen: 'Das sind mehr Personen, als die Unterkunft aufnimmt. Prüfe die Personenzahl.',
}
/** => { message, fehler: { feld: text } } (Feldfehler aus body.felder, sonst ein Text unter „_“). */
export function fehlerFuerFormular(err) {
const felder = err?.body?.felder
const STANDARD = 'Das hat nicht geklappt. Bitte versuch es noch einmal.'
const code = typeof err?.fehler === 'string' ? err.fehler : ''
const message = (code && CODES[code]) || (code ? STANDARD : err?.message) || STANDARD
if (felder && typeof felder === 'object' && Object.keys(felder).length) return { message, fehler: felder }
return { message, fehler: { _: message } }
}
+108
View File
@@ -0,0 +1,108 @@
/* Buchungskalender: iCal-Bereich einer Unterkunft (Importe von Booking.com/Airbnb, Export-Link, Kurzanleitungen).
unterkunft = Eintrag aus GET /api/unterkunft/unterkuenfte: { id, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }.
POST /unterkuenfte/:id/ical-importe { name, url } · DELETE …/ical-importe/:importId · POST …/ical-export/erneuern => { icalExportUrl }.
Nach jeder Aenderung ruft der Aufrufer aenderung() (laedt die Unterkunft neu). */
import { abschnitt, allgemeinerFehler, datumText, feld, frage, h, zeigeFehler } from './shop-dom.js'
import { enc, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
export function zeichneIcal(ctx, unterkunft, aenderung) {
const basis = `/api/unterkunft/unterkuenfte/${enc(unterkunft.id)}`
const q = `projectId=${enc(ctx.projectId)}`
const importe = Array.isArray(unterkunft.icalImporte) ? unterkunft.icalImporte : []
const dialog = h('div', { class: 'bk-dialog' })
const status = (i) => {
if (i.fehler) return `Der letzte Abgleich hat nicht geklappt: ${zeichen(i.fehler)}${i.letzterErfolgAt ? ` (zuletzt erfolgreich ${datumText(i.letzterErfolgAt)})` : ''}`
if (i.letzterErfolgAt) return `Zuletzt abgeglichen ${datumText(i.letzterErfolgAt)}`
return 'Noch nicht abgeglichen. Das passiert automatisch alle 15 Minuten.'
}
const importListe = importe.length
? h('ul', { class: 'bk-liste' }, importe.map((i) => h('li', { class: 'bk-import', 'data-import': i.id },
h('strong', null, zeichen(i.name)),
h('span', { class: i.fehler ? 'sh-fehler' : 'sh-hinweis', 'data-status': true }, status(i)),
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'import-weg', onclick: () => entfernenDialog(i) }, 'Entfernen'))))
: h('p', { class: 'sh-hinweis' }, 'Noch kein Kalender angebunden.')
function entfernenDialog(i) {
const box = frage('Kalender entfernen', h('p', null, `Die Belegung aus „${zeichen(i.name)}“ verschwindet dann aus deinem Kalender.`), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'import-weg-ok',
onclick: async (ev) => {
ev.currentTarget.disabled = true
try {
await ctx.request(`${basis}/ical-importe/${enc(i.id)}?${q}`, { method: 'DELETE' })
if (ctx.aktuell()) await aenderung('Der Kalender ist entfernt.')
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(box, err) }
},
}, 'Entfernen'),
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => dialog.replaceChildren() }, 'Abbrechen')))
dialog.replaceChildren(box)
}
const name = h('input', { type: 'text', id: 'bk-i-name', maxlength: '60', autocomplete: 'off', placeholder: 'Booking.com' })
const url = h('input', { type: 'text', id: 'bk-i-url', inputmode: 'url', autocomplete: 'off', placeholder: 'https://…' })
const neu = ctx.nurLesen ? null : h('form', { novalidate: true, class: 'bk-importform', onsubmit: (ev) => ev.preventDefault() },
feld('name', 'Name', name), feld('url', 'Link (iCal-Adresse)', url, 'Der Link beginnt mit https:// und endet oft auf .ics.'), allgemeinerFehler(),
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'import-dazu',
onclick: async (ev) => {
const form = ev.currentTarget.form || ev.currentTarget.closest('form')
const body = { name: String(name.value).trim(), url: String(url.value).trim() }
if (!body.name || !/^https:\/\//i.test(body.url)) return zeigeFehler(form, { message: '', fehler: { _: 'Bitte gib einen Namen und einen Link an, der mit https:// beginnt.' } })
ev.currentTarget.disabled = true
try {
await ctx.request(`${basis}/ical-importe?${q}`, { method: 'POST', body })
if (ctx.aktuell()) await aenderung('Der Kalender ist angebunden.')
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(form, err) }
},
}, 'Kalender hinzufügen'))
const exportUrl = zeichen(unterkunft.icalExportUrl)
const kopiert = h('p', { class: 'sh-hinweis', role: 'status', 'data-kopiert': true })
const erneuern = () => {
const box = frage('Link erneuern', h('p', null, 'Der alte Link funktioniert danach nicht mehr. Du musst den neuen Link bei Booking.com und Airbnb eintragen.'), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'erneuern-ok',
onclick: async (ev) => {
ev.currentTarget.disabled = true
try {
await ctx.request(`${basis}/ical-export/erneuern?${q}`, { method: 'POST' })
if (ctx.aktuell()) await aenderung('Der Link ist erneuert. Trage den neuen Link bei den Portalen ein.')
} catch (err) { ev.currentTarget.disabled = false; zeigeApiFehler(box, err) }
},
}, 'Link erneuern'),
h('button', { type: 'button', class: 'kb-btn-outline', onclick: () => dialog.replaceChildren() }, 'Abbrechen')))
dialog.replaceChildren(box)
}
const export_ = h('div', { class: 'bk-link' },
h('p', { class: 'sh-hinweis' }, 'Mit diesem Link sehen Booking.com und Airbnb, wann deine Unterkunft belegt ist. Es werden keine Gastdaten weitergegeben.'),
exportUrl ? h('input', { type: 'text', readonly: true, value: exportUrl, 'aria-label': 'Export-Link', 'data-export': true }) : h('p', { class: 'sh-hinweis' }, 'Der Link ist noch nicht verfügbar.'),
h('div', { class: 'bk-aktionen' },
exportUrl ? h('button', {
type: 'button', class: 'kb-btn-outline', 'data-aktion': 'export-kopieren',
onclick: async () => {
try { await navigator.clipboard.writeText(exportUrl); kopiert.textContent = 'Link kopiert.' } catch { kopiert.textContent = 'Kopieren hat nicht geklappt. Markiere den Link und kopiere ihn von Hand.' }
},
}, 'Kopieren') : null,
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'export-erneuern', onclick: erneuern }, 'Link erneuern')),
kopiert)
const anleitung = h('div', { class: 'bk-anleitung' },
h('details', { 'data-anleitung': 'booking' }, h('summary', null, 'Anleitung Booking.com'),
h('ol', null,
h('li', null, 'Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen.'),
h('li', null, 'Den Export-Link von oben dort einfügen und speichern.'),
h('li', null, 'Für die andere Richtung: Booking.com zeigt dir dort auch einen Link zum Exportieren. Den trägst du oben bei „Kalender anbinden“ ein.'))),
h('details', { 'data-anleitung': 'airbnb' }, h('summary', null, 'Anleitung Airbnb'),
h('ol', null,
h('li', null, 'Inserat → Verfügbarkeit → Kalender synchronisieren → Kalender verbinden.'),
h('li', null, 'Den Export-Link von oben dort einfügen und speichern.'),
h('li', null, 'Airbnb zeigt dir unter „Kalender exportieren“ einen Link. Den trägst du oben bei „Kalender anbinden“ ein.'))))
return abschnitt('Kalender-Abgleich (iCal)', 'So verhinderst du Doppelbuchungen: Belegungen von Booking.com oder Airbnb kommen in deinen Kalender, und deine Belegungen gehen dorthin.',
h('h4', null, 'Kalender anbinden'), importListe, neu,
h('h4', null, 'Dein Export-Link'), export_, anleitung, dialog)
}
+109
View File
@@ -0,0 +1,109 @@
/* Buchungskalender: Posten-Editor fuer die Freigabe einer Anfrage.
summeLive(posten, steuerart) ist ein Spiegel von server/services/unterkunft/preis.js summiere (gleiche Rundung): Anzeige waehrend des Tippens,
verbindlich rechnet der Server. Posten: { text, menge, einzelCent (negativ bei Rabatt), ustProzent }. */
import { euroText, euroZuCent, h } from './shop-dom.js'
export function summeLive(posten, steuerart) {
const bruttoCent = posten.reduce((a, p) => a + p.menge * p.einzelCent, 0)
if (steuerart === 'kleinunternehmer') return { bruttoCent, nettoCent: bruttoCent, ust: [] }
const proSatz = new Map()
for (const p of posten) proSatz.set(p.ustProzent, (proSatz.get(p.ustProzent) || 0) + p.menge * p.einzelCent)
const ust = [...proSatz.entries()]
.sort((a, b) => a[0] - b[0])
.map(([satz, brutto]) => {
const nettoCent = Math.round((brutto * 100) / (100 + satz))
return { satz, nettoCent, steuerCent: brutto - nettoCent }
})
const nettoCent = ust.reduce((a, u) => a + u.nettoCent, 0)
return { bruttoCent, nettoCent, ust }
}
/** Betrag mit Vorzeichen (Rabatt) in Euro-Schreibweise: -3850 ⇒ '-38,50 €'. */
export const euroMitVorzeichen = (cent) => (Number.isInteger(cent) ? (cent < 0 ? `-${euroText(-cent)}` : euroText(cent)) : '')
/** Feldtext ('-38,50', '−38,50 €', '12,5') in Cent mit Vorzeichen; null bei allem Unlesbaren. */
export function euroZuCentMitVorzeichen(text) {
if (typeof text !== 'string') return null
const s = text.trim()
const minus = /^[-−–]/.test(s)
const c = euroZuCent(minus ? s.slice(1) : s)
return c === null ? null : minus ? -c : c
}
const centFeld = (cent) => (Number.isInteger(cent) ? `${cent < 0 ? '-' : ''}${Math.floor(Math.abs(cent) / 100)},${String(Math.abs(cent) % 100).padStart(2, '0')}` : '')
/**
* Editor bauen. => { el, posten() => [{text,menge,einzelCent,ustProzent}] oder null bei Unlesbarem, summe() }.
* steuerart 'kleinunternehmer': keine USt-Spalte (ustProzent 0). aenderung() wird nach jeder Eingabe gerufen.
*/
export function baueEditor(start, steuerart, { aenderung } = {}) {
const klein = steuerart === 'kleinunternehmer'
const zeilen = []
const liste = h('div', { class: 'bk-posten' })
const summenEl = h('div', { class: 'bk-summen', 'data-summen': true, 'aria-live': 'polite' })
const lies = () => {
const ergebnis = []
for (const z of zeilen) {
const einzelCent = euroZuCentMitVorzeichen(String(z.einzel.value))
const menge = Number(String(z.menge.value).trim())
if (einzelCent === null || !Number.isInteger(menge) || menge < 1) return null
ergebnis.push({ text: String(z.text.value).trim(), menge, einzelCent, ustProzent: klein ? 0 : Number(z.ust.value) })
}
return ergebnis
}
const zeichneSumme = () => {
const p = lies()
if (!p) {
summenEl.replaceChildren(h('p', { class: 'sh-fehler' }, 'Bitte prüfe Menge und Preis der Posten.'))
return
}
const s = summeLive(p, steuerart)
summenEl.replaceChildren(
...(klein ? [] : [h('p', null, `Netto ${euroMitVorzeichen(s.nettoCent)}`), ...s.ust.map((u) => h('p', null, `${u.satz} % USt ${euroMitVorzeichen(u.steuerCent)}`))]),
h('p', { class: 'bk-gesamt' }, h('strong', null, `Gesamt ${euroMitVorzeichen(s.bruttoCent)}`)),
...(klein ? [h('p', { class: 'sh-hinweis' }, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')] : []))
}
const geaendert = () => {
zeichneSumme()
aenderung?.()
}
function fuegeZeile(p = { text: '', menge: 1, einzelCent: 0, ustProzent: 7 }) {
const z = {
text: h('input', { type: 'text', maxlength: '200', 'aria-label': 'Beschreibung', 'data-posten': 'text' }),
menge: h('input', { type: 'text', inputmode: 'numeric', 'aria-label': 'Menge', 'data-posten': 'menge' }),
einzel: h('input', { type: 'text', inputmode: 'decimal', autocomplete: 'off', 'aria-label': 'Einzelpreis in Euro (Rabatt mit Minus)', 'data-posten': 'einzel' }),
ust: klein ? null : h('select', { 'aria-label': 'Umsatzsteuer', 'data-posten': 'ust' }, [7, 19, 0].map((satz) => h('option', { value: String(satz) }, `${satz} %`))),
}
z.text.value = p.text
z.menge.value = String(p.menge)
z.einzel.value = centFeld(p.einzelCent)
if (z.ust) z.ust.value = String(p.ustProzent)
for (const el of [z.text, z.menge, z.einzel, z.ust]) el?.addEventListener('input', geaendert)
z.ust?.addEventListener('change', geaendert)
z.einzel.addEventListener('change', () => {
const c = euroZuCentMitVorzeichen(String(z.einzel.value))
if (c !== null) z.einzel.value = centFeld(c)
})
z.zeile = h('div', { class: 'bk-posten-zeile', 'data-zeile': true },
z.text, z.menge, z.einzel, z.ust,
h('button', {
type: 'button', class: 'kb-btn-outline', 'data-aktion': 'posten-weg', 'aria-label': 'Posten entfernen',
onclick: () => {
zeilen.splice(zeilen.indexOf(z), 1)
z.zeile.remove()
geaendert()
},
}, 'Entfernen'))
zeilen.push(z)
liste.append(z.zeile)
}
for (const p of start) fuegeZeile(p)
const kopf = h('div', { class: 'bk-posten-kopf', 'aria-hidden': 'true' },
h('span', null, 'Beschreibung'), h('span', null, 'Menge'), h('span', null, 'Einzelpreis €'), klein ? null : h('span', null, 'USt'), h('span'))
const el = h('div', { class: 'bk-editor' }, kopf, liste,
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'posten-neu', onclick: () => { fuegeZeile(); geaendert() } }, 'Posten hinzufügen'),
summenEl)
zeichneSumme()
return { el, posten: lies, summe: () => { const p = lies(); return p ? summeLive(p, steuerart) : null } }
}
+241
View File
@@ -0,0 +1,241 @@
/* Buchungskalender: Preis-Editor einer Unterkunft (Staffeln, Preise je Personenzahl, Reinigung, Zusatzpersonen, Rabatt, Steuer, Frist, Stornobedingungen) mit Beispielrechnung.
GET|PUT /api/unterkunft/unterkuenfte/:id/preise => { preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent,
direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }
POST /api/unterkunft/preisvorschau { unterkunftId, von, bis, personen } => { naechte, posten, summen:{ bruttoCent } } (rechnet mit den gespeicherten Preisen).
Zwei Preismodelle, umgeschaltet mit dem Haken "Preis hängt von der Personenzahl ab" (#bk-p-personenmodus, nur bei unterkunft.maxPersonen >= 2):
- aus: ein Preis pro Nacht je Staffel; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" gelten wie bisher.
- an: je Staffel k Euro-Felder (k = min(maxPersonen, 10), "1 Person" ... "k Personen") = Preis pro Nacht fuer die ganze Unterkunft; gesendet wird
staffeln:[{abNaechten, preisProNachtCent: personenCent[0], personenCent}]. Die beiden Personenfelder sind dann ausgeblendet, ihre Werte gehen unveraendert mit.
Beim Laden ist der Haken an, wenn die gespeicherten Staffeln personenCent haben (vorhandene Werte eingesetzt, fehlende leer, ueberzaehlige verworfen).
ctx.nurLesen sperrt alle Felder samt Haken. */
import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, hakenFeld, setzeFehler, zeigeFehler } from './shop-dom.js'
import { enc, heuteBerlin, plusTage, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
// Wortgleich zu docs/funktionen/unterkunft-storno-muster.md, Abschnitt "Stornobedingungen (Muster)" (ohne Markdown-Fettdruck); test/buchungenUi.test.js vergleicht beides.
export const STORNO_MUSTER = [
"1. Stornierung durch den Gast. Eine Stornierung der Buchung ist nur in Textform möglich (E-Mail an [E-Mail-Adresse des Vermieters]) und wird erst mit unserer Bestätigung wirksam; wir stornieren die Buchung in unserem System und senden Ihnen eine Stornorechnung. Maßgeblich für die Frist ist der Eingang Ihrer Nachricht bei uns.",
"",
"2. Kosten der Stornierung. Bei einer Stornierung berechnen wir folgenden Anteil des Gesamtpreises (ohne Endreinigung, soweit der Aufenthalt nicht angetreten wird):",
"- bis [30] Tage vor der Anreise: kostenfrei,",
"- [29] bis [14] Tage vor der Anreise: [30] Prozent,",
"- [13] bis [7] Tage vor der Anreise: [60] Prozent,",
"- ab [6] Tage vor der Anreise und bei Nichtanreise: [90] Prozent.",
"",
"Ihnen bleibt der Nachweis gestattet, dass uns kein oder ein geringerer Schaden entstanden ist. Uns bleibt unser Anspruch unberührt, wenn wir die Unterkunft anderweitig vermieten können: In diesem Fall berechnen wir nur den tatsächlichen Ausfall.",
"",
"3. Erstattung. Haben Sie bereits online bezahlt, erstatten wir den Teil des Preises, der nach Nr. 2 nicht anfällt, auf das Zahlungsmittel, mit dem Sie bezahlt haben. Die Erstattung geht über unser Stripe-Konto; die Gutschrift auf Ihrem Konto kann einige Tage dauern. Haben Sie noch nicht bezahlt, zahlen Sie den nach Nr. 2 anfallenden Betrag vor Ort oder nach Rechnung.",
"",
"4. Verspätete Anreise und vorzeitige Abreise. Reisen Sie später an oder früher ab, besteht kein Anspruch auf Erstattung des Preises für die nicht genutzten Nächte, es sei denn, wir können die Unterkunft anderweitig vermieten.",
"",
"5. Stornierung durch uns. Können wir die Unterkunft aus einem Grund, den wir zu vertreten haben, nicht bereitstellen, stornieren wir die Buchung, bieten nach Möglichkeit eine gleichwertige Unterkunft an und erstatten bereits gezahlte Beträge vollständig.",
"",
"6. Kein Widerrufsrecht. Für die Buchung zu einem bestimmten Zeitraum besteht kein gesetzliches Widerrufsrecht (§ 312g Abs. 2 Nr. 9 BGB). Diese Stornobedingungen gelten stattdessen.",
"",
"7. Reiserücktrittsversicherung. Wir empfehlen Ihnen eine Reiserücktrittsversicherung.",
].join('\n')
export const STORNO_HINWEIS = 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.'
const STANDARD = { staffeln: [{ abNaechten: 1, preisProNachtCent: null }], reinigungCent: 0, personenInklusive: 2, aufpreisPersonNachtCent: 0, direktrabattProzent: 0, steuerart: 'kleinunternehmer', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: '' }
const PERSONEN_MAX = 10
const ganzzahl = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,9}$/.test(t) ? Number(t) : null }
export async function zeichnePreise(ctx, unterkunft) {
const basis = `/api/unterkunft/unterkuenfte/${enc(unterkunft.id)}`
const q = `projectId=${enc(ctx.projectId)}`
const wurzel = h('div', { class: 'bk-preise' }, h('p', { class: 'kb-empty' }, 'Lade…'))
let preise = STANDARD
try {
const daten = await ctx.request(`${basis}/preise?${q}`)
if (daten?.preise) preise = { ...STANDARD, ...daten.preise }
} catch (err) {
wurzel.replaceChildren(h('p', { class: 'sh-fehler', role: 'alert' }, err.message))
return wurzel
}
// Preise je Personenzahl: nur sinnvoll, wenn mehr als eine Person in die Unterkunft passt; k Spalten (1 bis k Personen)
const gaeste = Number.isInteger(unterkunft?.maxPersonen) ? Math.min(unterkunft.maxPersonen, PERSONEN_MAX) : 0
const modusMoeglich = gaeste >= 2
const modusBox = h('input', { type: 'checkbox', id: 'bk-p-personenmodus' })
modusBox.checked = modusMoeglich && Array.isArray(preise.staffeln) && preise.staffeln.some((s) => Array.isArray(s?.personenCent) && s.personenCent.length > 0)
const personenText = (n) => `${n} ${n === 1 ? 'Person' : 'Personen'}`
const zeilenEl = h('div', { class: 'bk-staffeln' })
const zeile = (s) => {
const ab = h('input', { type: 'text', inputmode: 'numeric', 'aria-label': 'Ab Nächten', 'data-staffel': 'ab', autocomplete: 'off' })
ab.value = String(s.abNaechten ?? '')
const preis = euroEingabe({ cent: s.preisProNachtCent, label: 'Preis pro Nacht in Euro' })
preis.dataset.staffel = 'preis'
const spalten = Array.from({ length: modusMoeglich ? gaeste : 0 }, (_, i) => {
const eingabe = euroEingabe({ cent: Array.isArray(s.personenCent) ? s.personenCent[i] : null, label: `Preis pro Nacht in Euro für ${personenText(i + 1)}` })
eingabe.dataset.staffel = 'person'
eingabe.dataset.personen = String(i + 1)
return h('label', { class: 'bk-personpreis' }, h('span', null, personenText(i + 1)), eingabe)
})
const el = h('div', { class: 'bk-staffel', 'data-staffel-zeile': true }, 'ab ', ab, ' Nächten: ',
h('span', { class: 'bk-preis-einzeln' }, preis, ' € pro Nacht '),
h('span', { class: 'bk-personenpreise', 'data-personenpreise': true }, spalten),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-weg', 'aria-label': 'Staffel entfernen', onclick: () => { if (zeilenEl.children.length > 1) { el.remove(); halteErste() } } }, '−'))
return el
}
const halteErste = () => { const ab = zeilenEl.querySelector('[data-staffel="ab"]'); if (ab) { ab.value = '1'; ab.readOnly = true; ab.setAttribute('readonly', '') } }
zeilenEl.append(...(preise.staffeln?.length ? preise.staffeln : STANDARD.staffeln).map(zeile))
halteErste()
const reinigung = euroEingabe({ cent: preise.reinigungCent, id: 'bk-p-reinigung' })
const inklusive = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-inklusive' }); inklusive.value = String(preise.personenInklusive ?? 2)
const aufpreis = euroEingabe({ cent: preise.aufpreisPersonNachtCent, id: 'bk-p-aufpreis' })
const rabatt = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-rabatt' }); rabatt.value = String(preise.direktrabattProzent ?? 0)
const steuerart = h('select', { id: 'bk-p-steuerart' }, [['kleinunternehmer', 'Kleinunternehmer (keine Umsatzsteuer)'], ['regel', 'Regelbesteuerung (mit Umsatzsteuer)']].map(([w, t]) => h('option', { value: w }, t)))
steuerart.value = preise.steuerart === 'regel' ? 'regel' : 'kleinunternehmer'
const ustUe = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-ust-ue' }); ustUe.value = String(preise.ustUebernachtung ?? 7)
const ustRe = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-ust-re' }); ustRe.value = String(preise.ustReinigung ?? 7)
const frist = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-frist' }); frist.value = String(preise.fristTage ?? 3)
const storno = h('textarea', { id: 'bk-p-storno', rows: '6', maxlength: '5000' }); storno.value = zeichen(preise.stornobedingungen)
const ustFelder = h('div', { class: 'bk-ust' }, feld('ustUebernachtung', 'USt Übernachtung in %', ustUe), feld('ustReinigung', 'USt Endreinigung in %', ustRe))
const zeigeUst = () => { ustFelder.hidden = steuerart.value !== 'regel' }
steuerart.addEventListener('change', zeigeUst)
zeigeUst()
const vorschauText = h('p', { class: 'bk-vorschau', 'data-vorschau': true, role: 'status' })
const naechteEl = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-vn' }); naechteEl.value = '7'
const personenEl = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-p-vp' }); personenEl.value = '2'
async function vorschau() {
const naechte = ganzzahl(naechteEl)
const personen = ganzzahl(personenEl)
if (!naechte || !personen) { vorschauText.textContent = 'Gib Nächte und Personen als ganze Zahlen an.'; return }
const von = plusTage(heuteBerlin(), 60)
try {
const antwort = await ctx.request(`/api/unterkunft/preisvorschau?${q}`, { method: 'POST', body: { unterkunftId: unterkunft.id, von, bis: plusTage(von, naechte), personen } })
if (!ctx.aktuell()) return
vorschauText.textContent = Number.isInteger(antwort?.summen?.bruttoCent)
? `Beispiel: ${antwort.naechte ?? naechte} ${(antwort.naechte ?? naechte) === 1 ? 'Nacht' : 'Nächte'}, ${personen} ${personen === 1 ? 'Person' : 'Personen'} = ${euroText(antwort.summen.bruttoCent)}`
: 'Für diese Angaben gibt es noch keine Beispielrechnung.'
} catch (err) {
vorschauText.textContent = Object.values(err.body?.felder || {})[0] || 'Die Beispielrechnung klappt erst, wenn die Preise gespeichert sind.'
}
}
for (const el of [naechteEl, personenEl]) el.addEventListener('change', vorschau)
const modusFeld = hakenFeld('personenmodus', modusBox, 'Preis hängt von der Personenzahl ab', 'Zum Beispiel 55 € für 1 Person, 70 € für 2 Personen und 82 € für 3 Personen pro Nacht.')
modusFeld.hidden = !modusMoeglich
modusBox.hidden = !modusMoeglich
const modusHinweis = h('p', { class: 'sh-hinweis', 'data-hinweis': 'personenpreise' }, 'Preis pro Nacht für die ganze Unterkunft, je nach Anzahl der Gäste.')
const inklusiveFeld = feld('personenInklusive', 'Personen im Preis inklusive', inklusive)
const aufpreisFeld = feld('aufpreisPersonNachtCent', 'Aufpreis je weiterer Person und Nacht in Euro', aufpreis)
/** Haken an: je Staffel die Spalten je Personenzahl statt des einen Preises; die zwei Personenfelder des alten Modells verschwinden (ihre Werte gehen unveraendert mit). */
const zeigeModus = () => {
const an = modusMoeglich && modusBox.checked
for (const el of zeilenEl.querySelectorAll('.bk-preis-einzeln')) el.hidden = an
for (const el of zeilenEl.querySelectorAll('.bk-personenpreise')) el.hidden = !an
modusHinweis.hidden = !an
inklusiveFeld.hidden = an
aufpreisFeld.hidden = an
}
modusBox.addEventListener('change', zeigeModus)
const staffelDazu = () => { zeilenEl.append(zeile({ abNaechten: null, preisProNachtCent: null })); zeigeModus() }
const form = h('form', { novalidate: true, class: 'bk-preisform', onsubmit: (ev) => ev.preventDefault() },
h('h4', null, 'Preis pro Nacht (Staffeln)'),
h('p', { class: 'sh-hinweis' }, 'Je länger der Aufenthalt, desto günstiger die Nacht: Die passende Staffel gilt für alle Nächte der Buchung.'),
modusFeld, modusHinweis,
zeilenEl, h('div', { class: 'sh-fehler', 'data-fehler': 'staffeln', role: 'alert' }),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-dazu', onclick: staffelDazu }, '+ Staffel'),
feld('reinigungCent', 'Endreinigung in Euro', reinigung),
inklusiveFeld,
aufpreisFeld,
feld('direktrabattProzent', 'Direktbuchungsrabatt in %', rabatt, 'Ganze Zahl von 0 bis 50. Gilt für Übernachtung und Zusatzpersonen, nicht für die Reinigung.'),
feld('steuerart', 'Steuer', steuerart), ustFelder,
feld('fristTage', 'Frist für die Buchung nach Freigabe in Tagen', frist, '1 bis 14 Tage.'),
feld('stornobedingungen', 'Stornobedingungen', storno, 'Pflicht, bevor du den Buchungskalender freischaltest. Sie stehen auf der Buchungsseite.'),
ctx.nurLesen ? null : h('div', { class: 'bk-aktionen' },
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'storno-muster', onclick: () => { storno.value = STORNO_MUSTER } }, 'Mustertext übernehmen'),
h('p', { class: 'sh-hinweis', 'data-hinweis': 'storno-muster' }, STORNO_HINWEIS)),
allgemeinerFehler(),
h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'preise-speichern', onclick: speichern }, 'Preise speichern')))
zeigeModus()
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true
// Ein Feldfehler (z. B. Platzhalter in den Stornobedingungen) verschwindet, sobald im Feld getippt wird
for (const el of form.querySelectorAll('[data-feld]')) {
el.addEventListener('input', () => {
const name = el.getAttribute('data-feld')
const ziel = form.querySelector(`[data-fehler="${CSS.escape(name)}"]`)
if (ziel) ziel.textContent = ''
el.removeAttribute('aria-invalid')
})
}
async function speichern(ev) {
const knopf = ev.currentTarget
const fehler = {}
const staffeln = []
let staffelFehler = ''
const personenModus = modusMoeglich && modusBox.checked
for (const z of zeilenEl.children) {
const ab = ganzzahl(z.querySelector('[data-staffel="ab"]'))
if (personenModus) {
const spalten = [...z.querySelectorAll('[data-staffel="person"]')].map((el) => el.inCent())
if (ab === null || ab < 1 || spalten.some((p) => p === null)) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und für jede Personenzahl einen Preis an.'
else if (spalten.some((p) => p < 100 || p > 100000)) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
else staffeln.push({ abNaechten: ab, preisProNachtCent: spalten[0], personenCent: spalten })
continue
}
const p = z.querySelector('[data-staffel="preis"]').inCent()
if (ab === null || ab < 1 || p === null) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und einen Preis an.'
else if (p < 100 || p > 100000) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
else staffeln.push({ abNaechten: ab, preisProNachtCent: p })
}
if (!staffelFehler) {
if (staffeln.length < 1 || staffeln.length > 6) staffelFehler = 'Du kannst 1 bis 6 Staffeln anlegen.'
else if (staffeln[0].abNaechten !== 1) staffelFehler = 'Die erste Staffel beginnt bei 1 Nacht.'
else if (staffeln.some((s, i) => i > 0 && s.abNaechten <= staffeln[i - 1].abNaechten)) staffelFehler = 'Die Nächte der Staffeln müssen von oben nach unten größer werden, keine Zahl doppelt.'
}
if (staffelFehler) fehler.staffeln = staffelFehler
const zahl = (name, steuer, text, min, max) => { const n = ganzzahl(steuer); if (n === null || n < min || n > max) { fehler[name] = text; return null } return n }
const reinigungCent = reinigung.istLeer() ? 0 : reinigung.inCent()
if (reinigungCent === null) fehler.reinigungCent = 'Bitte gib einen Betrag wie 60,00 an.'
// Im Personen-Modus sind "inklusive" und "Aufpreis" ausgeblendet: ihr Wert geht unveraendert mit; Unbrauchbares im unsichtbaren Feld bleibt ohne Fehlermeldung beim gespeicherten Wert
let aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
if (personenModus && (aufpreisCent === null || aufpreisCent > 100000)) aufpreisCent = Number.isInteger(preise.aufpreisPersonNachtCent) ? preise.aufpreisPersonNachtCent : 0
if (aufpreisCent === null) fehler.aufpreisPersonNachtCent = 'Bitte gib einen Betrag wie 15,00 an.'
const inklusiveAlt = Number.isInteger(preise.personenInklusive) && preise.personenInklusive >= 1 && preise.personenInklusive <= 50 ? preise.personenInklusive : 2
const inklusiveNeu = ganzzahl(inklusive)
const inklusiveWert = personenModus
? (inklusiveNeu !== null && inklusiveNeu >= 1 && inklusiveNeu <= 50 ? inklusiveNeu : inklusiveAlt)
: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000)
const regel = steuerart.value === 'regel'
const ustWert = (name, steuer, alt) => {
if (!regel) return ganzzahl(steuer) ?? alt // andere Steuerart: gespeicherten Wert unveraendert zurueckschicken
const n = ganzzahl(steuer)
if (![0, 7, 19].includes(n)) { fehler[name] = 'Erlaubt sind 0, 7 oder 19 Prozent.'; return null }
return n
}
const storn = String(storno.value).trim()
if (storn.length < 20 || storn.length > 5000) fehler.stornobedingungen = 'Die Stornobedingungen brauchen 20 bis 5000 Zeichen.'
if (!fehler.stornobedingungen && /\[[^\]]*\]/.test(storn)) fehler.stornobedingungen = 'Bitte ersetze die Platzhalter in eckigen Klammern durch deine Werte.'
const body = {
staffeln, reinigungCent, aufpreisPersonNachtCent: aufpreisCent,
personenInklusive: inklusiveWert,
direktrabattProzent: zahl('direktrabattProzent', rabatt, 'Bitte gib eine ganze Zahl von 0 bis 50 an.', 0, 50),
steuerart: steuerart.value,
ustUebernachtung: ustWert('ustUebernachtung', ustUe, preise.ustUebernachtung ?? 7),
ustReinigung: ustWert('ustReinigung', ustRe, preise.ustReinigung ?? 7),
fristTage: zahl('fristTage', frist, 'Bitte gib eine ganze Zahl von 1 bis 14 an.', 1, 14),
stornobedingungen: storn,
}
if (Object.keys(fehler).length) return zeigeFehler(form, { message: '', fehler })
knopf.disabled = true
try {
await ctx.request(`${basis}/preise?${q}`, { method: 'PUT', body })
if (!ctx.aktuell()) return
setzeFehler(form, {})
ctx.zeigeMeldung('Die Preise sind gespeichert.', 'ok')
await vorschau()
} catch (err) {
zeigeApiFehler(form, err)
}
knopf.disabled = false
}
wurzel.replaceChildren(form,
abschnitt('Beispielrechnung', 'So rechnet dein Gast. Sie nutzt die gespeicherten Preise.',
h('div', { class: 'bk-vorschaufelder' }, feld('naechte', 'Nächte', naechteEl), feld('personen', 'Personen', personenEl)), vorschauText))
vorschau()
return wurzel
}
+302
View File
@@ -0,0 +1,302 @@
/* Buchungskalender, Reiter „Anfragen“: Filter (offen | gebucht | alle), Liste, Detail.
API (docs/funktionen/unterkunft-api.md, alle mit ?projectId=):
GET /api/unterkunft/anfragen?filter= => { anfragen:[{ id, nummer, von, bis, naechte?, gast:{name}, status, summen:{bruttoCent}, unterkunftName? }] }
GET /api/unterkunft/anfragen/:id => { anfrage:{ id, nummer, von, bis, personen, status, gast:{name,firma,email,telefon,nachricht?}, posten, summen,
steuerart, zahlart?, bezahltVia?, erstattetCent?, rechnungId?, rechnungNummer?, fristAt?, unterkunftName? }, vorschlag:{ naechte, posten, summen } }
POST …/freigeben {posten,fristTage,nachricht} (=> { buchungsLink? }), …/ablehnen {text}, …/bezahlt, …/stornieren {grund,erstattungCent}.
Fremdtext (Gast, Nachricht) nur ueber textContent. render(panel, ctx) wie shop-tab-*.js. */
import { abschnitt, allgemeinerFehler, centZuEuro, euroZuCent, fehlerKarte, feld, frage, h, zeigeFehler, datumText } from './shop-dom.js'
import { fehlerFuerFormular } from './buchungen-fehler.js'
import { baueEditor, euroMitVorzeichen } from './buchungen-posten.js'
export const STATUS = {
neu: ['Neu', 'wait'],
freigegeben: ['Freigegeben – wartet auf Buchung', 'wait'],
gebucht: ['Gebucht', 'ok'],
bezahlt: ['Bezahlt', 'ok'],
abgelehnt: ['Abgelehnt', 'bad'],
abgelaufen: ['Abgelaufen', 'off'],
storniert: ['Storniert', 'bad'],
}
const FILTER = [['offen', 'Offen'], ['gebucht', 'Gebucht'], ['alle', 'Alle']]
const FEHLER_LADEN = 'Das konnte nicht geladen werden. Bitte versuch es noch einmal.'
const BELEGT = 'Der Zeitraum ist inzwischen belegt.'
const zeichen = (x) => (typeof x === 'string' ? x : '')
const enc = encodeURIComponent
const pille = (status) => {
const [text, art] = STATUS[status] || [zeichen(status) || 'Unbekannt', 'off']
return h('span', { class: `sh-pille sh-pille-${art}`, 'data-status': status }, text)
}
/** '2026-11-12','2026-11-30' ⇒ '12.–30.11.2026'; über Monats-/Jahresgrenze '28.10.–03.11.2026' bzw. '28.12.2026–03.01.2027'. */
export function zeitraumText(von, bis) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/
const a = m.exec(von || '')
const b = m.exec(bis || '')
if (!a || !b) return ''
if (a[1] !== b[1]) return `${a[3]}.${a[2]}.${a[1]}–${b[3]}.${b[2]}.${b[1]}`
if (a[2] !== b[2]) return `${a[3]}.${a[2]}.–${b[3]}.${b[2]}.${b[1]}`
return `${a[3]}.–${b[3]}.${b[2]}.${b[1]}`
}
export function naechteText(von, bis) {
const ms = (t) => Date.UTC(...t.split('-').map((x, i) => (i === 1 ? Number(x) - 1 : Number(x))))
const n = Math.round((ms(bis) - ms(von)) / 86400000)
return Number.isFinite(n) ? `${n} ${n === 1 ? 'Nacht' : 'Nächte'}` : ''
}
const gastName = (gast) => (typeof gast === 'string' ? gast : zeichen(gast?.name))
export async function render(panel, ctx) {
const st = { filter: 'offen', lauf: 0 }
const kopf = h('div', { class: 'bk-filter' }, h('label', { for: 'bk-a-filter' }, 'Anzeigen'),
h('select', { id: 'bk-a-filter', 'data-aktion': 'filter' }, FILTER.map(([wert, text]) => h('option', { value: wert }, text))))
const inhalt = h('div', { class: 'bk-anfragen' })
panel.replaceChildren(kopf, inhalt)
const select = kopf.querySelector('select')
select.value = st.filter
select.addEventListener('change', () => { st.filter = select.value; zeigeListe() })
await zeigeListe()
async function zeigeListe() {
const lauf = ++st.lauf
const gueltig = () => st.lauf === lauf && ctx.aktuell()
inhalt.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let daten
try {
daten = await ctx.request(`/api/unterkunft/anfragen?projectId=${enc(ctx.projectId)}&filter=${enc(st.filter)}`)
} catch (err) {
if (gueltig()) inhalt.replaceChildren(fehlerKarte(err.message || FEHLER_LADEN, zeigeListe))
return
}
if (!gueltig()) return
const zeilen = Array.isArray(daten?.anfragen) ? daten.anfragen : []
if (!zeilen.length) {
inhalt.replaceChildren(h('div', { class: 'kb-panel kb-empty', 'data-leer': true }, st.filter === 'offen' ? 'Keine offenen Anfragen.' : 'Keine Anfragen.'))
return
}
inhalt.replaceChildren(h('ul', { class: 'bk-liste' }, zeilen.map((a) => h('li', null, h('button', {
type: 'button', class: 'bk-zeile', 'data-anfrage': a.id, onclick: () => zeigeDetail(a.id),
},
h('strong', { class: 'bk-nummer' }, zeichen(a.nummer)),
h('span', { class: 'bk-zeitraum' }, `${zeitraumText(a.von, a.bis)} · ${naechteText(a.von, a.bis)}`),
h('span', { class: 'bk-gast' }, gastName(a.gast)),
pille(a.status),
h('span', { class: 'bk-betrag' }, Number.isInteger(a.summen?.bruttoCent) ? euroMitVorzeichen(a.summen.bruttoCent) : ''))))))
}
async function zeigeDetail(id) {
const lauf = ++st.lauf
const gueltig = () => st.lauf === lauf && ctx.aktuell()
inhalt.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let daten
try {
daten = await ctx.request(`/api/unterkunft/anfragen/${enc(id)}?projectId=${enc(ctx.projectId)}`)
} catch (err) {
if (gueltig()) inhalt.replaceChildren(zurueck(), fehlerKarte(err.message || FEHLER_LADEN, () => zeigeDetail(id)))
return
}
if (!gueltig()) return
if (!daten?.anfrage) {
inhalt.replaceChildren(zurueck(), fehlerKarte(FEHLER_LADEN, () => zeigeDetail(id)))
return
}
inhalt.replaceChildren(zurueck(), baueDetail(daten.anfrage, daten.vorschlag))
}
const zurueck = () => h('button', { type: 'button', class: 'kb-btn-outline bk-zurueck', 'data-aktion': 'zurueck', onclick: () => zeigeListe() }, '‹ Zur Liste')
function baueDetail(a, vorschlag) {
const gast = a.gast && typeof a.gast === 'object' ? a.gast : { name: zeichen(a.gast) }
const kopfZeile = h('div', { class: 'bk-detail-kopf' },
h('h3', null, `Anfrage ${zeichen(a.nummer)}`), pille(a.status))
const daten = h('dl', { class: 'bk-daten' },
...[
['Unterkunft', zeichen(a.unterkunftName)],
['Zeitraum', `${zeitraumText(a.von, a.bis)} · ${naechteText(a.von, a.bis)}`],
['Personen', Number.isInteger(a.personen) ? String(a.personen) : ''],
['Gast', gastName(gast)],
['Firma', zeichen(gast.firma)],
['E-Mail', zeichen(gast.email)],
['Telefon', zeichen(gast.telefon)],
['Nachricht', zeichen(gast.nachricht)],
['Frist', a.fristAt && a.status === 'freigegeben' ? datumText(a.fristAt) : ''],
].filter(([, w]) => w).flatMap(([n, w]) => [h('dt', null, n), h('dd', null, w)]))
const karte = h('section', { class: 'kb-panel sh-abschnitt bk-detail', 'data-detail': a.id }, kopfZeile, daten)
if (a.status === 'neu') karte.append(neuBereich(a, vorschlag))
else karte.append(festerBereich(a))
return karte
}
/* ---------------------------------------------- Status neu: Freigeben / Ablehnen --- */
function neuBereich(a, vorschlag) {
const steuerart = a.steuerart || vorschlag?.steuerart || ctx.status?.steuerart || 'regelbesteuert'
const startPosten = Array.isArray(vorschlag?.posten) ? vorschlag.posten : []
const editor = baueEditor(startPosten, steuerart)
const frist = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-a-frist' })
frist.value = '7'
const nachricht = h('textarea', { id: 'bk-a-nachricht', rows: '3', maxlength: '2000' })
const form = h('form', { class: 'bk-freigabe', novalidate: true, onsubmit: (ev) => ev.preventDefault() },
h('h4', null, 'Angebot für den Gast'),
editor.el,
h('p', { class: 'sh-fehler', 'data-fehler': 'posten', role: 'alert' }),
feld('fristTage', 'Frist zum Buchen (Tage)', frist, 'So lange ist der Zeitraum für den Gast reserviert.'),
feld('nachricht', 'Nachricht an den Gast (optional)', nachricht),
allgemeinerFehler())
const freigeben = h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'freigeben',
onclick: () => sendeFreigabe(a, editor, frist, nachricht, form, freigeben),
}, 'Freigeben')
const ablehnen = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'ablehnen', onclick: () => ablehnDialog(a, ablehnen) }, 'Ablehnen')
return h('div', null, form, h('div', { class: 'bk-aktionen' }, freigeben, ablehnen), h('div', { class: 'bk-dialog' }))
}
async function sendeFreigabe(a, editor, frist, nachricht, form, knopf) {
const posten = editor.posten()
const tage = Number(String(frist.value).trim())
if (!posten) return zeigeFehler(form, { message: '', fehler: { posten: 'Bitte prüfe Menge und Preis der Posten.' } })
if (!Number.isInteger(tage) || tage < 1) return zeigeFehler(form, { message: '', fehler: { fristTage: 'Bitte gib die Frist in ganzen Tagen an.' } })
knopf.disabled = true
try {
const antwort = await ctx.request(`/api/unterkunft/anfragen/${enc(a.id)}/freigeben?projectId=${enc(ctx.projectId)}`, {
method: 'POST', body: { posten, fristTage: tage, nachricht: String(nachricht.value).trim() },
})
if (!ctx.aktuell()) return
ctx.zeigeMeldung('Freigegeben. Der Buchungslink wurde an deinen Gast geschickt – falls die Mail nicht ankommt, kannst du ihn hier kopieren.', 'ok')
zeigeLink(antwort?.buchungsLink, a)
} catch (err) {
knopf.disabled = false
// 409 heisst nicht immer belegt (auch zustimmung_fehlt, status_ungueltig): nur der Code zeitraum_belegt bekommt den Belegt-Text
if (err.status === 409 && err.fehler === 'zeitraum_belegt') zeigeFehler(form, { message: BELEGT, fehler: { _: BELEGT } })
else zeigeFehler(form, fehlerFuerFormular(err))
}
}
/** Nach „Freigeben“: der Buchungslink zum Kopieren, falls die Mail nicht ankommt (nur wenn die Antwort ihn enthaelt). */
function zeigeLink(link, a) {
const box = h('section', { class: 'kb-panel sh-abschnitt bk-link', 'data-freigegeben': a.id },
h('h3', null, `Anfrage ${zeichen(a.nummer)} ist freigegeben`),
h('p', null, 'Der Link wurde per Mail verschickt. Falls er nicht ankommt, kopiere ihn hier und schick ihn selbst.'))
if (typeof link === 'string' && link) {
const meldung = h('p', { class: 'sh-hinweis', role: 'status' })
box.append(
h('input', { type: 'text', readonly: true, value: link, 'aria-label': 'Buchungslink', 'data-link': true }),
h('button', {
type: 'button', class: 'kb-btn-outline', 'data-aktion': 'link-kopieren',
onclick: async () => {
try {
await navigator.clipboard.writeText(link)
meldung.textContent = 'Link kopiert.'
} catch {
meldung.textContent = 'Kopieren hat nicht geklappt. Markiere den Link und kopiere ihn von Hand.'
}
},
}, 'Link kopieren'),
meldung)
}
box.append(h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueck', onclick: () => zeigeListe() }, 'Zur Liste'))
inhalt.replaceChildren(box)
}
function ablehnDialog(a, ausloeser) {
const dialogEl = inhalt.querySelector('.bk-dialog')
const text = h('textarea', { id: 'bk-a-ablehn', rows: '3', maxlength: '1000' })
const box = frage('Anfrage ablehnen', h('p', { class: 'sh-hinweis' }, 'Der Gast bekommt deinen Text per Mail.'), feld('text', 'Nachricht an den Gast', text), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'ablehnen-senden',
onclick: async (ev) => {
ev.currentTarget.disabled = true
try {
await ctx.request(`/api/unterkunft/anfragen/${enc(a.id)}/ablehnen?projectId=${enc(ctx.projectId)}`, { method: 'POST', body: { text: String(text.value).trim() } })
if (!ctx.aktuell()) return
ctx.zeigeMeldung('Anfrage abgelehnt.', 'ok')
await zeigeListe()
} catch (err) {
ev.currentTarget.disabled = false
zeigeFehler(box, fehlerFuerFormular(err))
}
},
}, 'Ablehnen'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'abbrechen', onclick: () => { dialogEl.replaceChildren(); ausloeser.focus() } }, 'Abbrechen')))
dialogEl.replaceChildren(box)
text.focus()
}
/* ---------------------------------------------- weitere Status: Posten, Rechnung, Bezahlt, Storno --- */
function festerBereich(a) {
const posten = Array.isArray(a.posten) ? a.posten : []
const teile = []
if (posten.length) {
teile.push(h('table', { class: 'bk-tabelle' }, h('tbody', null, posten.map((p) => h('tr', null,
h('td', null, zeichen(p.text)), h('td', null, `${p.menge} ×`), h('td', null, euroMitVorzeichen(p.einzelCent)))))))
}
if (Number.isInteger(a.summen?.bruttoCent)) teile.push(h('p', { class: 'bk-gesamt' }, h('strong', null, `Gesamt ${euroMitVorzeichen(a.summen.bruttoCent)}`)))
const aktionen = h('div', { class: 'bk-aktionen' })
const dialogEl = h('div', { class: 'bk-dialog' })
const aktiv = a.status === 'gebucht' || a.status === 'bezahlt'
if (aktiv && a.rechnungId) {
teile.push(h('p', null, h('a', {
href: `/api/unterkunft/rechnungen/${enc(a.rechnungId)}.pdf?projectId=${enc(ctx.projectId)}`, target: '_blank', rel: 'noopener', 'data-aktion': 'rechnung',
}, `Rechnung ${zeichen(a.rechnungNummer)}`.trim() + ' (PDF)')))
}
if (a.status === 'gebucht') {
aktionen.append(h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'bezahlt', onclick: (ev) => markiereBezahlt(a, ev.currentTarget) }, 'Als bezahlt (vor Ort) markieren'))
}
if (aktiv) {
const stornoKnopf = h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'stornieren', onclick: () => stornoDialog(a, dialogEl, stornoKnopf) }, 'Stornieren')
aktionen.append(stornoKnopf)
}
return h('div', null, ...teile, aktionen.childNodes.length ? aktionen : null, dialogEl)
}
async function markiereBezahlt(a, knopf) {
knopf.disabled = true
try {
await ctx.request(`/api/unterkunft/anfragen/${enc(a.id)}/bezahlt?projectId=${enc(ctx.projectId)}`, { method: 'POST' })
if (!ctx.aktuell()) return
ctx.zeigeMeldung('Als bezahlt markiert.', 'ok')
await zeigeDetail(a.id)
} catch (err) {
knopf.disabled = false
ctx.zeigeMeldung(err.message, 'fehler')
}
}
function stornoDialog(a, dialogEl, ausloeser) {
const online = a.bezahltVia === 'stripe'
const grund = h('textarea', { id: 'bk-a-grund', rows: '3', maxlength: '500' })
const betrag = h('input', { type: 'text', inputmode: 'decimal', id: 'bk-a-erstattung', autocomplete: 'off' })
betrag.value = Number.isInteger(a.summen?.bruttoCent) ? centZuEuro(a.summen.bruttoCent - (a.erstattetCent || 0)) : ''
const box = frage('Buchung stornieren', h('p', { class: 'sh-hinweis' }, 'Der Gast bekommt eine Stornomail und eine Storno-Rechnung.'),
feld('grund', 'Grund', grund),
online ? feld('erstattungCent', 'Erstattung in Euro', betrag, 'Der Betrag geht an die Karte des Gastes zurück. 0 = keine Erstattung.') : null,
allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'storno-senden',
onclick: async (ev) => {
const knopf = ev.currentTarget
let erstattungCent = 0
if (online) {
erstattungCent = String(betrag.value).trim() === '' ? 0 : euroZuCent(String(betrag.value))
if (erstattungCent === null) return zeigeFehler(box, { message: '', fehler: { erstattungCent: 'Bitte gib einen Betrag wie 120,00 an.' } })
}
knopf.disabled = true
try {
await ctx.request(`/api/unterkunft/anfragen/${enc(a.id)}/stornieren?projectId=${enc(ctx.projectId)}`, { method: 'POST', body: { grund: String(grund.value).trim(), erstattungCent } })
if (!ctx.aktuell()) return
ctx.zeigeMeldung('Die Buchung ist storniert.', 'ok')
await zeigeDetail(a.id)
} catch (err) {
knopf.disabled = false
if (err.fehler === 'erstattung_fehlgeschlagen' || err.body?.fehler === 'erstattung_fehlgeschlagen') {
zeigeFehler(box, { message: 'Die Buchung ist storniert, aber die Erstattung ist fehlgeschlagen. Bitte versuch die Erstattung gleich noch einmal.', fehler: {} })
} else zeigeFehler(box, fehlerFuerFormular(err))
}
},
}, 'Stornieren'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'abbrechen', onclick: () => { dialogEl.replaceChildren(); ausloeser.focus() } }, 'Abbrechen')))
dialogEl.replaceChildren(box)
grund.focus()
}
}
+189
View File
@@ -0,0 +1,189 @@
/* Buchungskalender, Reiter „Einstellungen“: Firmendaten, Steuernummer/USt-IdNr., Rechnungspraefix, Auftrag zur Rechnungsstellung,
Bankverbindung, Akzentfarbe, Stripe-Konto und Online-Zahlung.
API (docs/funktionen/unterkunft-api.md, ?projectId=): GET /einstellungen => { einstellungen:{ firma, inhaber, strasse, plz, ort, email, telefon,
steuernummer, ustId, iban, bic, bank, rechnungsPraefix, akzentfarbe, onlineZahlung, rechnungAuftrag },
stripe:{ status:'keins'|'onboarding'|'aktiv', vorhandenesShopKonto:boolean }, rechnungenVorhanden:boolean };
PUT /einstellungen mit denselben Feldern (ohne Wrapper; rechnungAuftrag:true setzt rechnungAuftragAt, false widerruft);
POST /stripe/verbinden => { url } (Onboarding bei Stripe), POST /stripe/vorhandenes-konto und /stripe/aktualisieren => { stripe }.
Geprueft wird auf dem Server; Feldfehler stehen am Feld. Text nur ueber textContent (h). render(panel, ctx) wie shop-tab-*.js. */
import { abschnitt, allgemeinerFehler, feld, frage, h, hakenFeld, setzeFehler } from './shop-dom.js'
import { enc, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
export const AUFTRAG_TEXT = 'Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG).'
export const AUFTRAG_WIDERRUF = 'Ohne diese Zustimmung können keine Buchungen bestätigt werden. Zustimmung wirklich zurückziehen?'
export const AUFTRAG_FEHLT = 'Ohne diesen Auftrag kannst du Buchungen nicht bestätigen, weil wir keine Rechnungen für dich ausstellen dürfen.'
const STRIPE_FEHLER = 'Stripe hat keine gültige Adresse geliefert. Bitte versuch es gleich noch einmal.'
const PFLICHT = { firma: 'Bitte gib den Namen deiner Firma oder Unterkunft an.', inhaber: 'Bitte gib den Namen des Inhabers an.', strasse: 'Bitte gib Straße und Hausnummer an.', plz: 'Bitte gib die Postleitzahl an.', ort: 'Bitte gib den Ort an.', email: 'Bitte gib eine E-Mail-Adresse an.' }
const textfeld = (optionen = {}) => h('input', { type: 'text', autocomplete: 'off', ...optionen })
const istHttps = (url) => { try { return new URL(url).protocol === 'https:' } catch { return false } }
export async function render(panel, ctx = {}) {
const projectId = ctx.projectId
const q = `projectId=${enc(projectId)}`
const nurLesen = ctx.nurLesen === true
const antwort = await ctx.request(`/api/unterkunft/einstellungen?${q}`)
const e = antwort?.einstellungen && typeof antwort.einstellungen === 'object' ? antwort.einstellungen : {}
const stripe = antwort?.stripe || ctx.status?.stripe || { status: 'keins' }
const rechnungenVorhanden = (antwort?.rechnungenVorhanden ?? ctx.status?.rechnungenVorhanden) === true
const melde = (text, art) => { if (typeof ctx.zeigeMeldung === 'function') ctx.zeigeMeldung(text, art) }
const neu = () => render(panel, ctx).catch((err) => panel.replaceChildren(h('p', { class: 'sh-fehler', role: 'alert' }, err.message)))
/* ----------------------------------------------------------------- Felder --- */
const firma = textfeld({ maxlength: 120, autocomplete: 'organization' }); firma.value = zeichen(e.firma)
const inhaber = textfeld({ maxlength: 120 }); inhaber.value = zeichen(e.inhaber)
const strasse = textfeld({ maxlength: 120, autocomplete: 'street-address' }); strasse.value = zeichen(e.strasse)
const plz = textfeld({ maxlength: 10, inputmode: 'numeric', autocomplete: 'postal-code' }); plz.value = zeichen(e.plz)
const ort = textfeld({ maxlength: 80, autocomplete: 'address-level2' }); ort.value = zeichen(e.ort)
const email = h('input', { type: 'email', maxlength: 254, autocomplete: 'email' }); email.value = zeichen(e.email)
const telefon = h('input', { type: 'tel', maxlength: 40, autocomplete: 'tel' }); telefon.value = zeichen(e.telefon)
const steuernummer = textfeld({ maxlength: 30 }); steuernummer.value = zeichen(e.steuernummer)
const ustId = textfeld({ maxlength: 20, placeholder: 'DE123456789' }); ustId.value = zeichen(e.ustId)
const praefix = textfeld({ maxlength: 8, placeholder: 'RE' }); praefix.value = zeichen(e.rechnungsPraefix) || 'RE'
if (rechnungenVorhanden) { praefix.disabled = true; praefix.setAttribute('aria-disabled', 'true') }
const auftrag = h('input', { type: 'checkbox' }); auftrag.checked = e.rechnungAuftrag === true
const iban = textfeld({ maxlength: 34 }); iban.value = zeichen(e.iban)
const bic = textfeld({ maxlength: 11 }); bic.value = zeichen(e.bic)
const bank = textfeld({ maxlength: 80 }); bank.value = zeichen(e.bank)
const akzent = h('input', { type: 'color' }); akzent.value = /^#[0-9a-fA-F]{6}$/.test(zeichen(e.akzentfarbe)) ? e.akzentfarbe : '#111111'
const online = h('input', { type: 'checkbox' }); online.checked = e.onlineZahlung === true && stripe.status === 'aktiv'
const auftragHinweis = h('div', { class: 'sh-box sh-box-warn', role: 'note', 'data-hinweis': 'auftrag' }, AUFTRAG_FEHLT)
const zeigeAuftrag = () => { auftragHinweis.hidden = auftrag.checked }
// Widerruf einer gespeicherten Zustimmung nur nach Rueckfrage in der Seite
const widerrufPlatz = h('div', { 'data-widerruf-platz': '' })
auftrag.addEventListener('change', () => {
if (!auftrag.checked && e.rechnungAuftrag === true && !widerrufPlatz.firstChild) {
auftrag.checked = true
const zu = () => { widerrufPlatz.replaceChildren(); auftrag.focus() }
const box = frage('Zustimmung zurückziehen', h('p', null, AUFTRAG_WIDERRUF),
h('div', { class: 'sh-aktionen' },
h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'widerruf-ja', onclick: () => { auftrag.checked = false; zeigeAuftrag(); zu() } }, 'Ja, zurückziehen'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'widerruf-nein', onclick: zu }, 'Abbrechen')))
widerrufPlatz.replaceChildren(box)
box.focus()
}
zeigeAuftrag()
})
zeigeAuftrag()
/* ------------------------------------------------------------------ Stripe --- */
let beschaeftigt = false
async function stripeAktion(pfad, { weiter = false } = {}) {
if (beschaeftigt || nurLesen) return
beschaeftigt = true
for (const k of stripeKarte.querySelectorAll('button')) k.setAttribute('disabled', '')
try {
const a = await ctx.request(`/api/unterkunft/stripe/${pfad}?${q}`, { method: 'POST' })
if (weiter) {
if (!istHttps(a?.url)) throw new Error(STRIPE_FEHLER)
melde('Du wirst zu Stripe weitergeleitet …', 'ok')
window.location.href = a.url
return
}
if (typeof ctx.neuLaden === 'function') { try { await ctx.neuLaden() } catch { /* der Stand erscheint beim naechsten Laden */ } }
await neu()
} catch (err) {
melde(err?.message || 'Das hat nicht geklappt. Bitte versuch es noch einmal.', 'fehler')
for (const k of stripeKarte.querySelectorAll('button')) k.removeAttribute('disabled')
} finally {
beschaeftigt = false
}
}
const knopf = (text, aktion, pfad, optionen, art = 'kb-btn-outline') => h('button', { type: 'button', class: art, 'data-aktion': aktion, onclick: () => stripeAktion(pfad, optionen) }, text)
const zustandText = { keins: 'Noch nicht verbunden', onboarding: 'Angaben bei Stripe noch offen', aktiv: 'Verbunden und aktiv' }
const status = ['keins', 'onboarding', 'aktiv'].includes(stripe.status) ? stripe.status : 'keins'
const stripeKnoepfe = nurLesen ? [] : status === 'keins'
? [knopf('Stripe-Konto verbinden', 'stripe-verbinden', 'verbinden', { weiter: true }, 'kb-btn-primary'),
stripe.vorhandenesShopKonto === true ? knopf('Vorhandenes Webshop-Konto verwenden', 'stripe-vorhanden', 'vorhandenes-konto') : null]
: status === 'onboarding'
? [knopf('Angaben bei Stripe ergänzen', 'stripe-verbinden', 'verbinden', { weiter: true }, 'kb-btn-primary'), knopf('Status aktualisieren', 'stripe-aktualisieren', 'aktualisieren')]
: [knopf('Status aktualisieren', 'stripe-aktualisieren', 'aktualisieren')]
const stripeKarte = abschnitt('Stripe-Konto für Online-Zahlung', 'Gäste können die Buchung online bezahlen. Das Geld geht direkt auf dein Stripe-Konto.',
h('p', { 'data-stripe-status': status, class: status === 'aktiv' ? 'sh-ok' : 'sh-offen' }, zustandText[status]),
h('div', { class: 'sh-aktionen' }, ...stripeKnoepfe))
/* ----------------------------------------------------------------- Formular --- */
const speichern = h('button', { type: 'submit', class: 'kb-btn-primary', 'data-aktion': 'einstellungen-speichern' }, 'Einstellungen speichern')
const form = h('form', { class: 'sh-form sh-einstellungen', novalidate: true, 'aria-label': 'Einstellungen des Buchungskalenders' },
abschnitt('Deine Firmendaten', 'Diese Angaben stehen auf der Buchungsseite und auf den Rechnungen an deine Gäste. Pflichtfelder sind mit * markiert.',
feld('firma', 'Firma oder Name der Unterkunft *', firma),
feld('inhaber', 'Inhaber *', inhaber),
feld('strasse', 'Straße und Hausnummer *', strasse),
feld('plz', 'Postleitzahl *', plz),
feld('ort', 'Ort *', ort),
feld('email', 'E-Mail-Adresse *', email, 'An diese Adresse gehen neue Anfragen.'),
feld('telefon', 'Telefon (freiwillig)', telefon)),
abschnitt('Steuer', 'Eine der beiden Angaben ist Pflicht (§ 14 Abs. 4 UStG).',
feld('steuernummer', 'Steuernummer', steuernummer),
feld('ustId', 'USt-IdNr.', ustId, 'Zum Beispiel DE123456789.')),
abschnitt('Rechnungen', null,
feld('rechnungsPraefix', 'Rechnungspräfix', praefix, rechnungenVorhanden
? 'Der Präfix ist gesperrt, weil es schon Rechnungen gibt. Die Nummern bleiben so lückenlos.'
: '2 bis 8 Zeichen (A–Z, 0–9). Die Nummern lauten dann zum Beispiel RE-2026-0001. Du kannst den Präfix nur vor der ersten Rechnung ändern.'),
hakenFeld('rechnungAuftrag', auftrag, AUFTRAG_TEXT),
auftragHinweis,
widerrufPlatz),
abschnitt('Bankverbindung (freiwillig)', 'Erscheint als Hinweis, wenn Gäste vor Ort oder per Überweisung zahlen.',
feld('iban', 'IBAN', iban),
feld('bic', 'BIC', bic),
feld('bank', 'Name der Bank', bank)),
abschnitt('Darstellung', null, feld('akzentfarbe', 'Akzentfarbe', akzent, 'Diese Farbe bekommen Knöpfe auf deiner Buchungsseite.')),
status === 'aktiv'
? abschnitt('Online-Zahlung', null, hakenFeld('onlineZahlung', online, 'Online-Zahlung anbieten', 'Gäste können bei der Buchung per Stripe bezahlen.'))
: null,
allgemeinerFehler(),
nurLesen ? null : h('div', { class: 'sh-aktionen' }, speichern))
/* --------------------------------------------------------------- Speichern --- */
function sammle() {
const fehler = {}
const felder = { firma, inhaber, strasse, plz, ort, email }
for (const [name, el] of Object.entries(felder)) if (!el.value.trim()) fehler[name] = PFLICHT[name]
if (!steuernummer.value.trim() && !ustId.value.trim()) {
fehler.steuernummer = 'Bitte gib Steuernummer oder USt-IdNr. an.'
fehler.ustId = 'Bitte gib Steuernummer oder USt-IdNr. an.'
}
const p = praefix.value.trim().toUpperCase()
if (!rechnungenVorhanden && !/^[A-Z0-9]{2,8}$/.test(p)) fehler.rechnungsPraefix = 'Der Präfix braucht 2 bis 8 Zeichen aus A–Z und 0–9.'
if (Object.keys(fehler).length) return { fehler }
return {
einstellungen: {
firma: firma.value.trim(), inhaber: inhaber.value.trim(), strasse: strasse.value.trim(), plz: plz.value.trim(), ort: ort.value.trim(),
email: email.value.trim(), telefon: telefon.value.trim(), steuernummer: steuernummer.value.trim(), ustId: ustId.value.trim(),
iban: iban.value.trim(), bic: bic.value.trim(), bank: bank.value.trim(),
rechnungsPraefix: rechnungenVorhanden ? zeichen(e.rechnungsPraefix) || 'RE' : p,
rechnungAuftrag: auftrag.checked, akzentfarbe: akzent.value,
onlineZahlung: status === 'aktiv' && online.checked,
},
}
}
let laeuft = false
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
if (nurLesen || laeuft) return
laeuft = true
speichern.disabled = true
setzeFehler(form, {})
try {
const eingabe = sammle()
if (eingabe.fehler) {
setzeFehler(form, eingabe.fehler)
form.querySelector('[aria-invalid]')?.focus()
return
}
await ctx.request(`/api/unterkunft/einstellungen?${q}`, { method: 'PUT', body: eingabe.einstellungen })
melde('Die Einstellungen sind gespeichert.', 'ok')
if (typeof ctx.neuLaden === 'function') { try { await ctx.neuLaden() } catch { /* gespeichert ist gespeichert */ } }
} catch (err) {
zeigeApiFehler(form, err)
} finally {
laeuft = false
speichern.disabled = false
}
})
if (nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true
panel.replaceChildren(stripeKarte, form)
}
+189
View File
@@ -0,0 +1,189 @@
/* Buchungskalender, Reiter „Kalender“: Monatsansicht je Unterkunft, Farben nach Quelle, zwei Klicks auf freie Nächte sperren den Zeitraum.
API (alle mit ?projectId=): GET /api/unterkunft/unterkuenfte => { unterkuenfte:[{ id, name, … }] }
GET /unterkuenfte/:id/kalender?von&bis => { bereiche:[{ von, bis (Abreisetag, exklusiv), quelle, anfrageNummer?, sperreId?, notiz? }] }
quelle: 'gebucht' | 'reserviert' | 'ical' (auch 'ical:<id>') | 'manuell'.
POST /unterkuenfte/:id/sperren { von, bis, notiz } · DELETE /sperren/:id. Wochen beginnen am Montag. Fremdtext nur ueber textContent. */
import { abschnitt, allgemeinerFehler, fehlerKarte, feld, frage, h } from './shop-dom.js'
import { enc, heuteBerlin, ladeUnterkuenfte, msTag, plusTage, tagText, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
export const QUELLEN = [
['gebucht', 'Direkt gebucht', 'ist-gebucht'],
['reserviert', 'Reserviert (wartet auf Buchung)', 'ist-reserviert'],
['ical', 'Booking.com / Airbnb (iCal)', 'ist-ical'],
['manuell', 'Von dir gesperrt', 'ist-manuell'],
]
const MONATE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
const WOCHENTAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
const quelleArt = (q) => { const s = zeichen(q); return s.startsWith('ical') ? 'ical' : s.startsWith('anfrage') ? 'gebucht' : s }
/** Alle Tage des Monatsrasters (Montag bis Sonntag, volle Wochen) als 'YYYY-MM-DD'. */
export function rasterTage(jahr, monat) {
const erster = Date.UTC(jahr, monat, 1)
const letzter = Date.UTC(jahr, monat + 1, 0)
const start = erster - ((new Date(erster).getUTCDay() + 6) % 7) * 86400000
const ende = letzter + (6 - ((new Date(letzter).getUTCDay() + 6) % 7)) * 86400000
const tage = []
for (let ms = start; ms <= ende; ms += 86400000) tage.push(msTag(ms))
return tage
}
export async function render(panel, ctx) {
const heute = heuteBerlin()
const st = { unterkunft: null, jahr: Number(heute.slice(0, 4)), monat: Number(heute.slice(5, 7)) - 1, bereiche: [], start: null, lauf: 0 }
panel.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let liste
try {
liste = (await ladeUnterkuenfte(ctx)).filter((u) => u.aktiv !== false)
} catch (err) {
if (ctx.aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => render(panel, ctx)))
return
}
if (!ctx.aktuell()) return
if (!liste.length) {
panel.replaceChildren(h('div', { class: 'kb-panel kb-empty', 'data-leer': true }, 'Noch keine Unterkunft. Lege sie im Reiter „Unterkünfte & Preise“ an.'))
return
}
st.unterkunft = liste[0].id
const wahl = h('select', { id: 'bk-k-unterkunft', 'data-aktion': 'unterkunft' }, liste.map((u) => h('option', { value: u.id }, zeichen(u.name))))
wahl.addEventListener('change', () => { st.unterkunft = wahl.value; st.start = null; lade() })
const titel = h('strong', { class: 'bk-monat', 'aria-live': 'polite', 'data-monat': true })
const schritt = (n) => () => { const d = new Date(Date.UTC(st.jahr, st.monat + n, 1)); st.jahr = d.getUTCFullYear(); st.monat = d.getUTCMonth(); st.start = null; lade() }
const kopf = h('div', { class: 'bk-filter bk-kalenderkopf' },
liste.length > 1 ? h('label', { for: 'bk-k-unterkunft' }, 'Unterkunft') : null, liste.length > 1 ? wahl : h('strong', null, zeichen(liste[0].name)),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zurueck', 'aria-label': 'Vorheriger Monat', onclick: schritt(-1) }, '‹'),
titel,
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'weiter', 'aria-label': 'Nächster Monat', onclick: schritt(1) }, '›'))
const raster = h('div', { class: 'bk-raster', role: 'grid' })
const hinweis = h('p', { class: 'sh-hinweis', 'data-hinweis': true, role: 'status' })
const dialog = h('div', { class: 'bk-dialog' })
const legende = h('ul', { class: 'bk-legende', 'aria-label': 'Legende' }, QUELLEN.map(([, text, klasse]) => h('li', null, h('span', { class: `bk-farbe ${klasse}`, 'aria-hidden': 'true' }), text)))
panel.replaceChildren(abschnitt('Kalender', ctx.nurLesen ? 'Dein Kalender ist hier nur zum Ansehen.' : 'Klicke zwei freie Tage an, um den Zeitraum zu sperren, zum Beispiel für eigene Aufenthalte. Gesperrte Tage kannst du mit einem Klick wieder freigeben.',
kopf, raster, hinweis, legende, dialog))
await lade()
function bereichAm(tag) {
for (const art of ['gebucht', 'reserviert', 'ical', 'manuell']) {
const b = st.bereiche.find((x) => quelleArt(x.quelle) === art && x.von <= tag && tag < x.bis)
if (b) return b
}
return null
}
async function lade() {
const lauf = ++st.lauf
const tage = rasterTage(st.jahr, st.monat)
titel.textContent = `${MONATE[st.monat]} ${st.jahr}`
dialog.replaceChildren()
hinweis.textContent = ''
try {
const daten = await ctx.request(`/api/unterkunft/unterkuenfte/${enc(st.unterkunft)}/kalender?projectId=${enc(ctx.projectId)}&von=${tage[0]}&bis=${plusTage(tage.at(-1), 1)}`)
if (st.lauf !== lauf || !ctx.aktuell()) return
st.bereiche = Array.isArray(daten?.bereiche) ? daten.bereiche : []
} catch (err) {
if (st.lauf === lauf && ctx.aktuell()) { st.bereiche = []; zeichne(); hinweis.textContent = err.message }
return
}
zeichne()
}
function zeichne() {
const tage = rasterTage(st.jahr, st.monat)
const heute = heuteBerlin()
raster.replaceChildren(
...WOCHENTAGE.map((w) => h('div', { class: 'bk-wochentag', role: 'columnheader' }, w)),
...tage.map((tag) => {
const b = bereichAm(tag)
const art = b ? quelleArt(b.quelle) : null
const klassen = ['bk-tag', art ? `ist-${art}` : 'ist-frei', Number(tag.slice(5, 7)) - 1 === st.monat ? '' : 'bk-ausserhalb', tag === heute ? 'bk-heute' : '', st.start === tag ? 'bk-start' : '']
const label = `${tagText(tag)}, ${b ? (QUELLEN.find((q) => q[0] === art)?.[1] || 'belegt') : 'frei'}${b?.anfrageNummer ? `, Anfrage ${zeichen(b.anfrageNummer)}` : ''}`
return h('button', {
type: 'button', class: klassen.filter(Boolean).join(' '), 'data-tag': tag, 'aria-label': label, 'aria-pressed': st.start === tag ? 'true' : 'false',
onclick: () => klickTag(tag, b),
}, String(Number(tag.slice(8))))
}))
}
function klickTag(tag, b) {
dialog.replaceChildren()
hinweis.textContent = ''
if (b) {
st.start = null
zeichne()
if (quelleArt(b.quelle) === 'manuell' && b.sperreId) return aufhebenDialog(b)
hinweis.textContent = `${tagText(tag)}: ${QUELLEN.find((q) => q[0] === quelleArt(b.quelle))?.[1] || 'belegt'}${b.anfrageNummer ? ` (Anfrage ${zeichen(b.anfrageNummer)})` : ''}. Das kannst du hier nicht ändern.`
return
}
if (ctx.nurLesen) return
if (tag < heuteBerlin()) {
hinweis.textContent = 'Vergangene Tage kannst du nicht sperren.'
return
}
if (!st.start) {
st.start = tag
zeichne()
hinweis.textContent = `Erster Tag: ${tagText(tag)}. Klicke jetzt den letzten Tag an.`
return
}
const [von, letzter] = st.start <= tag ? [st.start, tag] : [tag, st.start]
const bis = plusTage(letzter, 1)
const belegt = st.bereiche.some((x) => x.von < bis && von < x.bis)
if (belegt) {
st.start = null
zeichne()
hinweis.textContent = 'In diesem Zeitraum liegen schon belegte Tage. Wähle einen freien Zeitraum.'
return
}
sperrDialog(von, bis)
}
function sperrDialog(von, bis) {
const notiz = h('input', { type: 'text', id: 'bk-k-notiz', maxlength: '200', autocomplete: 'off' })
const box = frage('Zeitraum sperren', h('p', null, `${tagText(von)} bis ${tagText(plusTage(bis, -1))} (Abreise ${tagText(bis)}) sperren?`),
feld('notiz', 'Notiz (nur für dich)', notiz), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'sperren-senden',
onclick: async (ev) => {
const knopf = ev.currentTarget
knopf.disabled = true
try {
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(st.unterkunft)}/sperren?projectId=${enc(ctx.projectId)}`, { method: 'POST', body: { von, bis, notiz: String(notiz.value).trim() } })
if (!ctx.aktuell()) return
st.start = null
ctx.zeigeMeldung('Der Zeitraum ist gesperrt.', 'ok')
await lade()
} catch (err) {
knopf.disabled = false
zeigeApiFehler(box, err)
}
},
}, 'Zeitraum sperren'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'abbrechen', onclick: () => { st.start = null; dialog.replaceChildren(); zeichne() } }, 'Abbrechen')))
dialog.replaceChildren(box)
notiz.focus()
}
function aufhebenDialog(b) {
const box = frage('Sperre aufheben', h('p', null, `${tagText(b.von)} bis ${tagText(plusTage(b.bis, -1))}${b.notiz ? `: ${zeichen(b.notiz)}` : ''}`), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
ctx.nurLesen ? null : h('button', {
type: 'button', class: 'kb-btn', 'data-aktion': 'aufheben',
onclick: async (ev) => {
const knopf = ev.currentTarget
knopf.disabled = true
try {
await ctx.request(`/api/unterkunft/sperren/${enc(b.sperreId)}?projectId=${enc(ctx.projectId)}`, { method: 'DELETE' })
if (!ctx.aktuell()) return
ctx.zeigeMeldung('Die Sperre ist aufgehoben.', 'ok')
await lade()
} catch (err) {
knopf.disabled = false
zeigeApiFehler(box, err)
}
},
}, 'Sperre aufheben'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'abbrechen', onclick: () => dialog.replaceChildren() }, 'Schließen')))
dialog.replaceChildren(box)
}
}
+33
View File
@@ -0,0 +1,33 @@
/* Buchungskalender, Reiter „Rechnungen“: Liste der Rechnungen und Storno-Rechnungen mit PDF-Link.
API: GET /api/unterkunft/rechnungen?projectId= => { rechnungen:[{ id, nummer, datum, art:'rechnung'|'storno', gastName, bruttoCent, anfrageNummer? }] };
PDF: GET /api/unterkunft/rechnungen/<id>.pdf?projectId= (Einzel-PDF, kein ZIP). Fremdtext nur ueber textContent. render(panel, ctx). */
import { abschnitt, datumText, euroText, h, infoKarte } from './shop-dom.js'
import { enc, zeichen } from './buchungen-uk-hilfe.js'
const ART = { rechnung: 'Rechnung', storno: 'Storno-Rechnung' }
const betragText = (cent) => {
if (!Number.isInteger(cent)) return ''
return cent < 0 ? `−${euroText(-cent)}` : euroText(cent)
}
export async function render(panel, ctx = {}) {
const q = `projectId=${enc(ctx.projectId)}`
const antwort = await ctx.request(`/api/unterkunft/rechnungen?${q}`)
const liste = Array.isArray(antwort?.rechnungen) ? antwort.rechnungen : []
if (!liste.length) {
panel.replaceChildren(infoKarte('Rechnungen', 'Noch keine Rechnungen. Sobald ein Gast bucht, erscheint hier die Rechnung.'))
return
}
const zeilen = liste.map((r) => h('li', { class: 'bk-rechnung', 'data-rechnung': zeichen(r.id), 'data-art': zeichen(r.art) },
h('span', { class: 'bk-nummer' }, zeichen(r.nummer)),
h('span', { class: 'bk-zeitraum' }, datumText(zeichen(r.datum), { zeit: false })),
h('span', { class: 'bk-gast' }, zeichen(r.gastName)),
h('span', { class: `sh-pille ${r.art === 'storno' ? 'sh-pille-bad' : 'sh-pille-ok'}` }, ART[r.art] || 'Rechnung'),
h('span', { class: 'bk-betrag' }, betragText(r.bruttoCent)),
h('a', {
class: 'kb-btn-outline', 'data-aktion': 'pdf', href: `/api/unterkunft/rechnungen/${enc(zeichen(r.id))}.pdf?${q}`, target: '_blank', rel: 'noopener',
'aria-label': `PDF zu ${zeichen(r.nummer)} herunterladen`,
}, 'PDF')))
panel.replaceChildren(abschnitt('Rechnungen', 'Rechnungen und Storno-Rechnungen deiner Gäste, ausgestellt in deinem Namen.', h('ul', { class: 'bk-liste' }, zeilen)))
}
+127
View File
@@ -0,0 +1,127 @@
/* Buchungskalender, Reiter „Unterkünfte & Preise“: Liste (anlegen, sortieren, aktivieren), Bearbeiten (Stammdaten, Preise, iCal).
API (alle mit ?projectId=): GET /api/unterkunft/unterkuenfte => { unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv,
icalImporte, icalExportUrl }] }; POST /unterkuenfte und PUT /unterkuenfte/:id mit { name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }
(Antwort { unterkunft:{…} }). Preise: buchungen-preise.js, iCal: buchungen-ical.js. Fremdtext nur ueber textContent. */
import { abschnitt, allgemeinerFehler, fehlerKarte, feld, h, hakenFeld, setzeFehler, zeigeFehler } from './shop-dom.js'
import { enc, ladeUnterkuenfte, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
import { zeichnePreise } from './buchungen-preise.js'
import { zeichneIcal } from './buchungen-ical.js'
const ganz = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,3}$/.test(t) ? Number(t) : null }
const zeit = (t) => /^([01]\d|2[0-3]):[0-5]\d$/.test(t)
export async function render(panel, ctx) {
const q = `projectId=${enc(ctx.projectId)}`
const inhalt = h('div', { class: 'bk-unterkuenfte' })
panel.replaceChildren(inhalt)
await zeigeListe()
async function zeigeListe() {
inhalt.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let liste
try {
liste = await ladeUnterkuenfte(ctx)
} catch (err) {
if (ctx.aktuell()) inhalt.replaceChildren(fehlerKarte(err.message, zeigeListe))
return
}
if (!ctx.aktuell()) return
const kopf = h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'neu', onclick: () => zeigeForm(null, liste) }, 'Unterkunft hinzufügen'))
if (!liste.length) {
inhalt.replaceChildren(kopf, h('div', { class: 'kb-panel kb-empty', 'data-leer': true }, 'Noch keine Unterkunft. Lege deine erste Unterkunft an.'))
return
}
inhalt.replaceChildren(kopf, h('ul', { class: 'bk-liste' }, liste.map((u, i) => h('li', { class: 'kb-panel', 'data-unterkunft': u.id },
h('div', { class: 'bk-detail-kopf' },
h('strong', null, zeichen(u.name)),
h('span', { class: `sh-pille sh-pille-${u.aktiv === false ? 'off' : 'ok'}` }, u.aktiv === false ? 'Deaktiviert' : 'Aktiv')),
h('p', { class: 'sh-hinweis' }, `bis ${u.maxPersonen ?? '?'} Personen, ab ${u.mindestNaechte ?? 1} ${u.mindestNaechte === 1 ? 'Nacht' : 'Nächten'}, Check-in ${zeichen(u.checkIn)}, Check-out ${zeichen(u.checkOut)}`),
h('div', { class: 'bk-aktionen' },
h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'bearbeiten', onclick: () => zeigeForm(u, liste) }, 'Bearbeiten'),
ctx.nurLesen ? null : [
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'hoch', 'aria-label': `${zeichen(u.name)} nach oben`, disabled: i === 0, onclick: () => schiebe(liste, i, -1) }, '↑'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'runter', 'aria-label': `${zeichen(u.name)} nach unten`, disabled: i === liste.length - 1, onclick: () => schiebe(liste, i, 1) }, '↓'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'umschalten', onclick: () => speichere(u, { aktiv: u.aktiv === false }, 'Gespeichert.') }, u.aktiv === false ? 'Aktivieren' : 'Deaktivieren'),
])))))
}
const koerper = (u, extra = {}) => ({
name: u.name, beschreibung: zeichen(u.beschreibung), maxPersonen: u.maxPersonen, mindestNaechte: u.mindestNaechte, checkIn: u.checkIn, checkOut: u.checkOut, sort: u.sort ?? 0, aktiv: u.aktiv !== false, ...extra,
})
async function speichere(u, extra, meldung) {
try {
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(u.id)}?${q}`, { method: 'PUT', body: koerper(u, extra) })
if (!ctx.aktuell()) return
ctx.zeigeMeldung(meldung, 'ok')
await zeigeListe()
} catch (err) { ctx.zeigeMeldung(err.message, 'fehler') }
}
async function schiebe(liste, i, d) {
const a = liste[i]
const b = liste[i + d]
try {
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(a.id)}?${q}`, { method: 'PUT', body: koerper(a, { sort: i + d }) })
await ctx.request(`/api/unterkunft/unterkuenfte/${enc(b.id)}?${q}`, { method: 'PUT', body: koerper(b, { sort: i }) })
if (ctx.aktuell()) await zeigeListe()
} catch (err) { ctx.zeigeMeldung(err.message, 'fehler') }
}
/** Formular (u = null: neu). Bei einer bestehenden Unterkunft folgen Preise und iCal. */
async function zeigeForm(u, liste) {
const name = h('input', { type: 'text', id: 'bk-u-name', maxlength: '120', autocomplete: 'off' }); name.value = zeichen(u?.name)
const beschr = h('textarea', { id: 'bk-u-beschr', rows: '3', maxlength: '500' }); beschr.value = zeichen(u?.beschreibung)
const maxP = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-u-max' }); maxP.value = String(u?.maxPersonen ?? 2)
const minN = h('input', { type: 'text', inputmode: 'numeric', id: 'bk-u-min' }); minN.value = String(u?.mindestNaechte ?? 1)
const ein = h('input', { type: 'text', id: 'bk-u-ein', placeholder: '15:00', maxlength: '5' }); ein.value = zeichen(u?.checkIn) || '15:00'
const aus = h('input', { type: 'text', id: 'bk-u-aus', placeholder: '10:00', maxlength: '5' }); aus.value = zeichen(u?.checkOut) || '10:00'
const aktiv = h('input', { type: 'checkbox', id: 'bk-u-aktiv' }); aktiv.checked = u ? u.aktiv !== false : true
const form = h('form', { novalidate: true, onsubmit: (ev) => ev.preventDefault() },
feld('name', 'Name der Unterkunft', name), feld('beschreibung', 'Kurzbeschreibung (optional)', beschr),
feld('maxPersonen', 'Höchstens Personen (1 bis 20)', maxP), feld('mindestNaechte', 'Mindestaufenthalt in Nächten (1 bis 90)', minN),
feld('checkIn', 'Check-in ab (HH:MM)', ein), feld('checkOut', 'Check-out bis (HH:MM)', aus),
hakenFeld('aktiv', aktiv, 'Aktiv (für Gäste buchbar)'), allgemeinerFehler(),
h('div', { class: 'bk-aktionen' },
ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'speichern', onclick: senden }, u ? 'Speichern' : 'Unterkunft anlegen'),
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'zur-liste', onclick: zeigeListe }, 'Zur Liste')))
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, textarea')) el.disabled = true
const bereiche = h('div', { class: 'bk-bearbeiten' })
inhalt.replaceChildren(abschnitt(u ? `Unterkunft „${zeichen(u.name)}“` : 'Neue Unterkunft', null, form), bereiche)
name.focus()
if (u) {
bereiche.replaceChildren(abschnitt('Preise', 'Preise sind Bruttopreise in Euro, so sieht sie dein Gast.', await zeichnePreise(ctx, u)), zeichneIcal(ctx, u, async (meldung) => {
ctx.zeigeMeldung(meldung, 'ok')
const neu = (await ladeUnterkuenfte(ctx)).find((x) => x.id === u.id)
if (ctx.aktuell()) await zeigeForm(neu || u, liste)
}))
}
async function senden(ev) {
const fehler = {}
const n = String(name.value).trim()
const mp = ganz(maxP)
const mn = ganz(minN)
if (!n) fehler.name = 'Bitte gib einen Namen an.'
if (mp === null || mp < 1 || mp > 20) fehler.maxPersonen = 'Bitte gib eine Zahl von 1 bis 20 an.'
if (mn === null || mn < 1 || mn > 90) fehler.mindestNaechte = 'Bitte gib eine Zahl von 1 bis 90 an.'
if (!zeit(String(ein.value).trim())) fehler.checkIn = 'Bitte gib eine Uhrzeit wie 15:00 an.'
if (!zeit(String(aus.value).trim())) fehler.checkOut = 'Bitte gib eine Uhrzeit wie 10:00 an.'
if (Object.keys(fehler).length) return zeigeFehler(form, { message: '', fehler })
setzeFehler(form, {})
const body = { name: n, beschreibung: String(beschr.value).trim(), maxPersonen: mp, mindestNaechte: mn, checkIn: String(ein.value).trim(), checkOut: String(aus.value).trim(), sort: u?.sort ?? liste.length, aktiv: aktiv.checked }
const knopf = ev.currentTarget
knopf.disabled = true
try {
const antwort = await ctx.request(u ? `/api/unterkunft/unterkuenfte/${enc(u.id)}?${q}` : `/api/unterkunft/unterkuenfte?${q}`, { method: u ? 'PUT' : 'POST', body })
if (!ctx.aktuell()) return
ctx.zeigeMeldung(u ? 'Gespeichert.' : 'Die Unterkunft ist angelegt. Jetzt kannst du die Preise eintragen.', 'ok')
const neu = antwort?.unterkunft?.id ? antwort.unterkunft : null
if (u || !neu) await zeigeListe()
else await zeigeForm({ ...neu }, [...liste, neu])
} catch (err) {
knopf.disabled = false
zeigeApiFehler(form, err)
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
/* Buchungskalender: gemeinsame Helfer der Reiter „Kalender“ und „Unterkünfte & Preise“ (Datumsrechnung, Fehlertexte, Unterkunftsliste).
Kalendertage sind Texte YYYY-MM-DD (Berlin); gerechnet wird nur mit UTC-Mitternacht, nie mit der Zeitzone des Browsers. */
import { zeigeFehler } from './shop-dom.js'
import { fehlerFuerFormular } from './buchungen-fehler.js'
export const enc = encodeURIComponent
export const zeichen = (x) => (typeof x === 'string' ? x : '')
export const tagMs = (tag) => { const [j, m, t] = String(tag).split('-').map(Number); return Date.UTC(j, m - 1, t) }
export const msTag = (ms) => new Date(ms).toISOString().slice(0, 10)
export const plusTage = (tag, n) => msTag(tagMs(tag) + n * 86400000)
/** Heute als Berlin-Datum. */
let jetzt = () => new Date()
/** Uhr der Portal-Reiter austauschen (nur fuer Tests); ohne Argument zurueck auf die echte Uhr. */
export const setzeUhrFuerTests = (fn) => { jetzt = fn || (() => new Date()) }
export const heuteBerlin = () => jetzt().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' })
/** '2026-11-12' ⇒ '12.11.2026'. */
export const tagText = (tag) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(tag || ''); return m ? `${m[3]}.${m[2]}.${m[1]}` : '' }
/** Fehler am Formular zeigen (Feldfehler am Feld, sonst allgemein); der Fokus geht auf das erste fehlerhafte Feld. */
export function zeigeApiFehler(form, err) {
zeigeFehler(form, fehlerFuerFormular(err))
}
/** Unterkuenfte des Projekts: GET /api/unterkunft/unterkuenfte => { unterkuenfte:[…] }. */
export async function ladeUnterkuenfte(ctx) {
const daten = await ctx.request(`/api/unterkunft/unterkuenfte?projectId=${enc(ctx.projectId)}`)
return (Array.isArray(daten?.unterkuenfte) ? daten.unterkuenfte : []).slice().sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
}
+82
View File
@@ -0,0 +1,82 @@
/* Buchungskalender im Kundenbereich (buchungen.js): Klassen mit bk-; Reiter, Pillen, Karten kommen aus shop.css (sh-). */
#view-buchungen .kb-panel { max-width: none; }
#view-buchungen .fn-project { margin-bottom: 16px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 14px 18px; }
#view-buchungen button:focus-visible, #view-buchungen input:focus-visible, #view-buchungen select:focus-visible, #view-buchungen textarea:focus-visible,
#view-buchungen a:focus-visible { outline: 3px solid #d4b4ff; outline-offset: 2px; }
.bk-filter { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.bk-liste { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.bk-zeile {
display: grid; grid-template-columns: 90px 1.4fr 1fr auto auto; gap: 8px 14px; align-items: center; width: 100%; padding: 12px 16px; text-align: left;
border-radius: 12px; border: 1px solid var(--kb-border); background: rgba(255, 255, 255, 0.03); color: inherit; font: inherit; cursor: pointer;
}
.bk-zeile:hover { border-color: var(--kb-purple-line); }
.bk-zeile .sh-pille { margin: 0; justify-self: start; }
.bk-betrag { font-weight: 700; text-align: right; }
.bk-zurueck { margin-bottom: 12px; }
.bk-detail-kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.bk-detail-kopf .sh-pille { margin: 0; }
.bk-daten { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 12px 0 18px; }
.bk-daten dt { color: var(--kb-muted); }
.bk-daten dd { margin: 0; overflow-wrap: anywhere; }
.bk-posten-kopf, .bk-posten-zeile { display: grid; grid-template-columns: minmax(0, 3fr) 70px 110px 80px auto; gap: 8px; align-items: center; }
.bk-posten-kopf { font-size: 12px; color: var(--kb-muted); margin-bottom: 4px; }
.bk-posten-zeile { margin-bottom: 8px; }
.bk-summen { margin: 12px 0; text-align: right; }
.bk-summen p { margin: 2px 0; }
.bk-gesamt { font-size: 18px; }
.bk-aktionen { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0; }
.bk-tabelle { width: 100%; border-collapse: collapse; margin: 10px 0; }
.bk-tabelle td { padding: 6px 4px; border-bottom: 1px solid var(--kb-border); }
.bk-tabelle td:last-child { text-align: right; }
.bk-link input { width: 100%; margin: 6px 0 10px; }
@media (max-width: 640px) {
.bk-zeile { grid-template-columns: 1fr auto; }
.bk-posten-kopf { display: none; }
.bk-posten-zeile { grid-template-columns: 1fr 1fr; }
.bk-posten-zeile input:first-child { grid-column: 1 / -1; }
}
.bk-kalenderkopf { flex-wrap: wrap; }
.bk-monat { min-width: 9em; text-align: center; }
.bk-raster { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; width: 100%; margin: 10px 0; }
.bk-wochentag { text-align: center; font-size: 12px; color: var(--kb-muted); }
.bk-tag { aspect-ratio: 1.5; min-height: 40px; border-radius: 8px; border: 1px solid var(--kb-border); background: rgba(255, 255, 255, 0.03); color: inherit; font: inherit; cursor: pointer; }
.bk-tag.bk-ausserhalb { opacity: 0.45; }
.bk-tag.bk-heute { border-color: var(--kb-purple-line); font-weight: 700; }
.bk-tag.bk-start { outline: 2px solid #d4b4ff; }
.ist-gebucht { background: rgba(74, 222, 128, 0.28); }
.ist-reserviert { background: rgba(251, 191, 36, 0.28); }
.ist-ical { background: rgba(96, 165, 250, 0.28); }
.ist-manuell { background: rgba(156, 163, 175, 0.35); }
.bk-legende { list-style: none; display: flex; flex-wrap: wrap; gap: 6px 16px; padding: 0; margin: 8px 0; }
.bk-legende li { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.bk-farbe { width: 16px; height: 16px; border-radius: 4px; border: 1px solid var(--kb-border); display: inline-block; }
.bk-staffel { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.bk-staffel input { width: 90px; }
/* Preise je Personenzahl: ein Euro-Feld je Gaestezahl unter der Beschriftung; bricht auf dem Handy um, kein Seitwaertsscrollen */
.bk-personenpreise { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; min-width: 0; max-width: 100%; }
.bk-personenpreise[hidden], .bk-preis-einzeln[hidden] { display: none; }
.bk-personpreis { display: grid; gap: 2px; font-size: 13px; font-weight: 600; }
.bk-personpreis > span { white-space: nowrap; }
.bk-vorschau { font-weight: 700; }
.bk-import { display: flex; align-items: center; gap: 4px 12px; flex-wrap: wrap; }
.bk-anleitung details { margin: 8px 0; }
/* Rechnungen */
.bk-rechnung {
display: grid; grid-template-columns: 110px 100px 1fr auto auto auto; gap: 8px 14px; align-items: center; padding: 12px 16px;
border-radius: 12px; border: 1px solid var(--kb-border); background: rgba(255, 255, 255, 0.03);
}
.bk-rechnung .sh-pille { margin: 0; }
@media (max-width: 720px) { .bk-rechnung { grid-template-columns: 1fr auto; } }
/* Knoepfe „kb-btn“ (Hauptaktion) wie kb-btn-primary; Eingaben ausserhalb von .sh-feld (Posten-Editor, Staffeln, Filter) wie die Felder der Formulare */
#view-buchungen .kb-btn { display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; padding: 10px 20px; font: inherit; font-weight: 600; cursor: pointer; background: var(--kb-purple); color: #fff; min-height: 40px; }
#view-buchungen .kb-btn:hover { background: var(--kb-purple-hover); }
#view-buchungen .kb-btn:disabled { opacity: 0.5; cursor: default; }
#view-buchungen .bk-posten input:not([type=checkbox]), #view-buchungen .bk-posten select, #view-buchungen .bk-staffel input, #view-buchungen .bk-filter select,
#view-buchungen .bk-vorschaufelder input, #view-buchungen .bk-importform input {
box-sizing: border-box; min-width: 0; padding: 9px 10px; border-radius: 10px; font: inherit; font-size: 15px; border: 1px solid var(--kb-border-strong); background: rgba(255, 255, 255, 0.05); color: inherit;
}
#view-buchungen .bk-posten select option, #view-buchungen .bk-filter select option { color: #111; }
#view-buchungen .bk-posten input, #view-buchungen .bk-posten select { width: 100%; }
#view-buchungen input[readonly] { box-sizing: border-box; width: 100%; padding: 9px 10px; border-radius: 10px; font: inherit; font-size: 15px; border: 1px solid var(--kb-border-strong); background: rgba(255, 255, 255, 0.05); color: inherit; }
+241
View File
@@ -0,0 +1,241 @@
/* Buchungskalender im Kundenbereich (ES-Modul, von app.js beim Oeffnen der Ansicht #buchungen geladen). Aufbau wie shop.js:
initBuchungen(rootEl, { projectId }): Projekt waehlen (GET /api/funktionen, features.unterkunft), Stand holen (GET /api/unterkunft/status)
und je nach state die Reiter (REITER, lazy geladen, render(panel, ctx)) oder (state off) die Anfrage-/Bestellkarte (Task 14).
ctx = { projectId, status, nurLesen, aktuell(), neuLaden(), zeigeMeldung(text, art), request }.
Helfer: shop-dom.js (h, request, Karten), Styles: buchungen.css (bk-) und shop.css (sh-). Fremdtext nur ueber textContent. */
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
import { renderEinschaltSeite, statusKarte } from './funktion-bestellen.js'
import { anfrageKarte, anfrageZeilen, renderAnfrageFormular } from './buchungen-anfrage.js'
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
export const REITER = [
{ key: 'anfragen', label: 'Anfragen', laden: () => import('./buchungen-tab-anfragen.js') },
{ key: 'kalender', label: 'Kalender', laden: () => import('./buchungen-tab-kalender.js') },
{ key: 'unterkuenfte', label: 'Unterkünfte & Preise', laden: () => import('./buchungen-tab-unterkuenfte.js') },
{ key: 'rechnungen', label: 'Rechnungen', laden: () => import('./buchungen-tab-rechnungen.js') },
{ key: 'einstellungen', label: 'Einstellungen', laden: () => import('./buchungen-tab-einstellungen.js') },
]
const NICHT_VERFUEGBAR = 'Der Buchungskalender ist noch nicht verfügbar.'
const auswahl = { projectId: '', reiter: 'anfragen' }
/** Gemerkte Auswahl vergessen (fuer Tests). */
export function vergissAuswahl() {
auswahl.projectId = ''
auswahl.reiter = 'anfragen'
}
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
const istAbo = (s) => s.modell === 'abo'
/** Mitarbeiter in der Kundenansicht (window.KbView, von der Hülle gesetzt): Einschalten bleibt gesperrt. */
const inKundenansicht = () => typeof window !== 'undefined' && Boolean(window.KbView?.id?.())
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
export function initBuchungen(root, { projectId, einschalten } = {}) {
if (!root) return Promise.resolve()
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
if (root.dataset) delete root.dataset.wunschProjekt
return starte(root, wunsch, einschalten)
}
async function starte(root, projectId, einschalten) {
if (projectId) auswahl.projectId = projectId
const s = { root, projects: [], modell: undefined, projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null, einschalten }
S = s
let funktionen
try {
funktionen = await request('/api/funktionen')
} catch (err) {
if (S === s) root.replaceChildren(fehlerKarte(err.message, () => starte(root, auswahl.projectId, einschalten)))
return
}
if (S !== s) return
s.modell = funktionen?.modell
if (!funktionen?.unterkunft?.verfuegbar) {
keineReiter(s)
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
return
}
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
if (!s.projects.length) {
keineReiter(s)
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
return
}
const hat = (zustaende) => s.projects.find((p) => zustaende.includes(zustandVon(p)))
s.projekt = s.projects.find((p) => p.projectId === auswahl.projectId) || hat(['active']) || hat(['setup']) || hat(['disabled']) || s.projects[0]
auswahl.projectId = s.projekt.projectId
s.meldungEl = h('div', { class: 'sh-meldung', role: 'status', 'aria-live': 'polite', tabindex: '-1' })
s.buehne = h('div', { id: 'buchungen-buehne' })
root.replaceChildren(...(s.projects.length > 1 ? [projektwahl(s)] : []), s.meldungEl, s.buehne)
await zeigeProjekt(s)
}
function projektwahl(s) {
const select = h('select', { id: 'buchungen-project' }, s.projects.map((p) => {
const option = h('option', { value: p.projectId }, p.projectName)
option.selected = p.projectId === s.projekt.projectId
return option
}))
select.addEventListener('change', () => {
auswahl.projectId = select.value
s.projekt = s.projects.find((p) => p.projectId === select.value)
s.neueAnfrage = false
zeigeMeldung(s, '')
zeigeProjekt(s)
})
return h('div', { class: 'fn-project kb-panel' }, h('label', { for: 'buchungen-project' }, 'Website'), select)
}
function zeigeMeldung(s, meldung, art = 'ok') {
const el = s.meldungEl
if (!el) return
el.textContent = meldung || ''
el.className = `sh-meldung sh-meldung-${art}`
el.setAttribute('role', art === 'fehler' ? 'alert' : 'status')
el.setAttribute('aria-live', art === 'fehler' ? 'assertive' : 'polite')
if (!meldung) return
if (art === 'fehler') el.focus({ preventScroll: true })
el.scrollIntoView?.({ block: 'nearest' })
}
const ladeStatus = (projectId) => request(`/api/unterkunft/status?projectId=${encodeURIComponent(projectId)}`)
async function zeigeProjekt(s) {
const lauf = ++s.lauf
const aktuell = () => S === s && s.lauf === lauf
keineReiter(s)
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
let status
try {
status = await ladeStatus(s.projekt.projectId)
} catch (err) {
if (aktuell()) s.buehne.replaceChildren(fehlerKarte(err.message, () => zeigeProjekt(s)))
return
}
if (!aktuell()) return
zeichneAnsicht(s, { status, aktuell })
}
function zeichneAnsicht(s, { status, aktuell }) {
s.status = status
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
const zustand = status?.state || zustandVon(s.projekt)
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
else if (zustand === 'ordered') zeigeBestellung(s)
else if (zustand === 'off') zeigeAnfrage(s, { status, aktuell })
else s.buehne.replaceChildren(infoKarte('Buchungskalender', 'Der Stand deines Buchungskalenders ist gerade nicht lesbar. Bitte lade die Seite neu.'))
}
/* Zustand off: Anfrageformular, Karte „wird geprueft“ / „abgelehnt“ oder (angenommen) der Hinweis auf die Bestellung. */
function zeigeAnfrage(s, { status, aktuell }) {
const projectId = s.projekt.projectId
const ansicht = status?.anfrage
// nach einem 409 bleibt die Meldung des Servers sichtbar, waehrend die Ansicht neu laedt
const neuLaden = async (o) => { await zeigeProjekt(s); if (o?.meldung && S === s) zeigeMeldung(s, o.meldung, 'fehler') }
if (ansicht?.status === 'angenommen' && istAbo(s)) {
// Abo-Modell: die Zusage fuehrt zum Einschalten (kein Preis, keine Haken, keine Bestellung)
const wurzel = h('div', { class: 'sh-wurzel' })
s.buehne.replaceChildren(wurzel)
renderEinschaltSeite(wurzel, {
ctx: { projectId, aktuell },
daten: { offen: [], kundenansicht: inKundenansicht() },
key: 'unterkunft',
zusammenfassung: () => (ansicht.anfrage ? [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: anfrageZeilen(ansicht.anfrage).map(({ name, wert }) => `${name}: ${wert}`) }] : []),
einschalten: s.einschalten,
nachEinschalten: () => zeigeProjekt(s),
})
} else if (ansicht?.status === 'angenommen') {
s.buehne.replaceChildren(infoKarte('Deine Anfrage wurde angenommen', 'Wir melden uns bei dir mit den nächsten Schritten zur Bestellung.', { chat: true }))
} else if (ansicht?.status === 'offen') {
s.buehne.replaceChildren(anfrageKarte(ansicht, { projectId, neuLaden }))
} else if (ansicht?.status === 'abgelehnt' && !s.neueAnfrage) {
s.buehne.replaceChildren(anfrageKarte(ansicht, { projectId, neu: () => { s.neueAnfrage = true; zeigeAnfrage(s, { status, aktuell }) } }))
} else {
const wurzel = h('div', { class: 'sh-wurzel' })
s.buehne.replaceChildren(wurzel)
renderAnfrageFormular(wurzel, {
projectId,
vorgabe: ansicht?.anfrage || null,
preis: textOr(s.projekt?.features?.unterkunft?.priceLabel) || '199 €',
modell: s.modell,
nachSenden: async (_a, o) => { s.neueAnfrage = false; if (aktuell()) await neuLaden(o) },
})
}
}
/* Zustand ordered: Statuskarte „Zahlung offen“ mit „Zahlung fortsetzen“ (generische Bestellseite, Checkout /api/funktionen/unterkunft/checkout). */
function zeigeBestellung(s) {
const eintrag = { ...s.projekt.features?.unterkunft, state: 'ordered' }
s.buehne.replaceChildren(statusKarte({ projekt: s.projekt, bestellung: eintrag, kundenansicht: false, key: 'unterkunft', name: 'Buchungskalender' }, {
weiter: async ({ meldung }) => { await zeigeProjekt(s); if (S === s) zeigeMeldung(s, meldung, 'fehler') },
}))
}
function zeigeReiter(s, { status, aktuell, zustand }) {
const neuLaden = async () => {
const neu = await ladeStatus(s.projekt.projectId)
if (!aktuell()) return neu
s.status = neu
ctx.status = neu
if ((neu.state || 'off') !== zustand) zeichneAnsicht(s, { status: neu, aktuell })
return neu
}
const ctx = {
projectId: s.projekt.projectId,
status,
nurLesen: zustand === 'disabled',
abwickeln: true, // Anfragen und Stornos bleiben auch bei abgeschalteter Funktion bearbeitbar (Pflichten gegenueber Gaesten)
aktuell,
request,
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
neuLaden,
}
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
let aktiv = null
let zaehler = 0
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
onclick: () => waehleReiter(r.key),
}, r.label))
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
leiste?.addEventListener('keydown', (ev) => {
const i = REITER.findIndex((r) => r.key === aktiv)
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
if (ziel === undefined) return
ev.preventDefault()
waehleReiter(REITER[ziel].key)
knoepfe[ziel].focus()
})
function waehleReiter(key) {
const reiter = REITER.find((r) => r.key === key) || REITER[0]
auswahl.reiter = reiter.key
aktiv = reiter.key
const nr = ++zaehler
for (const k of knoepfe) {
const an = k.dataset.reiter === reiter.key
k.setAttribute('aria-selected', String(an))
k.tabIndex = an ? 0 : -1
}
if (abo) {
panel.setAttribute('aria-label', reiter.label)
meldeReiter(s.root, { eintraege: reiterEintraege('buchungen', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
} else panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
zeigeMeldung(s, '')
panel.replaceChildren()
reiter.laden()
.then((modul) => (nr === zaehler && aktuell() ? modul.render(panel, ctx) : undefined))
.catch((err) => { if (nr === zaehler && aktuell()) panel.replaceChildren(fehlerKarte(err.message, () => waehleReiter(reiter.key))) })
}
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
waehleReiter(auswahl.reiter)
}
+29
View File
@@ -25,6 +25,9 @@
<link rel="stylesheet" href="/analyse.css">
<link rel="stylesheet" href="/termine.css">
<link rel="stylesheet" href="/shop.css">
<link rel="stylesheet" href="/buchungen.css">
<link rel="stylesheet" href="/zeiten.css">
<link rel="stylesheet" href="/auftrag.css">
</head>
<body class="portal-page dashboard-page kb-page">
<div class="dot-background" aria-hidden="true">
@@ -206,12 +209,38 @@
<div id="webshop-body"><p class="kb-empty">Lade…</p></div>
</section>
<!-- Buchungskalender: Unterkünfte, Preise, Anfragen, Rechnungen, Einstellungen (buchungen.js, initBuchungen); Unterseite von Funktionen -->
<section class="kb-view" id="view-buchungen">
<header class="kb-head">
<h1>Buchungskalender</h1>
<p>Anfragen und Buchungen für deine Unterkünfte – mit Belegungskalender und Rechnungen für deine Gäste.</p>
</header>
<div id="buchungen-body"><p class="kb-empty">Lade…</p></div>
</section>
<!-- Öffnungszeiten live: Einschalten oder Bestellen, Status und Editor (zeiten.js, initZeiten); Unterseite von Funktionen. Kein aria-live am Bereich -->
<section class="kb-view" id="view-zeiten">
<header class="kb-head">
<h1>Öffnungszeiten live</h1>
<p>Deine Website zeigt immer den aktuellen Stand: geöffnet oder geschlossen, mit Feiertagen, Urlaub und Hinweisen.</p>
</header>
<div id="zeiten-body"><p class="kb-empty">Lade…</p></div>
</section>
<!-- Rechnungen -->
<section class="kb-view" id="view-invoices">
<header class="kb-head">
<h1>Rechnungen</h1>
<p>Deine Rechnungen ansehen, bezahlen und herunterladen.</p>
</header>
<!-- Aufträge: Auftragsbestätigungen ansehen und bestätigen (auftrag-portal.js, von app.js loadCustomerOrders); erscheint nur,
wenn Aufträge freigeschaltet sind und der Kunde welche hat -->
<section class="ab-portal hidden" id="orders-section" aria-labelledby="orders-title">
<h2 id="orders-title">Aufträge</h2>
<p class="kb-note">Deine Auftragsbestätigungen. Offene Aufträge bestätigst du hier mit einem Klick.</p>
<div id="orders-body"></div>
</section>
<h2 class="ab-rechnungen-titel hidden" id="invoices-title">Rechnungen</h2>
<div class="kb-panel" id="invoices-body"><p class="kb-empty">Lade…</p></div>
<p class="kb-note hidden" id="invoices-abo-note">Die Zahlungsart für dein Abo änderst du über dein <button type="button" class="kb-link" data-open-account>Profilbild</button> oben rechts unter „Abo verwalten &amp; Rechnungen“.</p>
</section>
+55 -12
View File
@@ -44,9 +44,9 @@
<div class="brand">WEBKLAR</div>
<h1>Datenschutzerklärung</h1>
<p class="meta">Stand 05.10.2026</p>
<p class="meta">Stand 22.10.2026</p>
<p>Hier erfahren Sie, welche personenbezogenen Daten wir verarbeiten, wenn Sie unsere Websites besuchen, unser Kundenportal nutzen oder mit uns einen Vertrag schließen, und welche Rechte Sie haben.</p>
<p>Hier erfahren Sie, welche personenbezogenen Daten wir verarbeiten, wenn Sie unsere Websites besuchen, unser Kundenportal nutzen, mit uns einen Vertrag schließen oder wenn wir auf Ihre öffentliche Anfrage nach einer Website antworten, und welche Rechte Sie haben.</p>
<h2>1. Verantwortlicher</h2>
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein<br>
@@ -55,6 +55,7 @@
<h2>2. Geltungsbereich</h2>
<p>Diese Erklärung gilt für die Website webklar.com, für das Kundenportal project.webklar.com einschließlich der Vorschauseiten unter *.project.webklar.com und für unsere Leistungen, insbesondere unsere Abos (Hosting-Abo und Wartungs-Abo).</p>
<p>Die Erklärung gilt auch für Menschen und Betriebe, die im Netz öffentlich nach einer Website suchen und auf deren Anfrage wir antworten (Abschnitt 11). Das können auch Privatpersonen sein, zum Beispiel in Frage-Foren.</p>
<p>Websites, die wir im Auftrag unserer Kunden betreiben, verantwortet der jeweilige Kunde. Dort verarbeiten wir Daten als Auftragsverarbeiter nach den Weisungen des Kunden. Informationen dazu finden Sie in der Datenschutzerklärung der jeweiligen Website.</p>
<h2>3. Hosting, Server-Protokolle und Überwachung der Erreichbarkeit</h2>
@@ -65,17 +66,24 @@
<p>Mit UptimeRobot haben wir einen Vertrag über die Auftragsverarbeitung geschlossen; er ist Teil der Nutzungsbedingungen von UptimeRobot. UptimeRobot verarbeitet diese Daten auch außerhalb der EU, insbesondere in den USA. Grundlage dafür sind die Standardvertragsklauseln der EU-Kommission (Art. 46 Abs. 2 lit. c DSGVO). Sie sind Teil des Auftragsverarbeitungsvertrags von UptimeRobot (uptimerobot.com/dpa); eine Kopie erhalten Sie auch bei uns. Rechtsgrundlage ist für die Websites unserer Abo-Kunden Art. 6 Abs. 1 lit. b DSGVO, weil die Überwachung zu den Leistungen des Abos gehört, im Übrigen Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt auch hier im sicheren und funktionsfähigen Betrieb. Die Adresse einer Website entfernen wir bei UptimeRobot, sobald wir die Website nicht mehr betreiben.</p>
<h2>4. Cookies und Speicher im Browser</h2>
<p>Auf unseren eigenen Seiten (webklar.com und dem Kundenportal) setzen wir keine Analyse-, Tracking- oder Marketing-Werkzeuge ein und binden keine Inhalte fremder Anbieter wie Schriftarten oder Karten ein. Alle Bestandteile unserer Seiten werden von unserem eigenen Server geladen.</p>
<p>Auf webklar.com messen wir mit unserer eigenen Website-Analyse, wie die Seite genutzt wird (Abschnitt 4a). Darüber hinaus setzen wir auf unseren eigenen Seiten (webklar.com und dem Kundenportal) keine Analyse-, Tracking- oder Marketing-Werkzeuge ein und binden keine Inhalte fremder Anbieter wie Schriftarten oder Karten ein. Alle Bestandteile unserer Seiten werden von unserem eigenen Server geladen.</p>
<table>
<tr><th>Name</th><th>Zweck</th><th>Speicherdauer</th></tr>
<tr><td>webklar_portal_session (Cookie)</td><td>hält Sie im Kundenportal angemeldet</td><td>7 Tage bzw. bis zur Abmeldung</td></tr>
<tr><td>preview_session (Cookie)</td><td>Zugang zu Ihren Vorschauseiten unter *.project.webklar.com</td><td>7 Tage bzw. bis zur Abmeldung</td></tr>
<tr><td>theme (lokaler Speicher, webklar.com)</td><td>merkt sich die von Ihnen gewählte helle oder dunkle Darstellung</td><td>bis Sie ihn im Browser löschen</td></tr>
<tr><td>wk-consent (lokaler Speicher, webklar.com)</td><td>merkt sich Ihre Auswahl im Banner der Reichweitenmessung („Annehmen“ oder „Ablehnen“) mit Datum und Fassung des Banner-Textes (Abschnitt 4a)</td><td>183 Tage, danach fragen wir erneut</td></tr>
</table>
<p>Diese Speicherungen sind unbedingt erforderlich, damit wir die von Ihnen gewünschten Funktionen bereitstellen können (§ 25 Abs. 2 Nr. 2 TDDDG). Eine Einwilligung ist dafür nicht nötig. Die Verarbeitung stützt sich auf Art. 6 Abs. 1 lit. b DSGVO (Portal) bzw. lit. f DSGVO (Darstellung).</p>
<p>Diese Speicherungen sind unbedingt erforderlich, damit wir die von Ihnen gewünschten Funktionen bereitstellen können (§ 25 Abs. 2 Nr. 2 TDDDG). Eine Einwilligung ist dafür nicht nötig. Die Verarbeitung stützt sich auf Art. 6 Abs. 1 lit. b DSGVO (Portal) bzw. lit. f DSGVO (Darstellung, Auswahl im Banner).</p>
<h2 id="reichweitenmessung">4a. Reichweitenmessung auf webklar.com</h2>
<p>Auf webklar.com messen wir mit unserer eigenen Website-Analyse, wie unsere Website genutzt wird, um sie zu verbessern. Es ist dieselbe Messung, die wir unseren Kunden anbieten (Abschnitt 6b); für webklar.com sind wir selbst Verantwortlicher. Die Messung läuft auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3), ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Beim ersten Aufruf fragt ein Banner nach Ihrer Zustimmung.</p>
<p>Solange Sie im Banner nichts ausgewählt haben, zählen wir nur den Seitenaufruf: Gespeichert werden der Seitenpfad, das Land, das wir aus Ihrer IP-Adresse ableiten, die Geräteklasse, die wir aus dem Browser-Kennzeichen ableiten, der Zeitpunkt, auf die volle Stunde gerundet, und ein Zähler dafür, dass das Banner angezeigt wurde. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen. Wählen Sie „Ablehnen“, zählen wir einmal die Ablehnung und danach nichts mehr. Sendet Ihr Browser „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner nicht an und zählen nichts.</p>
<p>Wählen Sie „Annehmen“, messen wir zusätzlich, von welcher Website Sie gekommen sind (nur der Domainname), die Größenklasse Ihres Browserfensters, wie lange Sie auf einer Seite bleiben und wie weit Sie scrollen, wo Sie klicken (Klickposition und Aufbau des angeklickten Elements, ohne Texte und Formulareingaben, für eine „Heatmap“), ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann nur mit deren Domainname), und eine Besucher-Kennung, die täglich wechselt. Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen: über „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts) und „Ablehnen“.</p>
<p>IP-Adressen und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher und speichern sie in den Messdaten nicht. Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen, zusammengefasste Tageswerte ohne Besucher-Kennung nach 25 Monaten. Alle Einzelheiten, auch zum Widerspruch und zu Ihren Rechten, finden Sie auf unserer Zusatzseite <a href="https://webklar.com/datenschutz-analyse.html">webklar.com/datenschutz-analyse.html</a>.</p>
<h2>5. Kontaktaufnahme</h2>
<p>Über das Kontaktformular auf webklar.com erheben wir Name, E-Mail-Adresse, Firma (freiwillig) und Ihre Nachricht. Die Angaben speichern wir in unserer eigenen Datenbank auf unserem Server. Schreiben Sie uns per E-Mail oder rufen Sie an, verarbeiten wir die Angaben, die Sie uns dabei mitteilen.</p>
<p>Über das Kontaktformular auf webklar.com erheben wir Name, E-Mail-Adresse, Firma (freiwillig) und Ihre Nachricht. Die Angaben speichern wir in unserer eigenen Datenbank auf unserem Server und schicken sie zugleich per E-Mail an unser Postfach (Abschnitt 8), damit wir von Ihrer Anfrage erfahren. Schreiben Sie uns per E-Mail oder rufen Sie an, verarbeiten wir die Angaben, die Sie uns dabei mitteilen.</p>
<p>Wir nutzen die Daten, um Ihre Anfrage zu beantworten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, wenn Ihre Anfrage auf einen Vertrag zielt, sonst Art. 6 Abs. 1 lit. f DSGVO (Interesse an der Beantwortung). Entsteht kein Vertrag, löschen wir die Anfrage spätestens sechs Monate nach der letzten Nachricht.</p>
<h2>6. Kundenportal</h2>
@@ -108,6 +116,12 @@
<p>Zahlungen im Webshop laufen über Stripe Connect auf das eigene Stripe-Konto des Kunden; wir erhalten und verwahren kein Geld der Käufer. Um dieses Konto zu eröffnen, übermitteln wir, sobald der Kunde im Kundenportal die Verbindung mit Stripe auslöst, seine E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“), zusammen mit internen Kennzeichen, mit denen wir das Konto seinem Projekt zuordnen. Die weiteren Angaben für das Konto (zum Beispiel zur Person und zum Unternehmen) gibt der Kunde direkt bei Stripe ein. Wir speichern die Kennung des Stripe-Kontos und seinen Stand (ob Zahlungen und Auszahlungen möglich sind), den uns Stripe meldet, und zeigen ihn dem Kunden im Kundenportal an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag mit dem Kunden). Stripe verarbeitet die Daten für sein Konto als eigener Verantwortlicher; dabei können Daten an die Stripe, LLC in den USA übermittelt werden, die nach dem EU-US Data Privacy Framework zertifiziert ist (Abschnitt 7). Es gelten die Datenschutzhinweise von Stripe: stripe.com/de/privacy.</p>
<p>Löschfristen: Nicht bezahlte und stornierte Bestellungen löschen wir nach 30 Tagen. Bei bezahlten Bestellungen und Widerrufen anonymisieren wir Name, E-Mail-Adresse und Anschriften des Käufers drei Jahre nach dem Ende des Jahres der Bestellung; Positionen, Beträge, Bestellnummer und Status bleiben für die Unterlagen des Kunden. Schaltet der Kunde den Webshop ab oder endet der Vertrag über den Betrieb seiner Website, schalten wir den Webshop ab. Der Kunde kann Bestellungen und Widerrufe danach 90 Tage lang im Kundenportal lesen und exportieren; danach löschen wir alle Daten des Webshops (Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen und das Archiv der bestätigten Rechtstexte). Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schalten wir den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern. Löschen wir Daten oder anonymisieren wir sie, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3). Die Bestellung der Funktion und ihren Nachweis beschreibt Abschnitt 7.</p>
<h2 id="buchungskalender">6f. Buchungskalender auf Websites unserer Kunden</h2>
<p>Kunden können auf ihrer eigenen Website einen Buchungskalender für ihre Unterkünfte betreiben („Buchungskalender“), sobald wir die Funktion für sie freigeschaltet haben. Dabei verarbeiten wir die Daten der Personen, die dort eine Unterkunft anfragen oder buchen („Gäste“), ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden, der dafür Verantwortlicher ist (Auftragsverarbeitungsvertrag Buchungskalender). Wir verwenden diese Daten nicht für eigene Zwecke. Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal und für seine Bestellung der Funktion. Diese Datenschutzerklärung gilt für unsere eigenen Seiten und das Kundenportal. Wer auf der Website eines Kunden anfragt oder bucht, findet die Informationen dazu (Zwecke, Rechtsgrundlagen, Speicherdauer, Rechte) in der Datenschutzerklärung dieser Website. Anfragen von Gästen zu ihren Daten leiten wir an den Kunden weiter.</p>
<p>Für den Betrieb des Buchungskalenders speichern wir im Auftrag des Kunden: bei einer Anfrage Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und Nachricht, die Unterkunft, den Zeitraum und die Zahl der Personen sowie bis zur Entscheidung des Kunden einen Hashwert der IP-Adresse (siehe unten); nach der Freigabe die Posten und den Preis des Aufenthalts, die Stornobedingungen und die Fassung der Rechtstexte, die galten; bei der Buchung die Rechnungsadresse, die Zahlart und die Rechnung (und bei einer Stornierung die Stornorechnung) mit lückenloser Rechnungsnummer, in der der Kunde als Aussteller genannt ist; den Stand der Buchung (zum Beispiel gebucht, bezahlt, storniert) mit den Zeitpunkten, bei einer Online-Zahlung die Kennungen der Zahlung bei Stripe und erstattete Beträge, auch automatische Erstattungen (siehe unten). Zahlungsdaten wie die Kartennummer gibt der Gast nur auf der Zahlungsseite von Stripe ein; wir erhalten sie nicht (Online-Zahlung derzeit nur per Karte, auch mit Apple Pay und Google Pay). Der Kunde sieht und bearbeitet Anfragen, Buchungen und Rechnungen im Kundenportal. Im Namen des Kunden stellen wir die Rechnungen aus (Rechnung im Namen und im Auftrag des Kunden, § 14 Abs. 2 Satz 4 UStG) und senden E-Mails an Gäste (Eingang der Anfrage, Freigabe mit Link zur Buchung, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf einer Reservierung, Stornierung) und an den Kunden (neue Anfrage, neue Buchung, Hinweis bei fehlgeschlagenem Kalenderabgleich) über den E-Mail-Dienst der Hetzner Online GmbH (Rechenzentrum Nürnberg, Abschnitt 8). Als Absender erscheint der Kunde, Antworten gehen an ihn. Bis zum Versand liegt jede E-Mail in einer Warteschlange; nach dem Versand löschen wir dort Empfängeradresse und Inhalt, den Eintrag selbst nach sieben Tagen; E-Mails, die nicht zugestellt werden konnten, löschen wir nach 48 Stunden. Der Link zur Buchungsseite enthält ein geheimes Kennzeichen, das wir nur als Prüfsumme speichern; bei einer Online-Zahlung übergeben wir den Link mit dem Kennzeichen als Rücksprungadresse an Stripe, sodass er für Stripe und im Stripe-Dashboard des Kunden sichtbar ist (er gilt bis 90 Tage nach der Abreise, bei nicht zustande gekommenen Buchungen 30 Tage, und öffnet nur diese eine Buchungsseite). Das Widget setzt keine Cookies. Der Kunde kann Kalender anderer Plattformen einbinden und die geheime Kalenderadresse des Buchungskalenders bei ihnen eintragen; wir lesen aus fremden Kalendern nur belegte Zeiträume, nicht Angaben zu Gästen, und die Adresse des Buchungskalenders enthält nur belegte Nächte. Zum Schutz vor Missbrauch begrenzen wir die Zahl der Anfragen je IP-Adresse; die Zeitpunkte der Aufrufe merken wir uns nur im Arbeitsspeicher des Servers, nicht dauerhaft. Mit der Anfrage speichern wir nicht die IP-Adresse, sondern einen Hashwert davon, der mit einem geheimen Schlüssel unseres Servers berechnet wird (Pseudonym); ohne den Schlüssel lässt sich daraus keine IP-Adresse ableiten. Wir verwenden ihn nur für den Missbrauchsschutz und entfernen ihn, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Im Übrigen gelten die Protokolle in Abschnitt 3.</p>
<p>Online-Zahlungen im Buchungskalender laufen über Stripe Connect unmittelbar auf das eigene Stripe-Konto des Kunden; wir erhalten und verwahren kein Geld der Gäste und beteiligen uns nicht am Umsatz. Beim Start einer Zahlung übergeben wir Stripe die Bezeichnung des Aufenthalts mit Rechnungsnummer, den Betrag, die Kennungen der Buchung, die E-Mail-Adresse des Gastes zur Vorbelegung und den Rücksprung-Link zur Buchungsseite. Wird ein Aufenthalt storniert, schließen wir eine offene Zahlungsseite. Geht eine Zahlung nach einer Stornierung oder dem Ablauf der Reservierung ein oder zahlt der Gast eine bezahlte Buchung ein zweites Mal, erstatten wir den Betrag im Namen des Kunden und nach seiner dauerhaften Weisung automatisch in voller Höhe über sein Stripe-Konto; der Stand der Buchung ändert sich dadurch nicht. Um dieses Konto zu eröffnen, übermitteln wir, sobald der Kunde im Kundenportal die Verbindung mit Stripe auslöst, seine E-Mail-Adresse, die Adresse seiner Website und den Namen seines Projekts an die Stripe Payments Europe, Limited, 1 Grand Canal Street Lower, Grand Canal Dock, Dublin, D02 H210, Irland („Stripe“), zusammen mit internen Kennzeichen, mit denen wir das Konto seinem Projekt zuordnen. Die weiteren Angaben für das Konto gibt der Kunde direkt bei Stripe ein. Wir speichern die Kennung des Stripe-Kontos und seinen Stand (ob Zahlungen und Auszahlungen möglich sind), den uns Stripe meldet, und zeigen ihn dem Kunden im Kundenportal an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag mit dem Kunden). Stripe verarbeitet die Daten für sein Konto als eigener Verantwortlicher; dabei können Daten an die Stripe, LLC in den USA übermittelt werden, die nach dem EU-US Data Privacy Framework zertifiziert ist (Abschnitt 7). Es gelten die Datenschutzhinweise von Stripe: stripe.com/de/privacy.</p>
<p>Löschfristen: Den IP-Hash entfernen wir, sobald der Kunde eine Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen. Anfragen, aus denen keine Buchung wurde, anonymisieren wir nach sechs Monaten. Bei gebuchten Aufenthalten anonymisieren wir Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht drei Jahre nach dem Ende des Jahres der Abreise; die Rechnungen und die Rechnungsadresse bewahren wir für den Kunden acht Jahre auf (Buchungsbelege, § 147 AO), solange der Buchungskalender läuft. Schaltet der Kunde den Buchungskalender ab oder endet der Vertrag über den Betrieb seiner Website, schalten wir den Buchungskalender ab und weisen den Kunden danach per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen herunterzuladen. Der Kunde kann Anfragen, Buchungen und Rechnungen danach 90 Tage lang im Kundenportal lesen; danach löschen wir alle Daten des Buchungskalenders (Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen). Wird das Kundenkonto gelöscht, schalten wir den Buchungskalender ab und löschen seine Daten 90 Tage danach; einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. Beginnt innerhalb dieser Frist wieder ein Vertrag über den Betrieb der Website, schalten wir den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen. Löschen wir Daten oder anonymisieren wir sie, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Daten liegen auf unserem Server bei Hetzner im Rechenzentrum Falkenstein (Deutschland), wie die übrigen Portal-Daten (Abschnitt 3). Die Bestellung der Funktion und ihren Nachweis beschreibt Abschnitt 7.</p>
<h2>7. Abos und Zahlung über Stripe</h2>
<p>Schließen Sie eines unserer Abos ab, verarbeiten wir Name, E-Mail-Adresse, Rechnungsanschrift, das gewählte Abo und den gebuchten Zusatzspeicher, Abo- und Zahlungsstatus sowie Ihre Rechnungen. Zahlungsdaten wie Kartennummer oder IBAN geben Sie direkt bei unserem Zahlungsdienstleister ein. Wir erhalten davon nur eingeschränkte Angaben, etwa die Art des Zahlungsmittels und die letzten vier Ziffern.</p>
<p>Um Ihre Bestellungen nachweisen zu können, speichern wir bei jeder Bestellung Zeitpunkt, Version der Vertragsbedingungen, gegebenenfalls Ihre Angaben zum Leistungsbeginn sowie IP-Adresse und Browserkennung, im Kundenportal und als Angaben zum Abo bei Stripe. Das gilt für den Abschluss eines Abos, für den Wechsel vom Hosting-Abo ins Wartungs-Abo und für jede Bestellung von E-Mail-Zusatzspeicher. Außerdem halten wir Ihre Bestellungen und Änderungen als Vertragsereignisse mit Zeitpunkt und Status fest: Abschluss, Wechsel, Zusatzspeicher einschließlich Verringerungen, Kündigung und Widerruf. Bei Verringerungen, Kündigung und Widerruf speichern wir als Vertragsereignis nur Zeitpunkt und Status, keine IP-Adresse und Browserkennung. Den Stand Ihrer Bestellungen, etwa laufende Widerrufsfristen und die Einrichtung von Zusatzspeicher, sehen Sie im Kundenportal unter „Verträge“. Für uns sind die Vertragsereignisse zugleich die Aufgabenliste, um Änderungen am Zusatzspeicher umzusetzen. Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO. Unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und Ihrer Bestellungen. Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet.</p>
@@ -128,14 +142,43 @@
<h2>10. Domains unserer Kunden</h2>
<p>Registrieren wir für Sie eine Domain, geben wir die Daten des Domaininhabers (Name, Anschrift, E-Mail-Adresse, Telefonnummer) an den Domain-Registrar, die Hetzner Online GmbH, und an die zuständige Vergabestelle weiter, bei .de-Domains an die DENIC eG in Frankfurt am Main. Sie verarbeiten die Daten als eigene Verantwortliche. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.</p>
<h2>11. Interessenten</h2>
<p>Sprechen wir Unternehmen als mögliche Kunden an, verarbeiten wir öffentlich verfügbare Geschäftsdaten: Firmenname, Ansprechpartner, geschäftliche Kontaktdaten und Angaben zur bestehenden Website. Quellen sind etwa die Website des Unternehmens und öffentliche Branchenverzeichnisse. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Anbahnung von Geschäftsbeziehungen. Besteht kein Interesse oder widersprechen Sie, löschen wir die Daten. Ohne Kontakt löschen wir sie spätestens nach zwölf Monaten.</p>
<a id="interessenten"></a>
<h2>11. Interessenten: Antworten auf öffentliche Anfragen</h2>
<p>Manche Menschen und Betriebe suchen öffentlich im Netz jemanden, der ihnen eine Website baut, zum Beispiel in einer Kleinanzeige, auf einem Portal für Freelancer oder in einem Frage-Forum. Auf solche Gesuche melden wir uns von uns aus nur einmal und bieten unsere Leistungen an. Eine Ausnahme sind Frage-Foren: Dort antworten wir nicht im Forum (siehe unten). Weil wir Ihre Daten dabei nicht bei Ihnen erheben, sondern aus Ihrem öffentlichen Beitrag, informieren wir Sie hier und mit einem Hinweis am Ende unserer Antwort darüber (Art. 14 DSGVO).</p>
<p>So läuft es ab: Ein KI-Assistent (das Sprachmodell Claude von Anthropic) bewertet, ob die Anfrage zu unseren Leistungen passt, und entwirft eine Antwort (für Beiträge aus Frage-Foren nicht, siehe unten). Ein Mitarbeiter von WEBklar liest den Entwurf, ändert ihn bei Bedarf und sendet ihn einmal von Hand: über die Plattform, auf der Sie gefragt haben, oder per E-Mail, wenn Ihr Beitrag darum bittet. Automatische E-Mails verschicken wir nicht. Für E-Mails gilt Abschnitt 8. Passt Ihre Anfrage nach der Bewertung der KI nicht zu uns, schreiben wir Ihnen nicht und kürzen die Daten spätestens am Tag nach dem Fund (das ist der Tag, an dem wir Ihre Anfrage gefunden haben; siehe unten).</p>
<p>Am Ende unserer Antwort steht ein Hinweis. Er sagt, dass eine KI-Assistentin von WEBklar antwortet, dass wir die Angaben aus Ihrer Anfrage spätestens 90 Tage nach dem Tag, an dem wir Ihre Anfrage gefunden haben, in unserer Datenbank kürzen, das heißt, Titel, Text, Link und E-Mail-Adresse löschen, und er verweist auf diesen Abschnitt. Außerdem nennt er Ihr Widerspruchsrecht: Wenn Sie keine weitere Nachricht von uns möchten oder der Speicherung widersprechen, antworten Sie uns bitte kurz; sobald wir Ihre Antwort lesen, schreiben wir Ihnen zu dieser Anfrage nicht mehr und kürzen Ihre Daten (bei einem Widerspruch sofort, sonst innerhalb von 30 Tagen). Unsere Mitarbeiter sind angewiesen, diesen Hinweis in jede Antwort aufzunehmen, auch in eine selbst geschriebene, und beim Bearbeiten eines Entwurfs nicht zu entfernen.</p>
<p>Beiträge in Frage-Foren (derzeit gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org) beantworten wir nicht im Forum. Die KI entwirft dafür keine Antwort. Ein Mitarbeiter sieht den Beitrag an und meldet sich höchstens per E-Mail, wenn der Beitrag ausdrücklich um Angebote per E-Mail bittet; unsere Mitarbeiter sind angewiesen, in diese E-Mail am Ende den Hinweis aufzunehmen, der oben beschrieben ist. Bittet der Beitrag nicht darum, melden wir uns nicht.</p>
<p>Die Gesuche finden wir auf zwei Wegen. Erstens schicken uns Plattformen auf unseren Suchauftrag Benachrichtigungs-E-Mails (derzeit Kleinanzeigen, freelancermap und freelance.de); diese Plattformen rufen wir nicht selbst ab. Zweitens findet ein KI-Assistent mit einer Websuche öffentliche Beiträge im Netz und in Frage-Foren, derzeit in den Foren gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org sowie auf anderen öffentlichen Webseiten. Die Suche und den Abruf der Seiten führt Anthropic aus. Von der Websuche und vom Abruf ausgenommen sind die Plattformen Kleinanzeigen, freelancermap, freelance.de, Facebook, Instagram, LinkedIn, Xing, Reddit, Upwork, Fiverr und MyHammer sowie Seiten, deren Betreiber automatische Abrufe in der Datei robots.txt untersagen. Wir suchen nach Gesuchen, nicht nach bestimmten Personen.</p>
<p>Wir speichern den Titel des Beitrags, den Beitragstext (höchstens 4000 Zeichen), den Link, das Datum, Ort und Postleitzahl, wenn sie genannt sind, die Plattform, eine Punktzahl mit kurzer Begründung und, wenn der Beitrag um eine Antwort per E-Mail bittet, die E-Mail-Adresse aus dem Beitrag. Wir lesen Ihren Namen nicht gesondert aus; steht er im Titel oder im Text, speichern wir ihn damit. Telefonnummern entfernen wir beim Speichern automatisch aus dem Text; das folgt einer Faustregel und erkennt nicht jede Schreibweise. Dazu kommen der Entwurf unserer Antwort, die gesendete Nachricht mit Text und Zeitpunkt und ein Protokoll der einzelnen Arbeitsschritte, zum Beispiel „Neue Anfrage: Titel des Beitrags“.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, auf öffentliche Gesuche nach Website-Leistungen zu antworten. Wir melden uns je Anfrage von uns aus nur einmal, und Sie können der Verarbeitung jederzeit widersprechen.</p>
<p>Wir speichern die Daten auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3). Für die Bewertung, den Entwurf und die Websuche setzen wir das Sprachmodell Claude der Anthropic Ireland, Limited, Dublin, Irland, als Auftragsverarbeiter ein. Anthropic erhält dafür den Titel, den Text und den Ort des Beitrags sowie bei der Websuche unsere Suchaufträge und die gefundenen Seiten. Dabei können Daten in die USA übermittelt werden, abgesichert durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO); eine Kopie erhalten Sie bei uns (support@webklar.com).</p>
<p>Die KI sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Ob und was wir einer passenden Anfrage antworten, entscheidet ein Mitarbeiter (Abschnitt 14).</p>
<p>Wie lange wir die Daten speichern: Wir kürzen Ihre Daten in unserer Datenbank</p>
<ul>
<li>spätestens 90 Tage nach dem Fund;</li>
<li>spätestens am Tag nach dem Fund, wenn Ihre Anfrage nicht zu uns passt;</li>
<li>innerhalb von 30 Tagen, nachdem wir Ihre Antwort gelesen haben, wenn Sie uns sagen, dass Sie kein Interesse haben;</li>
<li>sofort, wenn Sie widersprechen: Wir sperren und kürzen sie, sobald wir Ihren Widerspruch lesen.</li>
</ul>
<p>Kürzen heißt: Wir löschen Titel, Beitragstext, Link, E-Mail-Adresse, Entwurf und unsere Nachricht an Sie und bereinigen die zugehörigen Einträge im Protokoll. Übrig bleiben nur pseudonymisierte Prüfwerte (Hash-Werte) des Beitrags, Ihrer E-Mail-Adresse und, wenn es eine Firmenadresse ist (also nicht bei Anbietern wie gmail.com oder web.de), der Domain dieser Adresse, das ist der Teil hinter dem @, zum Beispiel firma.de. Dazu bleiben Ort, Plattform, Punktzahl, Status und die Zeitpunkte von Beitrag und Fund, jedoch ohne Titel, Text, Link und Kontaktdaten.</p>
<p>Ein Hash-Wert ist ein Prüfwert, aus dem sich der ursprüngliche Inhalt nicht ablesen lässt. Wir erkennen damit nur, wenn derselbe Beitrag, dieselbe E-Mail-Adresse oder, bei einer Firmenadresse, eine andere Adresse derselben Firma noch einmal auftaucht; wir schreiben dann nicht noch einmal und nehmen den Beitrag nicht noch einmal auf. Diese Prüfwerte bewahren wir so lange auf, wie wir verhindern müssen, dass wir Sie wegen desselben Beitrags oder derselben E-Mail-Adresse (bei einer Firmenadresse: derselben Firma) erneut ansprechen. Ort, Plattform, Punktzahl, Status und die Zeitpunkte behalten wir für unsere Auswertung, damit unsere Zähler und der zeitliche Verlauf stimmen.</p>
<p>Haben Sie uns geantwortet und übernehmen wir Ihre Angaben als Kontakt oder Kunde in unsere Kundenverwaltung, gelten für diese Daten stattdessen Abschnitt 5 und, wenn ein Vertrag zustande kommt, die Abschnitte 7 und 9.</p>
<p>Für Kopien außerhalb der Datenbank gilt Folgendes. Die Benachrichtigungs-E-Mails der Plattformen enthalten die Anzeigentexte und gehen in unserem Postfach support@webklar.com bei der Hetzner Online GmbH ein (Abschnitt 8). Unser Dienst liest dort nur den Posteingang. Er lädt dazu jede neue E-Mail dieses Postfachs kurz, um den Absender zu prüfen; übernommen und gespeichert werden nur die Benachrichtigungen der Plattformen, alle anderen E-Mails verwirft er sofort. Die Benachrichtigungs-E-Mails der Plattformen löschen wir im Postfach spätestens 90 Tage nach dem Eingang, auch wenn wir die Anfrage in der Datenbank schon früher gekürzt haben.</p>
<p>Eine E-Mail, die ein Mitarbeiter Ihnen sendet, liegt außerdem in unserem Postfach; dafür gilt Abschnitt 8 und, soweit die E-Mail ein Geschäftsbrief ist, die Frist aus Abschnitt 9. Kürzen wir Daten, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Antwort, die wir auf einer Plattform abschicken, sehen auch deren Betreiber. Dafür gelten die Datenschutzhinweise der Plattform.</p>
<div class="box widerspruch">
<strong>Ihr Widerspruchsrecht gegen diese Verarbeitung (Art. 21 DSGVO)</strong>
<p>Sie können der Verarbeitung Ihrer Daten jederzeit widersprechen, ohne Gründe anzugeben. Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com.</p>
<p>Sobald wir Ihre Nachricht lesen, sperren wir den Beitrag und, soweit wir sie gespeichert haben, Ihre E-Mail-Adresse, schreiben Ihnen dazu nicht mehr und kürzen Ihre Daten wie oben beschrieben. Ist es eine Firmenadresse und nicht die eines Anbieters wie gmail.com oder web.de, sperren wir auch deren Domain, also auch andere Adressen derselben Firma. Wir erkennen Sie dabei nur am Beitrag, an Ihrer E-Mail-Adresse und, bei einer Firmenadresse, an deren Domain: Ein neues Gesuch, das Sie später einstellen, ist für uns ein neuer Beitrag, wenn es keine dieser Angaben enthält; sagen Sie uns dann bitte noch einmal Bescheid.</p>
<p>Die Benachrichtigung der Plattform in unserem Postfach erfasst das Kürzen in der Datenbank nicht; wir löschen sie dort spätestens 90 Tage nach dem Eingang (siehe oben). Weitere Rechte (Art. 15 bis 20 DSGVO) und das Beschwerderecht beschreibt Abschnitt 13.</p>
</div>
<h2>12. Empfänger im Überblick</h2>
<ul>
<li>Hetzner Online GmbH: Hosting, E-Mail und Datensicherung (Auftragsverarbeiter)</li>
<li>Hetzner Online GmbH: Hosting, E-Mail (auch das Postfach für die Benachrichtigungen der Plattformen, Abschnitt 11) und Datensicherung (Auftragsverarbeiter)</li>
<li>UptimeRobot s. r. o.: Überwachung der Erreichbarkeit, Daten auch außerhalb der EU (insbesondere USA), Auftragsverarbeiter (Abschnitt 3)</li>
<li>Stripe: Zahlungsabwicklung und Eröffnung des Stripe-Kontos für den Webshop unserer Kunden (Abschnitte 6d und 7)</li>
<li>Stripe: Zahlungsabwicklung und Eröffnung des Stripe-Kontos für den Webshop und den Buchungskalender unserer Kunden (Abschnitte 6d, 6f und 7)</li>
<li>Anthropic Ireland, Limited: Bewertung öffentlicher Anfragen, Entwurf von Antworten sowie Websuche und Seitenabruf durch ein Sprachmodell (Auftragsverarbeiter, Daten auch in den USA, Abschnitt 11)</li>
<li>Plattformen, auf denen wir Ihre Anfrage beantworten, zum Beispiel Kleinanzeigen: Unsere Antwort läuft über deren Dienst (Abschnitt 11)</li>
<li>Porkbun LLC: bisheriger E-Mail-Dienst, nur noch bis zur Löschung älterer E-Mails (Abschnitt 8)</li>
<li>Domain-Registrar und Vergabestellen, sofern wir eine Domain für Sie registrieren (Abschnitt 10)</li>
<li>Steuerberatung und Behörden, soweit gesetzlich erforderlich</li>
@@ -143,15 +186,15 @@
<p>Wir verkaufen keine Daten und geben sie nicht zu Werbezwecken weiter.</p>
<h2>13. Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18) und Datenübertragbarkeit (Art. 20). Eine Einwilligung können Sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3). Derzeit stützen wir keine Verarbeitung auf eine Einwilligung.</p>
<p>Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18) und Datenübertragbarkeit (Art. 20). Eine Einwilligung können Sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3). Auf eine Einwilligung stützen wir derzeit nur die erweiterte Reichweitenmessung auf webklar.com (Abschnitt 4a).</p>
<div class="box widerspruch">
<strong>Widerspruchsrecht (Art. 21 DSGVO)</strong>
<p>Verarbeiten wir Ihre Daten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO, können Sie aus Gründen, die sich aus Ihrer besonderen Situation ergeben, jederzeit widersprechen. Verarbeiten wir Ihre Daten zur Direktwerbung, können Sie dem jederzeit ohne Angabe von Gründen widersprechen. Dann nutzen wir Ihre Daten nicht mehr für diesen Zweck. Ein formloser Hinweis an die Kontaktdaten in Abschnitt 1 genügt.</p>
</div>
<p>Sie können sich außerdem bei einer Datenschutz-Aufsichtsbehörde beschweren. Für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz, Hintere Bleiche 34, 55116 Mainz, poststelle@datenschutz.rlp.de.</p>
<h2>14. Pflicht zur Bereitstellung, keine automatisierten Entscheidungen</h2>
<p>Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Automatisierte Entscheidungen einschließlich Profiling im Sinne von Art. 22 DSGVO finden nicht statt.</p>
<h2>14. Pflicht zur Bereitstellung, keine automatisierten Einzelentscheidungen nach Art. 22 DSGVO</h2>
<p>Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Entscheidungen, die Ihnen gegenüber rechtliche Wirkung entfalten oder Sie in ähnlicher Weise erheblich beeinträchtigen (Art. 22 DSGVO), treffen wir nicht automatisiert. Die Bewertung öffentlicher Anfragen durch einen KI-Assistenten (Abschnitt 11) sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Für alle anderen bereitet sie höchstens eine Antwort vor; ob und was wir antworten, entscheidet ein Mitarbeiter, der die Antwort von Hand abschickt.</p>
<h2>15. Änderungen</h2>
<p>Wir passen diese Erklärung an, wenn sich unsere Verarbeitung oder die Rechtslage ändert. Es gilt die jeweils hier veröffentlichte Fassung.</p>
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Buchungskalender Testseite</title>
<style>
body{margin:0;font-family:system-ui,sans-serif;background:#f6f7f9;color:#1f2937;line-height:1.5}
header,footer{padding:16px 24px;background:#e8ebf0}
main{max-width:1180px;margin:0 auto;padding:32px 16px}
/* Gestaltung ueber CSS-Variablen der Seite (alle optional):
[data-webklar-unterkunft]{ --wku-akzent:#0f766e; --wku-radius:8px; --wku-schrift:Georgia,serif; } */
</style>
</head>
<body>
<header>Testseite der Kundenwebsite (intern, nicht verlinkt)</header>
<main>
<h1>Unterkunft anfragen</h1>
<!-- data-api weglassen = Herkunft des Skripts; die SiteKey ist ein Platzhalter -->
<div data-webklar-unterkunft="abcdefghijklmnop"></div>
</main>
<footer>Ende der Testseite</footer>
<script src="unterkunft.js" defer></script>
</body>
</html>
+504
View File
@@ -0,0 +1,504 @@
/*! WEBklar Buchungskalender: Einbau-Skript fuer Kundenwebsites.
* <div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div>
* <script src="https://project.webklar.com/embed/unterkunft.js" defer></script>
* Keine Abhaengigkeiten, keine Cookies, kein Storage, keine Drittanfragen. Alle Texte nur ueber textContent.
* Gestaltung ueber CSS-Variablen der Seite (alle optional): --wku-akzent, --wku-akzent-text, --wku-hintergrund, --wku-text,
* --wku-rand, --wku-radius, --wku-schrift, --wku-schrift-titel.
* Belegte Naechte kommen als [von,bis) (bis = Abreisetag, nicht belegt). Datumsrechnung ist reine Kalenderrechnung auf Zeichenketten
* (UTC), die Zeitzone des Browsers spielt nie eine Rolle; "heute" und das Ende des Buchungsfensters liefert der Server. */
(function () {
'use strict'
var SELF = document.currentScript
var STYLE_ID = 'wku-style'
var API_PFAD = '/api/public/unterkunft/v1/'
var AUSWEICH = 'Anfragen gerne per Telefon oder E-Mail.'
var TIMEOUT_MS = 20000
var MONAT_KURZ = ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.']
var MONAT_LANG = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']
var WOCHE_KURZ = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
var WOCHE_LANG = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
var EMAIL_FORM = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/
var FARBE = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/
var zaehler = 0
/* ------------------------------------------------------------ reine Kalenderlogik --- */
function zwei(n) { return (n < 10 ? '0' : '') + n }
function iso(j, m, t) { return j + '-' + zwei(m) + '-' + zwei(t) }
function zerlege(s) { var p = s.split('-'); return [parseInt(p[0], 10), parseInt(p[1], 10), parseInt(p[2], 10)] }
function utc(s) { var p = zerlege(s); return Date.UTC(p[0], p[1] - 1, p[2]) }
function plusTage(s, n) {
var d = new Date(utc(s) + n * 86400000)
return iso(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())
}
function tageZwischen(a, b) { return Math.round((utc(b) - utc(a)) / 86400000) }
function wochentag(s) { return (new Date(utc(s)).getUTCDay() + 6) % 7 } // 0 = Montag
function tageImMonat(j, m) { return new Date(Date.UTC(j, m, 0)).getUTCDate() }
var WKU_LOGIK = {
monatsRaster: function (jahr, monat) {
var r = []
var lead = wochentag(iso(jahr, monat, 1))
var n = tageImMonat(jahr, monat)
var i
for (i = 0; i < lead; i += 1) r.push({ tag: null })
for (i = 1; i <= n; i += 1) r.push({ tag: iso(jahr, monat, i) })
while (r.length % 7) r.push({ tag: null })
return r
},
nachtBelegt: function (tag, belegt) {
for (var i = 0; i < (belegt || []).length; i += 1) if (belegt[i].von <= tag && tag < belegt[i].bis) return true
return false
},
waehle: function (zustand, tag, belegt, mindestNaechte) {
var z = zustand || {}
var L = WKU_LOGIK
var neu = function () { return L.nachtBelegt(tag, belegt) ? { von: z.von || null, bis: z.bis || null } : { von: tag, bis: null } }
if (!z.von || z.bis || tag <= z.von) return neu()
for (var d = z.von; d < tag; d = plusTage(d, 1)) if (L.nachtBelegt(d, belegt)) return neu()
if (tageZwischen(z.von, tag) < (mindestNaechte || 1)) return { von: z.von, bis: null, fehler: 'mindestnaechte' }
return { von: z.von, bis: tag }
},
zusammenfassung: function (von, bis) {
var a = zerlege(von)
var b = zerlege(bis)
var n = tageZwischen(von, bis)
var tail = ' · ' + n + (n === 1 ? ' Nacht' : ' Nächte')
if (a[0] !== b[0]) return a[2] + '. ' + MONAT_KURZ[a[1] - 1] + ' ' + a[0] + ' – ' + b[2] + '. ' + MONAT_KURZ[b[1] - 1] + ' ' + b[0] + tail
if (a[1] === b[1]) return a[2] + '.–' + b[2] + '. ' + MONAT_KURZ[a[1] - 1] + tail
return a[2] + '. ' + MONAT_KURZ[a[1] - 1] + ' – ' + b[2] + '. ' + MONAT_KURZ[b[1] - 1] + tail
}
}
var testModus = false
try { testModus = !!(SELF && SELF.dataset && SELF.dataset.test === '1') } catch (e) { testModus = false }
if (testModus) globalThis.__wkuLogik = WKU_LOGIK
function langesDatum(s) {
var p = zerlege(s)
return WOCHE_LANG[wochentag(s)] + ', ' + p[2] + '. ' + MONAT_LANG[p[1] - 1] + ' ' + p[0]
}
/* ------------------------------------------------------------------- Gestaltung --- */
var CSS = [
'.wku-root{--wku-i-akzent:var(--wku-akzent,var(--wku-vorgabe-akzent,#0f766e));--wku-i-akzent-text:var(--wku-akzent-text,var(--wku-vorgabe-akzent-text,#fff));',
'--wku-i-bg:var(--wku-hintergrund,#fff);--wku-i-text:var(--wku-text,#1f2937);--wku-i-rand:var(--wku-rand,#b6bcc6);--wku-i-radius:var(--wku-radius,12px);',
'--wku-i-weich:#f3f4f6;--wku-i-bereich:#e5eceb;--wku-i-fehler:#b91c1c;--wku-i-schrift:var(--wku-schrift,inherit);--wku-i-titel:var(--wku-schrift-titel,var(--wku-schrift,inherit))}',
'@supports (color:color-mix(in srgb,red,blue)){.wku-root{--wku-i-weich:color-mix(in srgb,var(--wku-i-text) 6%,var(--wku-i-bg));--wku-i-bereich:color-mix(in srgb,var(--wku-i-akzent) 18%,var(--wku-i-bg))}}',
'.wku-root{container:wku/inline-size;display:block;position:relative;box-sizing:border-box;color:var(--wku-i-text);font-family:var(--wku-i-schrift);font-size:16px;line-height:1.5;text-align:left;width:100%;max-width:100%}',
'.wku-root :is(button,input,select,textarea,label,h1,h2,h3,h4,h5,h6,p,a,fieldset,legend){all:revert;box-sizing:border-box}',
'.wku-root [hidden]{display:none}',
'.wku-root *,.wku-root *::before,.wku-root *::after{box-sizing:border-box}',
'.wku-root .wku-vh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}',
'.wku-root .wku-haupt{display:grid;grid-template-columns:minmax(0,1fr);gap:24px}',
'.wku-root .wku-karte{background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:var(--wku-i-radius);padding:20px}',
'.wku-root .wku-titel{font-family:var(--wku-i-titel);font-size:1.2rem;line-height:1.25;font-weight:700;margin:0 0 .75rem}',
'.wku-root.wku-ausweich{margin:0}',
'.wku-root .wku-monate{display:grid;grid-template-columns:minmax(0,1fr);gap:20px}',
'.wku-root .wku-zwei{display:none}',
'.wku-root .wku-monat-kopf{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:44px;margin-bottom:.35rem}',
'.wku-root .wku-monat-name{font-family:var(--wku-i-titel);font-size:1.05rem;font-weight:700;margin:0;flex:1;text-align:center}',
'.wku-root .wku-pfeil{font:inherit;font-size:1.4rem;line-height:1;min-width:44px;min-height:44px;border-radius:50%;border:1px solid var(--wku-i-rand);background:var(--wku-i-bg);color:inherit;cursor:pointer;padding:0}',
'.wku-root .wku-pfeil:hover{background:var(--wku-i-weich)}.wku-root .wku-pfeil[disabled]{opacity:.3;cursor:default}.wku-root .wku-pfeil-leer{display:inline-block;width:44px;height:44px}',
'.wku-root .wku-raster{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));row-gap:2px}',
'.wku-root .wku-wt{font-size:.78rem;font-weight:600;text-align:center;padding:.25rem 0;opacity:.7}',
'.wku-root .wku-leer{min-height:44px}',
'.wku-root .wku-tag{font:inherit;position:relative;min-height:44px;padding:0;margin:0;border:0;border-radius:0;background:transparent;color:inherit;text-align:center;cursor:pointer;overflow:hidden}',
'.wku-root .wku-tag>span{position:relative;display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;border:1px solid transparent;z-index:1}',
'.wku-root .wku-tag:hover>span{background:var(--wku-i-weich)}',
'.wku-root .wku-heute>span{border-color:var(--wku-i-text)}',
'.wku-root .wku-vergangen,.wku-root .wku-zu{opacity:.35;cursor:default}.wku-root .wku-vergangen:hover>span,.wku-root .wku-zu:hover>span{background:transparent}',
'.wku-root .wku-belegt{color:#6b7280;cursor:default;background:linear-gradient(to top right,transparent calc(50% - 1px),#9ca3af calc(50% - 1px),#9ca3af calc(50% + 1px),transparent calc(50% + 1px)) center/60% 60% no-repeat}',
'.wku-root .wku-belegt:hover>span{background:transparent}',
'.wku-root .wku-bereich{background:var(--wku-i-bereich)}',
'.wku-root .wku-an{background:linear-gradient(to right,transparent 50%,var(--wku-i-bereich) 50%)}',
'.wku-root .wku-ab{background:linear-gradient(to left,transparent 50%,var(--wku-i-bereich) 50%)}',
'.wku-root .wku-an.wku-ab{background:transparent}',
'.wku-root .wku-an>span,.wku-root .wku-ab>span,.wku-root .wku-an:hover>span,.wku-root .wku-ab:hover>span{background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);border-color:var(--wku-i-akzent)}',
'.wku-root :focus-visible{outline:3px solid var(--wku-i-text);outline-offset:2px}',
'.wku-root .wku-legende{display:flex;flex-wrap:wrap;gap:.25rem 1rem;margin:.75rem 0 0;font-size:.8rem;opacity:.8}',
'.wku-root .wku-zus{margin:1rem 0 0;font-weight:700;min-height:1.5rem}',
'.wku-root .wku-hinweis{margin:.25rem 0 0;font-size:.9rem;color:var(--wku-i-fehler);font-weight:600;min-height:1.4rem}',
'.wku-root .wku-form{margin:0}',
'.wku-root .wku-feld{margin:0 0 .9rem}',
'.wku-root .wku-label{display:block;font-weight:600;font-size:.92rem;margin-bottom:.25rem}',
'.wku-root .wku-stern{color:var(--wku-i-fehler)}',
'.wku-root .wku-eingabe{display:block;width:100%;min-height:44px;font:inherit;color:inherit;background:var(--wku-i-bg);border:1px solid var(--wku-i-rand);border-radius:calc(var(--wku-i-radius) * .7);padding:.5rem .7rem}',
'.wku-root textarea.wku-eingabe{min-height:5.5rem;resize:vertical}',
'.wku-root .wku-eingabe[aria-invalid="true"]{border-color:var(--wku-i-fehler);border-width:2px}',
'.wku-root .wku-feldfehler{color:var(--wku-i-fehler);font-weight:600;font-size:.88rem;margin:.25rem 0 0}',
'.wku-root .wku-reihe{display:grid;grid-template-columns:minmax(0,1fr);gap:0 12px}',
'.wku-root .wku-fehler{margin:0 0 .75rem;padding:.6rem .8rem;border-left:4px solid var(--wku-i-fehler);background:var(--wku-i-weich);font-weight:600}',
'.wku-root .wku-datenschutz{font-size:.85rem;margin:0 0 1rem;opacity:.9}.wku-root .wku-datenschutz a{color:inherit}',
'.wku-root .wku-btn{font:inherit;font-weight:700;min-height:48px;padding:.6rem 1.5rem;border-radius:var(--wku-i-radius);border:2px solid var(--wku-i-akzent);background:var(--wku-i-akzent);color:var(--wku-i-akzent-text);cursor:pointer;line-height:1.25;width:100%}',
'.wku-root .wku-btn:hover{filter:brightness(1.08)}.wku-root .wku-btn[disabled]{opacity:.6;cursor:default}',
'.wku-root .wku-danke{margin:0;font-size:1.05rem;font-weight:600}',
'.wku-root .wku-hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}',
'@container wku (min-width:520px){.wku-root .wku-reihe{grid-template-columns:1fr 1fr}}',
'@container wku (min-width:760px){.wku-root .wku-haupt{grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);align-items:start}}',
'@container wku (min-width:1100px){.wku-root .wku-zwei{display:block}.wku-root .wku-monate{grid-template-columns:1fr 1fr}.wku-root .wku-weiter-ein{display:none}}',
'@media (prefers-reduced-motion:no-preference){.wku-root .wku-btn,.wku-root .wku-tag>span{transition:background-color .15s}}'
].join('')
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return
var st = document.createElement('style')
st.id = STYLE_ID
st.textContent = CSS
;(document.head || document.documentElement).appendChild(st)
}
/* ------------------------------------------------------------------------ Helfer --- */
function h(tag, attrs, kinder) {
var el = document.createElement(tag)
var k
attrs = attrs || {}
for (k in attrs) {
if (!Object.prototype.hasOwnProperty.call(attrs, k) || attrs[k] === null || attrs[k] === undefined || attrs[k] === false) continue
if (k === 'text') el.textContent = attrs[k]
else if (k === 'class') el.className = attrs[k]
else el.setAttribute(k, attrs[k] === true ? '' : String(attrs[k]))
}
;(kinder || []).forEach(function (c) { if (c) el.appendChild(c) })
return el
}
function apiBasis(el) {
var a = el.getAttribute('data-api')
if (!a && SELF && SELF.src) { var m = /^(https?:\/\/[^\/]+)/.exec(SELF.src); a = m ? m[1] : '' }
return (a || '').replace(/\/+$/, '')
}
function holen(url, opt) {
var ctl = typeof AbortController === 'function' ? new AbortController() : null
var t = ctl ? setTimeout(function () { ctl.abort() }, TIMEOUT_MS) : null
var o = opt || {}
if (ctl) o.signal = ctl.signal
return fetch(url, o).then(function (r) {
if (t) clearTimeout(t)
return r.json().then(function (body) { return { status: r.status, ok: r.ok, body: body || {} } }, function () { return { status: r.status, ok: r.ok, body: {} } })
}, function (e) { if (t) clearTimeout(t); throw e })
}
function textFarbe(hex) {
var s = hex.slice(1)
if (s.length === 3) s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
var r = parseInt(s.slice(0, 2), 16); var g = parseInt(s.slice(2, 4), 16); var b = parseInt(s.slice(4, 6), 16)
return (0.299 * r + 0.587 * g + 0.114 * b) > 160 ? '#111827' : '#ffffff'
}
function ausweich(el) {
el.textContent = ''
el.appendChild(h('p', { class: 'wku-root wku-ausweich', text: AUSWEICH }))
}
/* ------------------------------------------------------------------------ Widget --- */
function starten(el) {
if (el.getAttribute('data-wku-fertig')) return
el.setAttribute('data-wku-fertig', '1')
var siteKey = el.getAttribute('data-webklar-unterkunft')
if (!/^[A-Za-z0-9_-]{8,64}$/.test(siteKey || '')) return
ensureStyle()
var basis = apiBasis(el) + API_PFAD + encodeURIComponent(siteKey)
holen(basis, { headers: { Accept: 'application/json' } }).then(function (r) {
if (!r.ok || !r.body || r.body.aktiv !== true || !r.body.unterkuenfte || !r.body.unterkuenfte.length) return ausweich(el)
bauen(el, basis, r.body)
}, function () { ausweich(el) })
}
function bauen(el, basis, cfg) {
var id = 'wku' + (zaehler += 1) + '-'
var heute = null
var bisMax = null
var belegt = []
var sel = { von: null, bis: null }
var hinweis = ''
var unterk = cfg.unterkuenfte[0]
var token = cfg.formToken
var ansicht = null // { j, m } erster angezeigter Monat
var fokusTag = null
var sendet = false
var root = h('div', { class: 'wku-root' })
if (cfg.akzentfarbe && FARBE.test(cfg.akzentfarbe) && root.style) {
root.style.setProperty('--wku-vorgabe-akzent', cfg.akzentfarbe)
root.style.setProperty('--wku-vorgabe-akzent-text', textFarbe(cfg.akzentfarbe))
}
var haupt = h('div', { class: 'wku-haupt' })
var kal = h('div', { class: 'wku-karte' })
var formBox = h('div', { class: 'wku-karte' })
var zus = h('p', { class: 'wku-zus', 'aria-live': 'polite' })
var hint = h('p', { class: 'wku-hinweis', 'aria-live': 'polite' })
var monate = h('div', { class: 'wku-monate' })
kal.appendChild(h('h2', { class: 'wku-titel', text: 'Zeitraum wählen' }))
kal.appendChild(monate)
kal.appendChild(h('div', { class: 'wku-legende' }, [h('span', { text: 'Grau durchgestrichen: belegt' }), h('span', { text: 'Umrandet: heute' })]))
kal.appendChild(zus)
kal.appendChild(hint)
haupt.appendChild(kal)
haupt.appendChild(formBox)
root.appendChild(haupt)
el.textContent = ''
el.appendChild(root)
/* ---- Formular ---- */
var feld = {}
var fehlerBox = h('p', { class: 'wku-fehler', role: 'alert', hidden: true })
var form = h('form', { class: 'wku-form', novalidate: true })
var selU = null
var selP = h('select', { class: 'wku-eingabe', id: id + 'personen', name: 'personen' })
function eingabe(name, label, opt) {
opt = opt || {}
var inp = h(opt.mehrzeilig ? 'textarea' : 'input', { class: 'wku-eingabe', id: id + name, name: name, type: opt.mehrzeilig ? null : (opt.type || 'text'), autocomplete: opt.auto || 'off', maxlength: opt.max || 200, required: opt.pflicht ? true : null, rows: opt.mehrzeilig ? 4 : null })
var fe = h('p', { class: 'wku-feldfehler', id: id + name + '-f', hidden: true })
var box = h('div', { class: 'wku-feld' }, [h('label', { class: 'wku-label', for: id + name }, [h('span', { text: label }), opt.pflicht ? h('span', { class: 'wku-stern', 'aria-hidden': 'true', text: ' *' }) : null]), inp, fe])
feld[name] = { inp: inp, fe: fe }
return box
}
function fuellePersonen() {
selP.textContent = ''
var max = Math.max(1, parseInt(unterk.maxPersonen, 10) || 1)
for (var i = 1; i <= max; i += 1) selP.appendChild(h('option', { value: i, text: i === 1 ? '1 Person' : i + ' Personen' }))
}
if (cfg.unterkuenfte.length > 1) {
selU = h('select', { class: 'wku-eingabe', id: id + 'unterkunft', name: 'unterkunft' })
cfg.unterkuenfte.forEach(function (u, i) { selU.appendChild(h('option', { value: i, text: u.name })) })
form.appendChild(h('div', { class: 'wku-feld' }, [h('label', { class: 'wku-label', for: id + 'unterkunft', text: 'Unterkunft' }), selU]))
}
form.appendChild(h('div', { class: 'wku-feld' }, [h('label', { class: 'wku-label', for: id + 'personen', text: 'Personen' }), selP]))
form.appendChild(eingabe('firma', 'Firma (optional)', { auto: 'organization' }))
form.appendChild(eingabe('name', 'Name', { pflicht: true, auto: 'name' }))
form.appendChild(h('div', { class: 'wku-reihe' }, [eingabe('email', 'E-Mail', { pflicht: true, type: 'email', auto: 'email' }), eingabe('telefon', 'Telefon (optional)', { type: 'tel', auto: 'tel', max: 50 })]))
form.appendChild(eingabe('nachricht', 'Nachricht (optional)', { mehrzeilig: true, max: 2000 }))
var hp = h('input', { type: 'text', name: 'kontakt_url', id: id + 'hp', tabindex: '-1', autocomplete: 'off' })
form.appendChild(h('div', { class: 'wku-hp', 'aria-hidden': 'true' }, [hp]))
var ds = h('p', { class: 'wku-datenschutz' })
var anb = cfg.anbieter || {}
ds.appendChild(document.createTextNode('Ihre Angaben werden zur Bearbeitung Ihrer Anfrage verwendet' + (anb.name ? ' (' + anb.name + ')' : '') + '. Mehr dazu in der '))
if (anb.datenschutzUrl && /^https?:\/\//i.test(anb.datenschutzUrl)) ds.appendChild(h('a', { href: anb.datenschutzUrl, target: '_blank', rel: 'noopener noreferrer', text: 'Datenschutzerklärung' }))
else ds.appendChild(document.createTextNode('Datenschutzerklärung'))
ds.appendChild(document.createTextNode('.'))
form.appendChild(ds)
var btn = h('button', { class: 'wku-btn', type: 'submit', text: 'Anfrage senden' })
form.appendChild(fehlerBox)
form.appendChild(btn)
formBox.appendChild(h('h2', { class: 'wku-titel', text: 'Unverbindlich anfragen' }))
formBox.appendChild(form)
fuellePersonen()
function zeigeFehler(text) { fehlerBox.textContent = text || ''; fehlerBox.hidden = !text }
function feldFehler(name, text) {
var f = feld[name]
if (!f) return false
f.fe.textContent = text || ''
f.fe.hidden = !text
if (text) { f.inp.setAttribute('aria-invalid', 'true'); f.inp.setAttribute('aria-describedby', f.fe.id) } else { f.inp.removeAttribute('aria-invalid'); f.inp.removeAttribute('aria-describedby') }
return true
}
/* ---- Kalender ---- */
function monatKey(j, m) { return j * 12 + (m - 1) }
function zweiSichtbar() { return (root.offsetWidth || root.clientWidth || 0) >= 1100 }
function aktualisiereZus() {
if (sel.von && sel.bis) { zus.textContent = WKU_LOGIK.zusammenfassung(sel.von, sel.bis); hint.textContent = '' }
else if (sel.von) { zus.textContent = 'Anreise ' + langesDatum(sel.von) + '. Bitte Abreise wählen.'; hint.textContent = hinweis }
else { zus.textContent = ''; hint.textContent = '' }
if (hinweis && !sel.bis) hint.textContent = hinweis
}
function tageBisFrei(von, bis) {
for (var d = von; d < bis; d = plusTage(d, 1)) if (WKU_LOGIK.nachtBelegt(d, belegt)) return false
return true
}
function tagKnopf(tag) {
var p = zerlege(tag)
var vergangen = tag < heute
var zu = tag > bisMax
var nb = WKU_LOGIK.nachtBelegt(tag, belegt)
var kl = ['wku-tag']
if (tag === heute) kl.push('wku-heute')
if (vergangen) kl.push('wku-vergangen')
if (zu) kl.push('wku-zu')
if (nb && !vergangen && !zu) kl.push('wku-belegt')
var istAn = sel.von === tag
var istAb = sel.bis === tag
if (istAn && sel.bis) kl.push('wku-an')
else if (istAn) kl.push('wku-an', 'wku-ab')
if (istAb) kl.push('wku-ab')
if (sel.von && sel.bis && tag > sel.von && tag < sel.bis) kl.push('wku-bereich')
var status = (nb ? (sel.von && !sel.bis && tag > sel.von && !vergangen && !zu && !WKU_LOGIK.nachtBelegt(sel.von, belegt) && tageBisFrei(sel.von, tag) ? 'belegt – als Abreisetag möglich' : 'belegt') : 'frei') + (istAn ? ', Anreise' : '') + (istAb ? ', Abreise' : '')
var b = h('button', { class: kl.join(' '), type: 'button', 'data-tag': tag, 'aria-label': langesDatum(tag) + ', ' + status, 'aria-pressed': (istAn || istAb) ? 'true' : 'false', tabindex: tag === fokusTag ? '0' : '-1' }, [h('span', { text: String(p[2]) })])
if (vergangen || zu) { b.setAttribute('aria-disabled', 'true') }
b.addEventListener('click', function () { tagKlick(tag) })
return b
}
function monatBauen(j, m, erster) {
var kopf = h('div', { class: 'wku-monat-kopf' })
var min = monatKey(heute.slice(0, 4) * 1, heute.slice(5, 7) * 1)
var max = monatKey(bisMax.slice(0, 4) * 1, bisMax.slice(5, 7) * 1)
var zur
if (erster) {
zur = h('button', { class: 'wku-pfeil', type: 'button', 'data-nav': 'zur', 'aria-label': 'Vorheriger Monat', text: '‹' })
if (monatKey(j, m) <= min) zur.setAttribute('disabled', '')
zur.addEventListener('click', function () { blaettern(-1) })
kopf.appendChild(zur)
} else kopf.appendChild(h('span', { class: 'wku-pfeil-leer' }))
kopf.appendChild(h('h3', { class: 'wku-monat-name', text: MONAT_LANG[m - 1] + ' ' + j }))
// Erster Monat: immer ein echter Weiter-Knopf (bei zwei sichtbaren Monaten per CSS ausgeblendet, dann sitzt er im zweiten Monat).
var vor = h('button', { class: 'wku-pfeil' + (erster ? ' wku-weiter-ein' : ''), type: 'button', 'data-nav': erster ? 'vor1' : 'vor2', 'aria-label': 'Nächster Monat', text: '›' })
if (monatKey(j, m) >= max) vor.setAttribute('disabled', '')
vor.addEventListener('click', function () { blaettern(1) })
kopf.appendChild(vor)
var grid = h('div', { class: 'wku-raster', role: 'group', 'aria-label': MONAT_LANG[m - 1] + ' ' + j })
WOCHE_KURZ.forEach(function (w, i) { grid.appendChild(h('div', { class: 'wku-wt', 'aria-hidden': 'true', text: w, title: WOCHE_LANG[i] })) })
WKU_LOGIK.monatsRaster(j, m).forEach(function (z) { grid.appendChild(z.tag ? tagKnopf(z.tag) : h('div', { class: 'wku-leer' })) })
return h('div', { class: 'wku-monat' + (erster ? '' : ' wku-zwei') }, [kopf, grid])
}
function zeichne(fokus) {
if (!heute) return
var a = ansicht
var b = a.m === 12 ? { j: a.j + 1, m: 1 } : { j: a.j, m: a.m + 1 }
monate.textContent = ''
monate.appendChild(monatBauen(a.j, a.m, true))
monate.appendChild(monatBauen(b.j, b.m, false))
aktualisiereZus()
if (fokus && fokusTag) {
var k = monate.querySelector('[data-tag="' + fokusTag + '"]')
if (k && k.focus) k.focus()
}
}
function blaettern(d) {
var m = ansicht.m + d
var j = ansicht.j
if (m < 1) { m = 12; j -= 1 } else if (m > 12) { m = 1; j += 1 }
var min = monatKey(heute.slice(0, 4) * 1, heute.slice(5, 7) * 1)
var max = monatKey(bisMax.slice(0, 4) * 1, bisMax.slice(5, 7) * 1)
if (monatKey(j, m) < min || monatKey(j, m) > max) return
ansicht = { j: j, m: m }
var erster = iso(j, m, 1)
fokusTag = erster < heute ? heute : erster
zeichne(false)
var zwei = zweiSichtbar()
var wunsch = d > 0 ? (zwei ? 'vor2' : 'vor1') : 'zur'
var knopf = monate.querySelector('[data-nav="' + wunsch + '"]')
if (!knopf || knopf.hasAttribute('disabled')) knopf = monate.querySelector(d > 0 ? '[data-nav="zur"]' : (zwei ? '[data-nav="vor2"]' : '[data-nav="vor1"]'))
if (knopf && knopf.focus) knopf.focus()
}
function tagKlick(tag) {
if (tag < heute || tag > bisMax) return
var neu = WKU_LOGIK.waehle(sel, tag, belegt, parseInt(unterk.mindestNaechte, 10) || 1)
hinweis = neu.fehler === 'mindestnaechte' ? 'Mindestaufenthalt: ' + (unterk.mindestNaechte === 1 ? '1 Nacht' : unterk.mindestNaechte + ' Nächte') + '.' : ''
sel = { von: neu.von || null, bis: neu.bis || null }
fokusTag = tag
zeichne(true)
}
monate.addEventListener('keydown', function (ev) {
var t = ev.target && ev.target.getAttribute ? ev.target.getAttribute('data-tag') : null
if (!t) return
var d = ev.key === 'ArrowRight' ? 1 : ev.key === 'ArrowLeft' ? -1 : ev.key === 'ArrowDown' ? 7 : ev.key === 'ArrowUp' ? -7 : 0
if (!d) return
ev.preventDefault()
var ziel = plusTage(t, d)
if (ziel < heute || ziel > bisMax) return
fokusTag = ziel
var p = zerlege(ziel)
var sichtbar = monatKey(p[0], p[1]) >= monatKey(ansicht.j, ansicht.m) && monatKey(p[0], p[1]) <= monatKey(ansicht.j, ansicht.m) + (zweiSichtbar() ? 1 : 0)
if (!sichtbar) ansicht = { j: p[0], m: p[1] }
zeichne(true)
})
function pad(n) { return zwei(n) }
var ladeNr = 0
function laden() {
var nr = (ladeNr += 1)
var jetzt = new Date()
var von = jetzt.getUTCFullYear() + '-' + pad(jetzt.getUTCMonth() + 1) + '-' + pad(jetzt.getUTCDate())
von = plusTage(von, -1)
var bis = plusTage(von, 600)
return holen(basis + '/belegung?unterkunft=' + encodeURIComponent(unterk.id) + '&von=' + von + '&bis=' + bis, { headers: { Accept: 'application/json' } }).then(function (r) {
if (nr !== ladeNr) return
if (!r.ok || !r.body.heute || !r.body.bisMax) throw new Error('belegung')
belegt = r.body.belegt || []
heute = r.body.heute
bisMax = r.body.bisMax
if (!ansicht) { var p = zerlege(heute); ansicht = { j: p[0], m: p[1] } }
if (!fokusTag) fokusTag = heute
sel = { von: null, bis: null }
hinweis = ''
zeichne(false)
})
}
function ladenOderAusweichen() { laden().then(null, function () { ausweich(el) }) }
if (selU) selU.addEventListener('change', function () {
unterk = cfg.unterkuenfte[parseInt(selU.value, 10) || 0]
fuellePersonen()
ladenOderAusweichen()
})
form.addEventListener('submit', function (ev) {
ev.preventDefault()
if (sendet) return
zeigeFehler('')
Object.keys(feld).forEach(function (k) { feldFehler(k, '') })
if (!sel.von || !sel.bis) { zeigeFehler('Bitte wählen Sie Anreise und Abreise im Kalender.'); return }
var wert = function (n) { return String(feld[n].inp.value || '').trim() }
var fehler = 0
if (!wert('name')) { feldFehler('name', 'Bitte geben Sie Ihren Namen an.'); fehler += 1 }
if (!EMAIL_FORM.test(wert('email'))) { feldFehler('email', 'Bitte geben Sie eine gültige E-Mail-Adresse an.'); fehler += 1 }
if (fehler) { var ersteFehl = form.querySelector('[aria-invalid="true"]'); if (ersteFehl && ersteFehl.focus) ersteFehl.focus(); return }
sendet = true
btn.setAttribute('disabled', '')
var body = { unterkunft: unterk.id, von: sel.von, bis: sel.bis, personen: parseInt(selP.value, 10) || 1, name: wert('name'), firma: wert('firma'), email: wert('email'), telefon: wert('telefon'), nachricht: wert('nachricht'), formToken: token, kontakt_url: hp.value }
holen(basis + '/anfrage', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body) }).then(function (r) {
sendet = false
btn.removeAttribute('disabled')
if (r.ok && r.body.nummer) {
formBox.textContent = ''
formBox.appendChild(h('p', { class: 'wku-danke', role: 'status', tabindex: '-1', text: 'Danke! Ihre Anfrage ' + r.body.nummer + ' ist eingegangen. Sie erhalten gleich eine Bestätigung per E-Mail.' }))
var d = formBox.querySelector('.wku-danke'); if (d && d.focus) d.focus()
laden().then(null, function () {})
return
}
var b = r.body || {}
var gezeigt = false
var uebrig = []
if (b.felder) Object.keys(b.felder).forEach(function (k) {
if (!Object.prototype.hasOwnProperty.call(b.felder, k)) return
if (feldFehler(k, String(b.felder[k]))) gezeigt = true
else if (k === 'zeitraum' || k === 'personen') uebrig.push(String(b.felder[k]))
})
if (uebrig.length) { zeigeFehler(uebrig.join(' ')); gezeigt = true }
var fehlerFokus = form.querySelector('[aria-invalid="true"]')
if (fehlerFokus && fehlerFokus.focus) fehlerFokus.focus()
if (gezeigt) return
if (b.fehler === 'neu_laden') {
zeigeFehler('Das Formular war zu lange geöffnet. Bitte senden Sie Ihre Anfrage noch einmal ab.')
holen(basis, { headers: { Accept: 'application/json' } }).then(function (c) { if (c.ok && c.body.formToken) token = c.body.formToken }, function () {})
return
}
if (b.fehler === 'mindestnaechte') {
var mn = parseInt((b.felder && b.felder.mindestNaechte) || unterk.mindestNaechte, 10) || 1
zeigeFehler('Mindestaufenthalt: ' + (mn === 1 ? '1 Nacht' : mn + ' Nächte') + '. Bitte wählen Sie einen längeren Zeitraum.')
return
}
if (b.fehler === 'belegt' || r.status === 409) { zeigeFehler('Dieser Zeitraum ist leider nicht mehr frei. Bitte wählen Sie einen anderen.'); laden().then(null, function () {}) }
else if (r.status === 429) zeigeFehler('Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.')
else {
zeigeFehler('Die Anfrage konnte nicht gesendet werden. Bitte versuchen Sie es erneut oder melden Sie sich per Telefon oder E-Mail.')
holen(basis, { headers: { Accept: 'application/json' } }).then(function (c) { if (c.ok && c.body.formToken) token = c.body.formToken }, function () {})
}
}, function () {
sendet = false
btn.removeAttribute('disabled')
zeigeFehler('Die Anfrage konnte nicht gesendet werden. Bitte versuchen Sie es erneut oder melden Sie sich per Telefon oder E-Mail.')
})
})
ladenOderAusweichen()
}
function alleStarten() {
var liste = document.querySelectorAll('[data-webklar-unterkunft]')
for (var i = 0; i < liste.length; i += 1) starten(liste[i])
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', alleStarten)
else alleStarten()
})()
Loaded 100 of 672 files, more files were not shown because too many files have changed in this diff. Show more