Öffnungszeiten live: Design (Entscheidungen Z1–Z23 des Inhabers, Prüfung gegen den Code eingearbeitet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ae0f67457
commit
360d218d4e
1 file changed
+585
@@ -0,0 +1,585 @@
|
||||
# Öffnungszeiten live (Funktion im Kundenportal) – Design
|
||||
|
||||
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 (`ä`, `–`); 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).
|
||||
Reference in new issue
Block a user