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>
This commit is contained in:
commit
e87140e5ba
245 files changed
+38799
-1050
No files matched your search
@@ -23,7 +23,7 @@ Die Funktion ersetzt die Kachel „Terminplaner · Kommt bald“ (`public/funkti
|
||||
| # | Entscheidung |
|
||||
|---|---|
|
||||
| E1 | Abrechnung: **einmalige Einrichtung + nur solange ein Wartungs-Abo läuft** (wie Website-Analyse). Endet das Abo, endet der Terminplaner. Hosting-Abo reicht nicht. |
|
||||
| E2 | Einrichtungspreis **149 €** (brutto, einmalig, Stripe Checkout wie News). |
|
||||
| E2 | Einrichtungspreis **149 €** (ursprünglich „brutto“, seit TP2 ohne USt nach § 19 UStG; einmalig, Stripe Checkout wie News). |
|
||||
| E3 | Umfang V1: **Personen und Plätze** (Einzelperson; Team mit Wahl der Person oder „egal“; buchbare Plätze wie Kabine, Hebebühne). Pro Leistung: **sofort verbindlich** oder **Anfrage** (Betrieb bestätigt). Gruppen/Kurse/Tische später. |
|
||||
| E4 | Der Gast **bestätigt seine E-Mail-Adresse per Link**, bevor der Termin gilt; der Slot ist so lange 30 Minuten reserviert. |
|
||||
| E5 | **Ein gemeinsames Widget**, je Kunde gestaltet (Farben, Schrift, Rundungen, Texte, Position) + offene, versionierte Schnittstelle für Sonderfälle. |
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# Analyse-Übersicht neu („Cockpit“) – Design
|
||||
|
||||
Stand: 2026-10-04 · Status: Aufbau, Design-Richtung und Abschnitte 1–3 vom Nutzer im Chat freigegeben, Spec zur Durchsicht
|
||||
Baut auf der Website-Analyse auf (Spec `2026-10-01-website-analyse-design.md`, live seit 01.10.2026).
|
||||
Recherche: `/root/reports/analyse-ueberarbeitung-2026-10-04/recherche.md` (30 Tools, 25 Features, UI-Muster).
|
||||
Entwürfe: `/root/reports/analyse-ueberarbeitung-2026-10-04/entwurf-a-cockpit.png` (gewählt), `entwurf-b-bericht.png`.
|
||||
Regel des Nutzers: bauen → live (Nutzer pusht `main`) → Nutzer testet live.
|
||||
|
||||
## Ziel
|
||||
|
||||
Die Analyse-Seite im Kundenportal (Funktionen → Analyse) soll für kleine Betriebe ohne Analytics-Kenntnisse
|
||||
**auf einen Blick** sagen, wie die Website läuft: ein Satz in Klartext, Vergleich zum Vorzeitraum,
|
||||
Kontakt-Aktionen als Hauptzahl. Das Interface darf sich vom restlichen Portal abheben, muss aber
|
||||
aufgeräumt und einheitlich sein.
|
||||
|
||||
## Entscheidungen (vom Nutzer, 04.10.2026)
|
||||
|
||||
| # | Entscheidung |
|
||||
|---|---|
|
||||
| U1 | Umfang: nur das Paket **„Übersicht neu“** (Kennzahlen mit Vorzeitraum-Vergleich und Mini-Verlauf, Klartext-Zusammenfassung, Kontakt-Aktionen als Hauptzahl, Datenbasis-Hinweis). **Nicht** in diesem Projekt: Besucher-Details (Wochentag × Uhrzeit, Kanäle, Zeit je Seite), Website-Check (Erreichbarkeit, SSL, Links, SEO), Bericht per Mail. |
|
||||
| U2 | Design-Richtung **A „Cockpit“**: dunkel wie das Portal, eigene Akzentfarbe Türkis nur innerhalb der Analyse, leuchtender Block „Das Wichtigste“. |
|
||||
| U3 | Aufbau mit drei Reitern **Übersicht · Heatmap · Einstellungen**; Heatmap und „Ausschalten“ hängen nicht mehr unten an der Übersicht. |
|
||||
| U4 | Nur **vorhandene Daten**. Keine Änderung an Messskript (`w.js`, `h.js`), Banner, AVV, Leistungsbeschreibung, Datenschutz-Zusatzseite oder Abo-Bedingungen. |
|
||||
| U5 | Klartext entsteht durch **feste Regeln im Browser**, keine KI, kein externer Dienst. |
|
||||
|
||||
Rechtliche Einordnung (keine Rechtsberatung): Der Vergleich nutzt die Tageswerte des Vorzeitraums (bis zu 180
|
||||
Tage zurück; Tageswerte werden 25 Monate aufbewahrt). Durchschnitte und Tageswerte für Besucher, Kontakte und
|
||||
Verweildauer stammen aus bereits erhobenen und verdichteten Daten (`daily`: `visitors`, `ct_kind`,
|
||||
`en_count`, `en_sec_sum`, `scroll_sum`). Die Leistungsbeschreibung (§ 5: „Kennzahlen und Diagramme für die
|
||||
letzten 7, 30 oder 90 Tage“) deckt das ab; kein neuer Text, keine neue Einwilligung.
|
||||
|
||||
## 1. Daten: Statistik-Antwort des Collectors
|
||||
|
||||
`GET /a/admin/stats/:siteKey?range=7|30|90` (interner Listener, `analytics/src/stats.js`) liefert **zusätzlich**
|
||||
zu den bisherigen Feldern (alle bisherigen Felder bleiben unverändert, damit der Wechsel abwärtskompatibel ist):
|
||||
|
||||
```js
|
||||
{
|
||||
range: 30,
|
||||
from: '2026-09-04', to: '2026-10-03', // Berliner Kalendertage des Zeitraums: abgeschlossene Tage (to = gestern)
|
||||
today: { day: '2026-10-04', pv, visitors, contacts }, // heute bis jetzt, live, nicht im Zeitraum enthalten
|
||||
firstDay: '2026-08-20', // frühester Berliner Tag mit Daten dieser Website ('' = keine)
|
||||
totals: {
|
||||
pv, visitors, contacts, consentRate, // wie bisher
|
||||
engaged, // Zahl der en-Ereignisse (Seitenbesuche mit Zustimmung und Abschluss)
|
||||
avgSec, // Ø Sekunden pro Seitenbesuch = en_sec_sum / en_count, gerundet; null wenn engaged = 0
|
||||
avgScroll, // Ø Scrolltiefe in % = scroll_sum / en_count, gerundet; null wenn engaged = 0
|
||||
},
|
||||
series: [{ day, pv, visitors, contacts, avgSec }], // bisher nur {day, pv}; avgSec je Tag null ohne en
|
||||
pages, refs, countries, devices, contacts, banner, // unverändert
|
||||
previous: {
|
||||
from: '2026-08-06', to: '2026-09-04', // die `range` Kalendertage direkt vor `from`
|
||||
complete: true, // firstDay !== '' && firstDay <= previous.from
|
||||
totals: { pv, visitors, contacts, consentRate, engaged, avgSec, avgScroll },
|
||||
series: [{ day, pv, visitors, contacts, avgSec }],
|
||||
contacts: [{ key, count }], // je Art, für die Veränderung pro Zeile
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Regeln:
|
||||
- Tageslisten werden **kalendarisch** gebildet (Berliner Tage, wie `dayList`), nicht durch Abziehen von
|
||||
`range × 24 h` – sonst verrutscht der Vorzeitraum bei der Zeitumstellung um einen Tag.
|
||||
- Gemeinsame Hilfsfunktion sammelt die Dimensionen für eine beliebige Tagesliste: verdichtete Tage aus `daily`,
|
||||
nicht verdichtete Tage live aus `events_raw` (wie heute). Kein Tag wird doppelt gezählt.
|
||||
- `firstDay` = kleinster Tag aus `daily` und `events_raw` dieser Website (`MIN(day)` bzw. `dayOf(MIN(ts))`).
|
||||
- `visitors` bleibt die Summe der Tages-Besucher (Salz wechselt täglich, kein tagesübergreifender Abgleich).
|
||||
- Der Portal-Server reicht die Antwort unverändert durch (`GET /api/funktionen/analyse/stats`).
|
||||
|
||||
Portal-Server, einzige weitere Änderung: `analyseView` (Route `GET /api/funktionen`) gibt zusätzlich
|
||||
`activatedAt` aus dem Analyse-Dokument zurück (`null`, wenn nicht aktiv).
|
||||
|
||||
Nachtrag 04.10.2026 (Entscheidung im Final-Review, F1): Der Zeitraum besteht aus **abgeschlossenen** Tagen
|
||||
(endet gestern), damit der angebrochene heutige Tag nicht gegen volle Vortage verglichen wird (sonst zeigte z. B.
|
||||
der 7-Tage-Blick tagsüber fälschlich „weniger“). Der heutige Stand steht getrennt in `today` und in der Kopfleiste
|
||||
(„Heute bisher: 42 Aufrufe“). Der Satz „direkt kontaktiert“ zählt nur Telefon, E-Mail und WhatsApp (F2).
|
||||
|
||||
## 2. Oberfläche
|
||||
|
||||
### 2.1 Aufbau der Seite (Zustand „aktiv“)
|
||||
|
||||
```
|
||||
[ Kopfleiste: ● Website-Name · „Analyse aktiv · Heute bisher: 42 Aufrufe · Stand: 19:42 Uhr“ (Website-Auswahl) [7 | 30 | 90 Tage] ]
|
||||
[ Reiter: Übersicht | Heatmap | Einstellungen ]
|
||||
Übersicht:
|
||||
[ Das Wichtigste · 5. Sep. – 4. Okt. — Klartext-Satz, darunter bis zu 4 Hinweise mit Symbol ]
|
||||
[ Seitenaufrufe ] [ Besucher* ] [ Kontakt-Aktionen* ] [ Ø Zeit pro Seite* ] je: Zahl, ▲/▼-Chip, „vs. <Vorwert>“, Mini-Verlauf
|
||||
[ Seitenaufrufe pro Tag (Linie + gestrichelter Vorzeitraum) ] [ Kontakt-Aktionen: Anrufe, E-Mails, WhatsApp, Links nach außen + Quote ]
|
||||
[ Was angesehen wird: Seiten | Herkunft ] [ Wer zu Besuch ist: Geräte | Länder ]
|
||||
[ Datenbasis: Ring mit Zustimmungsquote + Erklärung ] [ Heatmap: Vorschaubild + „Öffnen“ → Reiter Heatmap ]
|
||||
Fußzeile: „Länderdaten: IP-Geolocation by DB-IP“
|
||||
```
|
||||
|
||||
- **Kopfleiste**: Statuspunkt (türkis = aktiv). Bei `devMode` statt „Analyse aktiv“ gelb: „Im Dev-Modus wird
|
||||
nicht gemessen.“ „Stand“ = Ladezeitpunkt der Daten. Bei mehreren Websites ein Auswahlfeld in der Leiste
|
||||
(ersetzt das bisherige `fn-project`-Panel). Der Zeitraum-Schalter erscheint nur im Reiter Übersicht
|
||||
(die Heatmap zeigt fest 30 Tage).
|
||||
- **Reiter**: `role="tablist"`, Zustand bleibt beim Wechsel von Zeitraum oder Website erhalten
|
||||
(`analyseSel.tab`), der URL-Hash bleibt `#analyse`. Auf dem Handy horizontal wischbar.
|
||||
- **Das Wichtigste**: siehe 2.3. Unter 30 Seitenaufrufen im Zeitraum: „Sobald ein paar mehr Besuche da sind,
|
||||
steht hier das Wichtigste über deine Website in zwei, drei Sätzen.“
|
||||
- **Kennzahlen-Kacheln** (4): Seitenaufrufe, Besucher*, Kontakt-Aktionen*, Ø Zeit pro Seite* (Format `m:ss`,
|
||||
darunter klein „Ø 64 % gescrollt“). Chip mit Veränderung (2.4). Mini-Verlauf aus `series` (Fläche + Linie,
|
||||
ohne Achsen). Ohne Vergleich steht statt Chip „Vergleich ab TT.MM.“ (2.4).
|
||||
- **Diagramm**: Seitenaufrufe pro Tag, aktuelle Linie türkis mit Fläche, Vorzeitraum gestrichelt grau (nach
|
||||
Tagesindex ausgerichtet; nur wenn `previous.complete`). Legende „30 Tage“ / „Zeitraum davor“. Antippen oder
|
||||
Überfahren eines Tages zeigt eine Blase „Sa., 19.09.: 371 Aufrufe (davor 210)“. SVG behält `<title>`.
|
||||
- **Kontakt-Aktionen**: Summe im Kopf, Zeilen Anrufe (`tel`), E-Mails (`mail`), WhatsApp (`wa`), Links nach außen
|
||||
(`out`) mit Symbol, Zahl und Veränderungs-Chip; Arten mit 0 in beiden Zeiträumen entfallen. Darunter die
|
||||
Quote: „Auf 100 Besucher kommen 7,8 Kontakt-Aktionen (davor 6,5).“ (erst ab 30 Besuchern; Kontakt-Aktionen sind
|
||||
Klicks, keine Personen – daher „pro 100 Besucher“, nicht „x % der Besucher“). Ohne Kontakte: „Noch keine
|
||||
Kontakt-Aktionen. Sie zählen, wenn jemand auf deiner Website auf Telefonnummer, E-Mail-Adresse oder
|
||||
WhatsApp tippt.“
|
||||
- **Ranglisten** in zwei Karten mit kleinem Umschalter: „Was angesehen wird“ (Seiten | Herkunft), „Wer zu Besuch
|
||||
ist“ (Geräte | Länder). Zeile = Balken als Hintergrund des Namens, Zahl, Anteil in %. Erst 6 Zeilen, „Alle
|
||||
anzeigen“ klappt bis 20 auf. `/` heißt „Startseite“. Herkunft mit lesbaren Namen für bekannte Hosts
|
||||
(google.* → Google, bing.com → Bing, instagram.com → Instagram, facebook.com/m.facebook.com/l.facebook.com →
|
||||
Facebook, duckduckgo.com → DuckDuckGo, ecosia.org → Ecosia), sonst der Host; `''` → „Direkt oder unbekannt“.
|
||||
- **Datenbasis** (ersetzt Banner-Liste und Sternchen-Fußnote): Ring mit `consentRate`, Text „61 % haben dem
|
||||
Banner zugestimmt (2.410 von 3.950 Entscheidungen). Seitenaufrufe zählen wir vollständig; Besucher,
|
||||
Kontakt-Aktionen und Zeit pro Seite (*) nur bei Zustimmung.“ Ohne Entscheidungen: „Noch hat niemand über das
|
||||
Banner entschieden.“
|
||||
- **Heatmap-Karte**: kleines statisches Vorschaubild (CSS-Verlauf, keine echte Website), Satz, Knopf „Öffnen“
|
||||
wechselt in den Reiter Heatmap.
|
||||
|
||||
### 2.2 Reiter Heatmap und Einstellungen, andere Zustände
|
||||
|
||||
- **Heatmap**: die bestehende Heatmap in Geräterahmen (`renderAnalyseHeat`, `geraete.js/.css`, iframes,
|
||||
postMessage-Zähler, 20-s-Rückfall „In neuem Tab öffnen“) unverändert im Verhalten, optisch im Cockpit-Stil
|
||||
(Seitenauswahl und Computer/Tablet-Schalter in einer Leiste). Die iframes laden erst, wenn der Reiter
|
||||
geöffnet wird.
|
||||
- **Einstellungen**: Karte „Status“ (aktiv seit `activatedAt`, Banner-Farbe als Farbfeld, Fassung
|
||||
`avvVersion`), Karte „Unterlagen“ (Links AVV und Leistungsbeschreibung aus `legal`), Karte „Analyse
|
||||
ausschalten“ (`renderAnalyseOff`, Texte und Ablauf unverändert).
|
||||
- **Einschalten** (`renderAnalyseActivation`), **gesperrt**, **nicht verfügbar**, **unbekannt**, **noch nicht
|
||||
freigeschaltet**: gleicher Cockpit-Rahmen (Kopfleiste ohne Zeitraum und Reiter). Alle Texte, Links, Häkchen,
|
||||
Farbwahl und Banner-Vorschau bleiben **wörtlich** wie heute (Rechtsrunde 01./02.10.).
|
||||
- **Laden**: Platzhalter-Kacheln in der Form der echten Kacheln statt „Lade…“. **Fehler 503**: Karte
|
||||
„Auswertung gerade nicht erreichbar“ mit Knopf „Neu laden“.
|
||||
|
||||
### 2.3 Klartext-Regeln („Das Wichtigste“)
|
||||
|
||||
Reine Funktion `summarize(stats)` → `{ lead, items: [{ icon, text }] }` in `public/analyse-view.js`
|
||||
(ohne DOM, Node-testbar). Zahlen deutsch formatiert. Nur bei `totals.pv >= 30`, sonst Leerzustand (2.1).
|
||||
|
||||
**Satz** (`lead`):
|
||||
- Vergleich möglich (2.4): „Deine Website wurde in den letzten N Tagen **X-mal** aufgerufen – **Y % mehr** /
|
||||
**Y % weniger** als in den N Tagen davor.“ Bei |Veränderung| < 5 %: „… – etwa so oft wie in den N Tagen
|
||||
davor.“ Bei kleinen Zahlen (2.4) absolut: „… – **12 Aufrufe mehr** als in den N Tagen davor.“
|
||||
- Kein Vergleich: „Deine Website wurde in den letzten N Tagen **X-mal** aufgerufen.“ Liegt `firstDay` im
|
||||
Zeitraum: „Seit dem Start der Analyse am TT.MM. wurde deine Website **X-mal** aufgerufen.“
|
||||
|
||||
**Hinweise** (`items`, in dieser Reihenfolge, nur wenn die Bedingung erfüllt ist, höchstens 4):
|
||||
1. Kontakte (`contacts >= 1`): „N-mal hat dich jemand direkt kontaktiert, meist per Telefon (96).“ + bei
|
||||
Vergleich und Vorwert ≥ 1: „Das sind 45 mehr als zuvor.“ / „… weniger …“ / „Genauso oft wie zuvor.“
|
||||
Bei 0 Kontakten und `visitors >= 30`: „Von den Besuchern mit Zustimmung hat noch niemand über die Website
|
||||
angerufen oder geschrieben.“ (Kontakt-Aktionen zählen nur bei Zustimmung.)
|
||||
2. Wochentag (mindestens 7 Tage mit Daten im Zeitraum und `pv >= 70`): Ø Aufrufe je Wochentag über die Tage
|
||||
ab `firstDay`; stärkster Wochentag, wenn sein Schnitt ≥ 1,2 × Gesamtschnitt: „Samstag ist im Schnitt dein
|
||||
stärkster Tag.“ Zusätzlich Spitzentag, wenn ≥ 1,8 × Tagesschnitt: „Spitzentag war Sa., 19.09. mit 371
|
||||
Aufrufen.“ (beides in einem Hinweis, wenn beides zutrifft).
|
||||
3. Herkunft (erster Eintrag in `refs` mit Host, Anteil ≥ 20 % der Herkunfts-Summe): „Die meisten, die über
|
||||
einen Link kamen, kommen von Google (880).“
|
||||
4. Gerät (`m` oder `d` ≥ 55 % der Geräte-Summe): „66 % der Aufrufe kommen vom Handy.“ / „… vom Computer.“
|
||||
5. Seite (`pages[0]`): „Am meisten angesehen: die Startseite, danach /preise (1.310).“ bzw. „Am meisten
|
||||
angesehen: /preise (1.310 Aufrufe).“
|
||||
|
||||
### 2.4 Vergleich und Veränderungs-Chips
|
||||
|
||||
- Vergleich möglich, wenn `previous.complete` und `previous.totals.pv >= 20`. Sonst kein Chip; Kacheln zeigen
|
||||
„Vergleich ab TT.MM.“ mit TT.MM. = `firstDay` + 2 × range Tage; ohne `firstDay` gar nichts. Fehlt
|
||||
`previous` ganz (alter Collector), wie „kein Vergleich“ ohne Datum.
|
||||
- Funktion `delta(cur, prev, { kind })` → `{ dir: 'up'|'down'|'flat', text, aria }`:
|
||||
- Zählwerte: Prozent (ganzzahlig, „▲ 14 %“), wenn `prev >= 100` und `cur >= 100`; sonst absolut („▲ 12“,
|
||||
„▼ 3“). Gleich → „± 0“ (`flat`). |Prozent| < 1 → `flat` „± 0 %“.
|
||||
- Sekunden: absolut, „▲ 6 s“ / „▼ 1:05“ (ab 60 s als m:ss).
|
||||
- `aria`: „plus 14 Prozent gegenüber dem Zeitraum davor“ usw. Farbe nie alleiniger Träger (Pfeile ▲▼).
|
||||
- Farben: mehr = türkis, weniger = rosé, gleich = grau. Bewusst **keine** Wertung „gut/schlecht“ (weniger
|
||||
Zeit pro Seite kann gut sein).
|
||||
|
||||
### 2.5 Optik („Cockpit“)
|
||||
|
||||
- Alles unter einem Wurzelelement `.an` mit eigenen CSS-Variablen; außerhalb ändert sich nichts.
|
||||
Akzent `--an-acc: #2dd4bf`, stark `#5eead4`, weich `rgba(45,212,191,.1)`; weniger `#fda4af`;
|
||||
Karten `rgba(14,16,20,.92)`, Rand `rgba(255,255,255,.09)`, Radius 16 px, Abstände 14 px.
|
||||
- Kartentitel einheitlich: 13 px, fett, Großbuchstaben mit Sperrung, gedämpft; Zahlen `tabular-nums`.
|
||||
- „Das Wichtigste“: Verlauf türkis → violett, türkiser Rand, Satz 21 px.
|
||||
- Desktop: Raster wie Entwurf A (4 Kacheln; Diagramm 1,65 : 1 Kontakte; 2 × 2 Karten). Diagramm und
|
||||
Kontaktkarte gleich hoch, das Diagramm füllt die Höhe.
|
||||
- Handy (≤ 880 px, App-Ansicht): Kopfleiste zweizeilig, Zeitraum-Schalter volle Breite, Kacheln 2 × 2, Hinweise
|
||||
einspaltig, alle Karten untereinander. Keine waagrechte Seiten-Scrollleiste.
|
||||
- `prefers-reduced-motion`: keine Übergänge.
|
||||
|
||||
## 3. Code-Aufteilung
|
||||
|
||||
`public/funktionen.js` (980 Zeilen) wird entlastet:
|
||||
- **`public/analyse.js`** (neu): gesamte Analyse-Oberfläche – `initAnalyse`, `renderAnalyseActivation`,
|
||||
`renderAnalyseOff`, Übersicht, Heatmap, Einstellungen. `app.js` lädt `/analyse.js` für die Ansicht `analyse`.
|
||||
- **`public/fn-util.js`** (neu): `esc`, `safeUrl`, `request` – bisher lokal in `funktionen.js`, jetzt von
|
||||
beiden Modulen importiert.
|
||||
- **`public/analyse-view.js`**: bekommt die reinen Funktionen `summarize`, `delta`, `fmtDuration`,
|
||||
`contactRate`, `compareFrom`, `refName`, `weekdayStats` sowie `analyseOf`, `analyseState`,
|
||||
`pickAnalyseProject` (bisher in `funktionen.js`, von der Kachel und der Analyse gebraucht).
|
||||
- **`public/analyse.css`** (neu, in `dashboard.html` eingebunden): alle `.an`-Regeln; die alten
|
||||
`.an-*`-Regeln in `funktionen.css` entfallen, soweit sie nur die Analyse betreffen.
|
||||
- Kachel „Analyse“ auf der Funktionen-Seite bleibt in `funktionen.js` (unverändert im Verhalten).
|
||||
- Bestehende Element-IDs, die Tests und Abläufe nutzen, bleiben: `#analyse-body`, `#an-stage`, `#an-preview`,
|
||||
`#an-avv`, `#an-on`, `#an-error`, `#an-off-*`, `#an-heat-page`.
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- **Collector** (`analytics/test/stats.test.js`): `previous` mit richtigen Tagen und Summen; `complete`
|
||||
false/true je nach `firstDay`; `firstDay` aus `daily` und Rohdaten; `series` mit visitors/contacts/avgSec;
|
||||
`avgSec`/`avgScroll` null ohne en; Vorzeitraum über eine Zeitumstellung (Ende Oktober) lückenlos und ohne
|
||||
Überlappung; nichts doppelt (verdichtet + live). Vertragstest `test/analyticsContract.test.js` um die neuen
|
||||
Felder ergänzen.
|
||||
- **Portal, rein** (`test/analyseInsights.test.js`, neu): `summarize` für Wachstum, Rückgang, gleich, kleine
|
||||
Zahlen, ohne Vergleich, neu gestartet, zu wenig Daten, ohne Kontakte, Wochentag/Spitzentag, Herkunft, Gerät,
|
||||
höchstens 4 Hinweise; `delta` (Prozent/absolut/flat/Sekunden/aria); `compareFrom`; `contactRate`;
|
||||
`refName`; HTML-Maskierung aller Werte, die aus Daten kommen (Seitenpfade, Hosts).
|
||||
- **Portal, bestehend**: `funktionenTiles.test.js` und `analyseView.test.js` auf `analyse.js`/`fn-util.js`
|
||||
umstellen; alle bisherigen Text-Prüfungen (Einschalten, Ausschalten) laufen unverändert weiter.
|
||||
`analyseRoutes.test.js`: `activatedAt` in der Ansicht.
|
||||
- **Browser** (`test/ui/analyse.ui.mjs`, erweitert; Playwright mit gemockter API): Desktop 1440 und Handy 402 ×
|
||||
Zustände aktiv (volle Daten), aktiv neu (ohne Vergleich), wenig Daten, Dev-Modus, Einschalten, fehlende
|
||||
Angaben, 503; Reiterwechsel; Heatmap-iframes erst nach Reiterwechsel; keine `pageerror`; keine waagrechte
|
||||
Scrollleiste. Screenshots nach `/root/reports/analyse-ueberarbeitung-2026-10-04/screens/`.
|
||||
- Gesamte Suiten: `npm test` (Portal) und `analytics: npm test` grün; Node 20 (Portal) und Node 24 (Collector).
|
||||
|
||||
## 5. Livegang
|
||||
|
||||
1. Nutzer pusht `feature/analyse-uebersicht` nach `main` (mein Push nach `main` ist im Auto-Modus gesperrt).
|
||||
2. `deploy.sh` startet Portal und – weil `analytics/` geändert – den Collector neu. Keine Schema-Änderung,
|
||||
keine `.env`-Änderung.
|
||||
3. Prüfung durch mich: Collector `/a/health`, Stats-Abruf einer echten aktiven Website im Container
|
||||
(neue Felder vorhanden, alte unverändert), Portal-Bundle enthält `analyse.js`, Ansicht lädt ohne Fehler.
|
||||
4. Nutzer testet live mit einer Website mit aktiver Analyse.
|
||||
|
||||
Rückweg: Portal und Collector sind gegenseitig abwärtskompatibel (alte Oberfläche ignoriert neue Felder, neue
|
||||
Oberfläche kommt ohne `previous` aus); Zurückrollen = Revert-Commit auf `main`.
|
||||
|
||||
## Nicht in diesem Projekt
|
||||
|
||||
Besucher-Details, Website-Check, Bericht per Mail (U1); Änderungen an Messskript, Banner und Rechtstexten (U4);
|
||||
WOMS; Anpassen der Banner-Farbe nach dem Einschalten; Export.
|
||||
@@ -0,0 +1,349 @@
|
||||
# Webshop (Funktion `shop`): Design
|
||||
|
||||
Stand 2026-10-05. Basis: Portal-Zweig `feature/terminplaner-tp2` af63b69 (Register `config.features`, generischer Checkout,
|
||||
Zustimmungsnachweis `featureConsents`, versionierte Rechtsunterlagen). Arbeitszweig `feature/webshop`, Worktree `dev-webshop`.
|
||||
Pfade: **P** = Portal-Repo (`kundenbereich.webklar.com`), **W** = WOMS-Repo (`ticket.webklar.com`).
|
||||
Fakten aus dem Code: `/root/reports/webshop-2026-10-05/fakten-*.md`; Recht und Stripe: `recherche-recht-stripe.md`.
|
||||
|
||||
Auftrag des Inhabers (2026-10-05, `/goal`): „Online-Webshop-Integration, auf Anfrage, einmalige Kosten, der Kunde
|
||||
konfiguriert alle Artikel, Stripe-Anbindung (PayPal optional, nur Stripe ist ok), keine Rückfragen, Recht beachten.“
|
||||
Alle Entscheidungen unten (W1–W30) hat Claude ohne Rückfrage getroffen; sie stehen gesammelt in Abschnitt 12.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel und Abgrenzung
|
||||
|
||||
**Der Webshop liefert:** Ein Kunde fragt im Portal einen Webshop für eine seiner Websites an. Das Team prüft die Anfrage
|
||||
(was wird verkauft, ist es erlaubt, passt es) und gibt sie frei. Danach bestellt der Kunde die Einrichtung (einmalig
|
||||
299 €, Stripe-Checkout wie bei den anderen Funktionen). Es entsteht ein Einrichtungs-Ticket, das Team baut den Shop in
|
||||
die Website ein. Der Kunde verbindet sein **eigenes** Stripe-Konto (Stripe Connect), pflegt Artikel, Versand, Abholung
|
||||
und seine Rechtstexte im Portal und öffnet den Shop. Endkunden kaufen im Widget auf seiner Website und bezahlen über
|
||||
Stripe Checkout direkt an den Kunden. Bestellungen, Versand, Erstattung und Widerrufe verwaltet der Kunde im Portal.
|
||||
|
||||
**Nicht Teil (V1):** digitale Produkte/Downloads, Abos/wiederkehrende Zahlungen, Gutscheine/Rabattcodes, Versand
|
||||
außerhalb Deutschlands und Österreichs, Lagerverwaltung über mehrere Lager, Rechnungserstellung durch WEBklar (die
|
||||
Stripe-Quittung und die Bestellbestätigung gehen an den Käufer, Rechnungen schreibt der Händler selbst), Anbindung
|
||||
eines schon bestehenden Stripe-Kontos per OAuth (später), alkoholische Getränke, Tabak, Arzneimittel, Waffen,
|
||||
Erotik, Lebende Tiere und alles auf Stripes Liste verbotener Geschäfte (Bestellgate + Leistungsbeschreibung).
|
||||
|
||||
**Warum nur Stripe (W1):** Stripe läuft schon für Abo und Funktionen. Stripe Checkout bietet Karte, Apple Pay, Google Pay,
|
||||
Klarna und SEPA-Lastschrift im selben Checkout an. **PayPal geht über Stripe bei Connect-Direct-Charges nicht** (Stripe-Doku,
|
||||
Recherche A3); eine eigene PayPal-Anbindung wäre eine zweite Integration samt eigenem Webhook, eigener Erstattung und eigenem
|
||||
Konto-Onboarding je Händler. Der Inhaber hat „nur Stripe“ ausdrücklich erlaubt; PayPal bleibt ein späterer Ausbau.
|
||||
|
||||
**Warum Stripe Connect mit eigenem Konto des Kunden (W2):** Das Geld fließt direkt vom Käufer an den Händler (Direct
|
||||
Charges auf dem verbundenen Konto). WEBklar nimmt nie fremdes Geld an, behält keine Gebühr ein und haftet nicht für
|
||||
Rückbuchungen. Damit ist WEBklar kein Zahlungsdienst im Sinne des ZAG (Stripe ist das lizenzierte Institut), und der
|
||||
Händler hat sein volles Stripe-Dashboard (Auszahlungen, Erstattungen, Belege). Konten werden mit den
|
||||
`controller`-Eigenschaften angelegt: Stripe-Dashboard voll, Gebühren zahlt das Konto, Verluste trägt Stripe/Konto,
|
||||
Anforderungen sammelt Stripe (Gegenstück zum früheren `type: 'standard'`).
|
||||
|
||||
**Pilot und Schalter (W3):** `SHOP_ENABLED=off|pilot|on` wie `TERMINE_ENABLED`; `pilot` nur für `SHOP_PILOT_EMAILS`.
|
||||
Pilotkunde ist das Testkonto (kenso.gri@gmail.com, Projekt `test-website`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Ablauf aus Kundensicht
|
||||
|
||||
| Zustand | Kachel (Funktionen-Tab) | Ansicht `#webshop` | Mail |
|
||||
|---|---|---|---|
|
||||
| nichts angefragt | „Webshop anfragen“ | Anfrageformular | – |
|
||||
| Anfrage offen | „Anfrage wird geprüft“ | Anfrage lesen, zurückziehen | T21 an Team |
|
||||
| Anfrage abgelehnt | „Anfrage abgelehnt“ + Begründung, neue Anfrage möglich | Begründung | K24 |
|
||||
| freigegeben (`off`) | „Webshop bestellen · 299 €“ | Bestellseite (generisch) | K24 |
|
||||
| `ordered` | „Zahlung offen“ | Statuskarte, „Zahlung fortsetzen“ | – |
|
||||
| `setup` | „In Einrichtung“ | Statuskarte + **alle Reiter nutzbar** (Artikel, Stripe, Rechtliches) | K15, T16 |
|
||||
| `active` | „Webshop öffnen“ | Verwaltung | K16 |
|
||||
| `disabled` | „Webshop ansehen“ | lesen, CSV-Export | – |
|
||||
|
||||
**Auf Anfrage (W4):** Der Inhaber will die Funktion „auf Anfrage“. Umsetzung als vorgeschaltete Freigabe: Die Kachel ist
|
||||
für jeden Kunden mit Website-Projekt sichtbar (wenn der Schalter es erlaubt), bestellbar wird sie erst nach Freigabe
|
||||
durch das Team. Die Anfrage fragt: Was wird verkauft (Freitext + Warenart: Waren/Lebensmittel/Kleidung/Kunst/Sonstiges),
|
||||
ungefähre Artikelzahl, Versand und/oder Abholung, Händler ist Unternehmer (Pflicht-Haken), keine der ausgeschlossenen
|
||||
Waren (Pflicht-Haken), Nachricht ans Team. Das Team sieht die Anfrage im WOMS (Kunde → Funktionen) und nimmt sie an oder
|
||||
lehnt sie mit Begründung ab. Der Preis bleibt fest (299 €); Sonderfälle (sehr viele Artikel, Sonderdesign) bespricht das
|
||||
Team im Chat und rechnet Mehraufwand nach Stunden ab, wie beim Terminplaner (Leistungsbeschreibung).
|
||||
|
||||
**Einmalige Kosten (W5):** 299 € einmalig (Kleinunternehmer, ohne USt gemäß § 19 UStG), kein Wartungs-Abo nötig, keine
|
||||
laufende Gebühr von WEBklar, keine Umsatzbeteiligung. Der Shop läuft, solange WEBklar die Website betreibt (Hosting oder
|
||||
Wartung). Stripe-Gebühren zahlt der Händler direkt an Stripe.
|
||||
|
||||
**Ab `setup` nutzbar (W6):** Anders als beim Terminplaner (Einstellung ab Bestellung gesperrt) pflegt der Kunde ab
|
||||
`setup` Artikel, Stripe-Konto und Rechtstexte, damit der Shop zum Einbau fertig ist. Die Website zeigt den Shop erst,
|
||||
wenn alles erfüllt ist (Abschnitt 4.6, „Shop geöffnet“).
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenmodell (Appwrite, `scripts/shop-setup.mjs`, nur Server-Key)
|
||||
|
||||
Alle Collections ohne Client-Rechte (wie `termineConfig`); alles läuft über den Portal-Server.
|
||||
|
||||
- **`shopConfig`** (Dokument-ID = ID des `portalFeatures`-Dokuments; eins je Projekt):
|
||||
`customerId`, `projectId`, `siteKey` (Kopie aus portalFeatures, Index), `anfrage` (JSON-Text ≤ 4000),
|
||||
`anfrageStatus` (`''|offen|angenommen|abgelehnt|zurueckgezogen`), `anfrageAt`, `anfrageEntschiedenAt`,
|
||||
`anfrageEntschiedenVon`, `anfrageBegruendung` (≤ 1000), `stripeAccountId`, `stripeStatus` (JSON ≤ 4000:
|
||||
charges_enabled, payouts_enabled, details_submitted, requirements.currently_due, disabled_reason, geprüft am),
|
||||
`einstellungen` (JSON ≤ 20000), `rechtstexte` (JSON ≤ 100000), `rechtstexteBestaetigtAt`, `rechtstexteBestaetigtVon`,
|
||||
`rechtstexteFassung` (Hash), `geoeffnet` (bool), `geoeffnetAt`.
|
||||
- **`shopProducts`**: `shopId` (= shopConfig-ID, Index), `customerId`, `aktiv` (bool), `sort` (int), `name` (≤ 120),
|
||||
`daten` (JSON ≤ 30000: Kurz-/Langbeschreibung, Preis in Cent, Grundpreis-Menge+Einheit, Steuersatz, Bestand,
|
||||
Varianten, Bilder, Lieferart, Widerrufsausschluss, Produktsicherheit, Lebensmittelangaben, Kategorie).
|
||||
- **`shopOrders`**: `shopId` (Index), `customerId`, `bestellnummer` (eindeutig je Shop, Index), `status` (Index),
|
||||
`tokenHash`, `stripeSessionId` (Index), `paymentIntentId`, `summeCent`, `erstelltAt`, `ablaufAt`, `bezahltAt`,
|
||||
`kaeuferEmail` (≤ 320), `daten` (JSON ≤ 60000: Positionen als Schnappschuss, Lieferart, Adressen, Versandkosten,
|
||||
Steuerausweis, Fassung der Rechtstexte, Verlauf), `anonymisiertAt`.
|
||||
- **`shopWiderrufe`**: `shopId` (Index), `customerId`, `bestellnummer`, `name`, `email`, `nachricht` (≤ 2000),
|
||||
`eingangAt`, `bestaetigungGesendetAt`, `bearbeitetAt`.
|
||||
- **Bilder** auf der Platte unter `upload-data/shop/<siteKey>/<id>.<ext>` (gitignored, wie News-Fotos), ausgeliefert über
|
||||
die öffentliche API.
|
||||
|
||||
Bestellnummer (W7): `<JJ>-<6 Zeichen base32>` (z. B. `26-K7QM2X`), zufällig, bei Kollision im Shop neu ziehen. Keine
|
||||
fortlaufende Zählung nötig (kein Rechnungsnummernkreis, Rechnungen schreibt der Händler).
|
||||
|
||||
---
|
||||
|
||||
## 4. Shop-Verwaltung im Portal (`#webshop`)
|
||||
|
||||
Reiter: **Übersicht · Artikel · Bestellungen · Einstellungen · Rechtliches**. Projektwahl wie beim Terminplaner (Shop je
|
||||
Website-Projekt).
|
||||
|
||||
### 4.1 Übersicht
|
||||
Checkliste „Bereit zum Öffnen“ mit Haken: Einbau fertig (`active`), Stripe verbunden und Zahlungen freigeschaltet
|
||||
(`charges_enabled`), Rechtstexte vollständig und bestätigt, mindestens ein aktiver Artikel, Versand oder Abholung
|
||||
eingerichtet. Schalter „Shop geöffnet“ (nur wenn alle Haken grün; serverseitig erneut geprüft). Kennzahlen: offene
|
||||
Bestellungen, Bestellungen 30 Tage, Umsatz 30 Tage (nur Anzeige). Knopf „Shop-Vorschau“ (Standalone-Seite, Abschnitt 5.4).
|
||||
|
||||
### 4.2 Stripe-Konto
|
||||
Knopf „Stripe-Konto einrichten“: Server legt das verbundene Konto an (Land DE, E-Mail und Website des Kunden
|
||||
vorbefüllt) und holt einen Onboarding-Link (Account Link), Weiterleitung zu Stripe; Rücksprung `#webshop?stripe=zurueck`
|
||||
liest den Status neu. Danach: Status (Zahlungen aktiv / Auszahlungen aktiv / fehlende Angaben) und Knöpfe
|
||||
„Angaben ergänzen“ (neuer Account Link) und „Stripe-Dashboard öffnen“ (dashboard.stripe.com). Status wird zusätzlich
|
||||
per Connect-Webhook `account.updated` aktuell gehalten.
|
||||
|
||||
### 4.3 Artikel
|
||||
Liste (sortierbar mit Hoch/Runter, aktiv/inaktiv, Bestand) und Editor:
|
||||
- Name, Kategorie (frei, gruppiert im Shop), Kurzbeschreibung (≤ 300), Beschreibung (≤ 5000, Absätze, kein HTML).
|
||||
- Preis brutto in € (PAngV: Gesamtpreis), Steuersatz (19 / 7 / 0 % – bei Kleinunternehmer ausgeblendet).
|
||||
- Grundpreis (PAngV § 4): Menge + Einheit (g, kg, ml, l, m, m², Stück-los) → Anzeige „x € / 1 kg“ bzw. „/ 100 g“ nach
|
||||
§ 5 PAngV (bei ≤ 250 g/ml darf 100 g/ml verwendet werden; V1 rechnet immer auf 1 kg / 1 l / 1 m / 1 m²).
|
||||
- Bestand (leer = unbegrenzt) und „ausverkauft anzeigen“ statt ausblenden.
|
||||
- Varianten (optional, eine Dimension, z. B. „Größe“): je Wert eigener Preis und Bestand (max 30).
|
||||
- Bilder: bis 4, JPEG/PNG/WebP ≤ 8 MB, serverseitig geprüft (Signatur), erstes = Titelbild.
|
||||
- Lieferart: Versand und Abholung / nur Abholung.
|
||||
- Widerruf (§ 312g Abs. 2 BGB): „gesetzliches Widerrufsrecht“ (Standard) oder Ausschluss mit Grund: schnell
|
||||
verderblich, nach Kundenwunsch angefertigt, versiegelt aus Hygienegründen. Der Shop zeigt den Hinweis am Artikel und in
|
||||
der Bestellübersicht, die Bestellbestätigung nennt ihn.
|
||||
- Produktsicherheit (GPSR Art. 19): Hersteller (Name, Anschrift, E-Mail/Website; Vorgabe „ich selbst“ = Anbieterdaten),
|
||||
bei Herstellern außerhalb der EU die verantwortliche Person in der EU, Warn- und Sicherheitshinweise (Freitext).
|
||||
- Lebensmittel (LMIV Art. 14): Schalter „ist ein Lebensmittel“ → Pflichtfeld „Pflichtangaben“ (Bezeichnung, Zutaten,
|
||||
Allergene hervorgehoben, Menge, ggf. Hersteller), wird vor dem Kauf am Artikel gezeigt.
|
||||
|
||||
### 4.4 Bestellungen
|
||||
Liste (Status-Filter, Suche nach Nummer/Name), Detail mit Positionen, Lieferart, Adresse, Zahlung, Verlauf. Aktionen:
|
||||
„Versendet“ (optional Sendungsnummer + Versanddienst → Mail G10), „Abholbereit“ (→ G11), „Abgeschlossen“, „Erstatten“
|
||||
(voll oder Teilbetrag, Grund; Stripe-Refund auf dem verbundenen Konto → G13), „Stornieren“ (nur unbezahlt). Widerrufe
|
||||
erscheinen an der Bestellung und als eigene Liste. CSV-Export (Bestellungen eines Zeitraums).
|
||||
|
||||
### 4.5 Einstellungen
|
||||
Shopname, Anbieter (Firma/Name, Anschrift, E-Mail, Telefon, optional USt-IdNr.), Steuer (Kleinunternehmer ja/nein),
|
||||
Versand (an/aus, Länder DE / DE+AT, Versandkosten je Land, versandkostenfrei ab), Abholung (an/aus, Adresse, Hinweis z. B.
|
||||
Öffnungszeiten), Lieferzeit (Pflicht, z. B. „2–4 Werktage“), Zahlungsarten (Karte inkl. Apple Pay/Google Pay immer;
|
||||
Klarna und SEPA-Lastschrift wählbar – nur einschalten, wenn im eigenen Stripe-Konto aktiviert; SEPA mit Warnung, weil die
|
||||
Zahlung erst nach Tagen sicher ist; der Shop zeigt genau diese Liste),
|
||||
Akzentfarbe, Mindestbestellwert (optional).
|
||||
|
||||
### 4.6 Rechtliches (Händler-Rechtstexte) und „Shop geöffnet“
|
||||
Der Händler ist Verkäufer und verantwortlich. WEBklar liefert Muster, keine Rechtsberatung (W8, RDG):
|
||||
- **Widerrufsbelehrung**: vorbefüllt mit dem gesetzlichen Muster (Anlage 1 zu Art. 246a § 1 Abs. 2 EGBGB) inkl. der
|
||||
Hinweise auf die elektronische Widerrufsfunktion, Name/Anschrift/E-Mail eingesetzt; Rücksendekosten trägt der Käufer
|
||||
(Standard, umschaltbar); Muster-Widerrufsformular (Anlage 2). Bearbeitbar; „Auf Muster zurücksetzen“.
|
||||
- **AGB**: Muster-AGB für kleine Shops (Vertragsschluss, Preise, Versand, Zahlung über Stripe, Eigentumsvorbehalt,
|
||||
Gewährleistung gesetzlich, Streitbeilegung VSBG-Hinweis), deutlich als Muster gekennzeichnet; bearbeitbar.
|
||||
- **Versand- und Zahlungsinformationen**: aus den Einstellungen erzeugt (nicht frei editierbar, damit sie stimmen).
|
||||
- **Impressum-URL** und **Datenschutz-URL** (Pflicht, https).
|
||||
- Bestätigung: Haken „Ich habe die Texte geprüft. Ich bin als Verkäufer für Rechtstexte, Produktangaben und Preise
|
||||
verantwortlich. WEBklar stellt Muster bereit und berät nicht rechtlich.“ (Zeit, Person, Hash der Texte gespeichert;
|
||||
jede Änderung der Texte hebt die Bestätigung auf und schließt den Shop nicht, verlangt aber neue Bestätigung vor der
|
||||
nächsten Öffnung — Festlegung: geöffnet bleibt geöffnet, Banner „bitte neu bestätigen“; neue Bestellungen werden mit
|
||||
der zuletzt bestätigten Fassung geschlossen, siehe 5.2).
|
||||
|
||||
**Shop geöffnet** (serverseitig bei jeder Bestellung geprüft): Funktion `active` · `geoeffnet` · Stripe `charges_enabled` ·
|
||||
bestätigte Rechtstexte vorhanden · Artikel lieferbar. Fällt ein Punkt weg, zeigt das Widget „Der Shop ist gerade
|
||||
geschlossen“ und nimmt keine Bestellungen an.
|
||||
|
||||
---
|
||||
|
||||
## 5. Shop auf der Website (Widget `P/public/embed/shop.js`)
|
||||
|
||||
### 5.1 Einbau
|
||||
`<div data-webklar-shop="<siteKey>"></div>` + `<script src="https://project.webklar.com/embed/shop.js" defer></script>`
|
||||
(Muster News/Termine). Eigene Styles mit Präfix/Shadow-DOM nach Vorbild Termine, Akzentfarbe aus den Einstellungen.
|
||||
Ansichten: Kategorien + Artikelraster, Artikeldetail (Bilder, Beschreibung, Preis, Grundpreis, Varianten, Lieferhinweis,
|
||||
Widerrufsausschluss, Produktsicherheit, Lebensmittelangaben), Warenkorb (im `sessionStorage` des Besuchers, nur Artikel-IDs
|
||||
und Mengen; Begründung: technisch notwendig, § 25 Abs. 2 Nr. 2 TDDDG), Kasse, Danke-Seite, Widerrufsformular. Ganz oben
|
||||
im Widget (Beginn des Bestellvorgangs, § 312j Abs. 1 BGB): Lieferländer und akzeptierte Zahlungsarten. Fußzeile immer:
|
||||
AGB · Widerrufsbelehrung · Versand & Zahlung · **Vertrag widerrufen** · Impressum · Datenschutz.
|
||||
|
||||
### 5.2 Kasse und Vertragsschluss (W9)
|
||||
Die verbindliche Bestellung geschieht **im Widget** mit dem Knopf **„Zahlungspflichtig bestellen“** (§ 312j Abs. 3 BGB);
|
||||
Stripe Checkout ist danach nur noch die Zahlung. Unmittelbar über dem Knopf (§ 312j Abs. 2, Art. 246a § 1 EGBGB):
|
||||
Positionen mit wesentlichen Merkmalen, Einzel- und Gesamtpreis inkl. USt (bzw. Kleinunternehmer-Hinweis), Versandkosten,
|
||||
Gesamtsumme, Lieferart, Lieferzeit, Zahlungsarten, Hinweis auf Widerrufsrecht bzw. dessen Ausschluss je Artikel, Link
|
||||
AGB und Widerrufsbelehrung, Anbieter-Identität. Lieferart wählt der Käufer im Widget; Name, E-Mail, Telefon (optional)
|
||||
und Lieferadresse sammelt Stripe Checkout (`shipping_address_collection` nur bei Versand, erlaubte Länder aus den
|
||||
Einstellungen). Korrektur von Eingabefehlern (§ 312i Abs. 1 Nr. 1): Warenkorb und Lieferart sind bis zum Knopf änderbar,
|
||||
Adresse in Stripe bis zur Zahlung.
|
||||
|
||||
Server (`POST /api/public/shop/:siteKey/bestellung`): prüft „Shop geöffnet“, rechnet alle Preise aus der Datenbank neu
|
||||
(nie aus dem Browser), prüft und **reserviert** Bestand (offene Bestellung zählt bis `ablaufAt`), legt `shopOrders` mit
|
||||
Status `offen` an (Schnappschuss der Positionen und der bestätigten Rechtstexte-Fassung), erstellt die Checkout-Session
|
||||
**auf dem verbundenen Konto** (`stripeAccount`): `mode: payment`, `price_data` je Position + Versandkosten als
|
||||
`shipping_options` (fester Betrag aus dem Server), `payment_method_types` aus den Einstellungen, `locale: 'de'`,
|
||||
`expires_at` = jetzt + 30 min, `client_reference_id` = Bestell-ID, `metadata` {shopId, orderId, bestellnummer},
|
||||
`customer_creation: 'if_required'`, `phone_number_collection`. `success_url` = Seite des Kunden mit
|
||||
`?wk_bestellung=<id>&wk_t=<token>` (nur erlaubte Hosts des Projekts, sonst Standalone-Seite), `cancel_url` = Warenkorb.
|
||||
Unter einer Sperre je Shop (`withLocks`), damit zwei gleichzeitige Käufe das letzte Stück nicht doppelt reservieren.
|
||||
|
||||
### 5.3 Zahlung, Bestätigung, Verlauf
|
||||
- **Connect-Webhook** `POST /api/shop/stripe-webhook` (eigenes Signatur-Geheimnis `STRIPE_CONNECT_WEBHOOK_SECRET`,
|
||||
Endpunkt mit `connect: true`): `checkout.session.completed` (bei `payment_status: paid`),
|
||||
`checkout.session.async_payment_succeeded` → `bezahlt`, Bestand endgültig abziehen, Käuferdaten aus der Session
|
||||
speichern, **G09 Bestellbestätigung** an den Käufer, **K25 Neue Bestellung** an den Händler.
|
||||
`async_payment_failed` / `expired` → `storniert`, Reservierung frei (G14 nur bei fehlgeschlagener Zahlung).
|
||||
`account.updated` → `stripeStatus`. `charge.refunded` → Erstattung aus dem Stripe-Dashboard nachtragen.
|
||||
Alles idempotent (Status-Übergänge nur vorwärts, Sperre je Bestellung).
|
||||
- **Danke-Seite** fragt `GET /api/public/shop/:siteKey/bestellung/:id?t=<token>`; ist die Bestellung noch `offen`, holt der
|
||||
Server die Session bei Stripe und schließt selbst ab (gleicher Code wie der Webhook) — fällt der Webhook aus, geht
|
||||
trotzdem nichts verloren.
|
||||
- **G09 Bestellbestätigung** (dauerhafter Datenträger, § 312f Abs. 2 BGB): Anbieter, Bestellnummer, Positionen,
|
||||
Preise, Versand, Gesamt, Lieferart/-zeit, Lieferadresse, Zahlungsart, Hinweis auf Widerrufsausschlüsse, vollständige
|
||||
AGB, vollständige Widerrufsbelehrung und Muster-Widerrufsformular (im Mailtext), Link „Vertrag widerrufen“.
|
||||
Absender: WEBklar-Versandadresse mit Anzeigename „<Shopname>“, Reply-To = E-Mail des Händlers.
|
||||
|
||||
### 5.4 Standalone-Seite und Widerrufsfunktion (W10, § 356a BGB)
|
||||
- `https://project.webklar.com/shop/<siteKey>` zeigt das Widget auf einer neutralen Seite (Vorschau, Rücksprung-Ersatz).
|
||||
- `https://project.webklar.com/shop/<siteKey>/widerruf` und die Widget-Ansicht „Vertrag widerrufen“: Schaltfläche
|
||||
„Vertrag widerrufen“ führt zu einem Formular (Name, E-Mail, Bestellnummer, optional Erklärung/Artikel), danach
|
||||
Bestätigungsschaltfläche **„Widerruf bestätigen“**. Eingang wird gespeichert, **G12 Eingangsbestätigung** mit Inhalt,
|
||||
Datum und Uhrzeit sofort an den Käufer (dauerhafter Datenträger), **K26** an den Händler. Erreichbar ohne Login, auch
|
||||
ohne passende Bestellung (der Händler klärt), Missbrauchsschutz per Rate-Limit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bestellung der Funktion bei WEBklar (Register-Eintrag `shop`)
|
||||
|
||||
Registereintrag nach Muster `termine`: `product: 'shopSetup'` (29900 Cent, Lookup-Key `webklar_shop_einrichtung_29900`),
|
||||
`priceLabel '299 €'`, `setupEstimate 'meist innerhalb von 2–3 Wochen'`, `requiresWartungsAbo: false`,
|
||||
`ticketFreischaltung: true`, `returnHash 'webshop'`, `returnProjekt: true`, Haken:
|
||||
- `verfuegbar(email)`: Schalter `SHOP_ENABLED` / Pilotliste.
|
||||
- `bestellpruefung`: Anfrage des Projekts ist `angenommen` (sonst offener Punkt „Anfrage noch nicht freigegeben“).
|
||||
- `unterlagenBereit`: Leistungsbeschreibung + AVV Webshop der aktuellen Fassung abrufbar (HTML + PDF, sha256).
|
||||
- `belehrung`: die gemeinsame Widerrufsbelehrung der Funktionen.
|
||||
- `freischaltbar(doc)`: wie News (Ticket geschlossen genügt).
|
||||
|
||||
Zustimmungen (alle Haken Pflicht, Fassung = `config.shop.fassung`):
|
||||
1. **unterlagen**: Leistungsbeschreibung Webshop und AVV Webshop in der Fassung X.
|
||||
2. **verantwortung**: „Ich verkaufe als Unternehmer in eigenem Namen. Für Produkte, Preise, Produktangaben und meine
|
||||
Rechtstexte bin ich verantwortlich. Ich verkaufe keine ausgeschlossenen Waren (Leistungsbeschreibung Nr. 3).“
|
||||
3. **stripe**: „Zahlungen laufen über mein eigenes Stripe-Konto zu den Bedingungen von Stripe. WEBklar erhält kein Geld
|
||||
meiner Käufer.“
|
||||
4. **beginn**: vorzeitiger Beginn, Erlöschen (§ 356 Abs. 5 Nr. 2 BGB), Wertersatz (§ 357a Abs. 2 BGB) – Wortlaut wie
|
||||
Terminplaner.
|
||||
5. **information**: Pflichtinformationen (Leistung, 299 € einmalig ohne USt nach § 19 UStG, Dauer, Laufzeit „solange die
|
||||
Website bei WEBklar läuft“), Widerrufsbelehrung mit Muster-Formular gelesen.
|
||||
|
||||
Ticket: Betreff „Webshop einbauen – <Projekt>“, Details: siteKey, Snippet, Domain, Anfrage (Warenart, Versand/Abholung,
|
||||
Artikelzahl), Schritte (Shop-Seite anlegen, Snippet einsetzen, Menüpunkt, Fußzeilen-Link „Vertrag widerrufen“ auf allen
|
||||
Seiten, Datenschutz-Absatz Webshop einfügen, Impressum prüfen, Testkauf im Stripe-Testmodus entfällt → Vorschau prüfen),
|
||||
Verweis WOMS. Einbauanleitung `docs/funktionen/shop-einbau.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. WOMS
|
||||
|
||||
Funktionen-Panel des Kunden: Webshop-Zeile mit Anfrage (Inhalt, Zeit) und Knöpfen **„Anfrage annehmen“** /
|
||||
**„Ablehnen“** (Begründung Pflicht) → Portal-Admin-Route; Zustand, Ticket, Stripe-Status (nur lesen), Kennzahlen.
|
||||
Aktivieren/Abschalten über die vorhandenen generischen Aktionen. „Rechtliches“: Karten Leistungsbeschreibung Webshop und
|
||||
AVV Webshop.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mails
|
||||
|
||||
Neue IDs (nächste freie laut Test-Liste; Zählung im Plan geprüft): **K24** „Webshop-Anfrage: Entscheidung“ (Varianten
|
||||
angenommen/abgelehnt), **K25** „Neue Bestellung im Webshop“ (an Händler), **K26** „Widerruf im Webshop eingegangen“ (an
|
||||
Händler), **T21** „Neue Webshop-Anfrage“ (Team). Käufer-Mails **G09** Bestellbestätigung, **G10** versendet, **G11**
|
||||
abholbereit, **G12** Eingangsbestätigung Widerruf, **G13** Erstattung, **G14** Zahlung fehlgeschlagen — eigene Vorlagen
|
||||
im Stil der G-Vorlagen des Terminplaners (Absender WEBklar, Anzeigename Shop, Reply-To Händler), über die vorhandene
|
||||
Gast-Mail-Warteschlange, falls wiederverwendbar (Plan), sonst eigene kleine Warteschlange mit Wiederholung.
|
||||
K15/K16/T16 bekommen ihre Texte aus dem Register (vorhanden).
|
||||
|
||||
---
|
||||
|
||||
## 9. Datenschutz und Aufbewahrung
|
||||
|
||||
- Rollen: Händler = Verantwortlicher für Käuferdaten; WEBklar = Auftragsverarbeiter (AVV Webshop, Hetzner als
|
||||
Unterauftragsverarbeiter, Mailversand über Hetzner); Stripe = eigener Vertragspartner des Händlers (nicht
|
||||
Unterauftragsverarbeiter von WEBklar). Gespeichert werden nur Bestelldaten, die für Abwicklung und Nachweis nötig sind.
|
||||
- Aufbewahrung (W11): personenbezogene Käuferdaten (Name, E-Mail, Telefon, Adressen) einer Bestellung werden **3 Jahre
|
||||
nach Ende des Bestelljahres** anonymisiert (Gewährleistung 2 Jahre + Puffer); Beträge, Positionen, Bestellnummer
|
||||
bleiben für die Buchhaltung des Händlers (dessen Aufbewahrungspflicht deckt der Händler über Stripe-Belege und eigene
|
||||
Unterlagen; er kann jederzeit CSV exportieren). Unbezahlte/stornierte Bestellungen: Käuferdaten nach 30 Tagen weg.
|
||||
Widerrufe: wie die Bestellung. Wird der Shop abgeschaltet: 90 Tage Lesen + Export, dann Löschung aller Shopdaten
|
||||
(Bilder eingeschlossen). Job im bestehenden Löschlauf (`retention.js`).
|
||||
- Portal-Datenschutzerklärung: neuer Abschnitt „Webshop“ (Stripe Connect: Weitergabe von E-Mail/Website des Kunden an
|
||||
Stripe zur Kontoeröffnung, Abruf des Kontostatus; Verarbeitung als Auftragsverarbeiter).
|
||||
- Muster-Datenschutzabsatz für die Website des Händlers (`docs/funktionen/shop-datenschutz-absatz.md`): Bestellabwicklung,
|
||||
WEBklar als Auftragsverarbeiter, Stripe (inkl. PayPal/Klarna über Stripe), Warenkorb im Browser-Speicher.
|
||||
|
||||
---
|
||||
|
||||
## 10. Rechtstexte von WEBklar (neu, Fassung `2026-10-05`)
|
||||
|
||||
1. **Leistungsbeschreibung Webshop** (`public/rechtliches/leistungsbeschreibung-webshop-2026-10-05.html` + PDF):
|
||||
Leistung (Anfrage, Einrichtung, Einbau, Betrieb des Widgets, Verwaltung im Portal), ausgeschlossene Waren,
|
||||
Voraussetzungen (eigenes Stripe-Konto, Unternehmer, eigene Rechtstexte), keine Rechtsberatung/Muster ohne Gewähr,
|
||||
Verfügbarkeit (keine Zusage einer bestimmten Verfügbarkeit über das Hosting hinaus), Haftung (Begrenzung wie
|
||||
bestehende LB; keine Haftung für Inhalte, Preise und Rechtstexte des Händlers), Preis 299 € einmalig, Laufzeit/Ende
|
||||
(mit dem Website-Betrieb; Abschalten, 90 Tage Export, Löschung), Änderungen.
|
||||
2. **AVV Webshop** (Geschwister AVV Terminplaner): Gegenstand, Art der Daten (Käufer: Name, E-Mail, Telefon, Liefer- und
|
||||
Rechnungsadresse, Bestellung, Widerrufe), Betroffene (Käufer), Unterauftragsverarbeiter Hetzner, Löschfristen genau wie
|
||||
gebaut, TOM-Anlage, Weisungen über das Portal.
|
||||
3. Gemeinsame **Widerrufsbelehrung Funktionen** bleibt (bestehende Datei der aktuellen Fassung).
|
||||
4. **Kanzlei-Fragen** `docs/rechtliches/webshop-kanzlei-paket.md` (Muster-AGB, Muster-Widerrufsbelehrung inkl. 356a,
|
||||
Zustimmungstexte, LB-Haftung, Rolle Plattform/ZAG) – vor `on`, nicht vor `pilot`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Livegang (je Schritt mit Freigabe des Inhabers; erst nach TP2-Livegang)
|
||||
|
||||
1. Stripe-Dashboard WEBklar: **Connect aktivieren** (Plattformprofil: „Plattform, Verkäufer verkaufen eigene Waren,
|
||||
Direct Charges, keine Gebühr“), Branding (Name/Icon/Farbe für das Onboarding).
|
||||
2. `node scripts/shop-setup.mjs` (Collections) und `scripts/stripe-products.mjs --anlegen` (shopSetup).
|
||||
3. Connect-Webhook anlegen (`scripts/shop-setup.mjs --webhook` oder Dashboard, Ereignisse aus 5.3), Geheimnis als
|
||||
`STRIPE_CONNECT_WEBHOOK_SECRET` in `/home/webklar/apps/.env`, Compose-Zeilen `SHOP_ENABLED`, `SHOP_PILOT_EMAILS`.
|
||||
4. Push `feature/webshop` → `main` (nach TP2), WOMS-Zweig → `test`.
|
||||
5. Pilot mit Testkonto: Anfrage, Freigabe, Bestellung (Test-Coupon), Einbau auf `test-website`, Stripe-Konto im
|
||||
Testmodus? (Live-Konto des Pilots; Testkauf 1 € mit Erstattung), Widerruf testen.
|
||||
6. Kanzlei-Paket → Befund einarbeiten → `SHOP_ENABLED=on`.
|
||||
|
||||
---
|
||||
|
||||
## 12. Entscheidungen (Claude, 2026-10-05, ohne Rückfrage auf Wunsch des Inhabers)
|
||||
|
||||
| Nr. | Entscheidung | Begründung |
|
||||
|---|---|---|
|
||||
| W1 | Nur Stripe; PayPal/Klarna/Wallets über Stripe Checkout | eine Integration, gleiche Zahlungsarten |
|
||||
| W2 | Stripe Connect, eigenes Konto des Händlers, Direct Charges, keine Plattformgebühr | kein Fremdgeld bei WEBklar (ZAG), Händler hat volles Dashboard |
|
||||
| W3 | Schalter `SHOP_ENABLED` off/pilot/on | wie Terminplaner |
|
||||
| W4 | „Auf Anfrage“ = Anfrage → Team-Freigabe → Bestellung | Prüfung erlaubter Waren, Wunsch des Inhabers |
|
||||
| W5 | 299 € einmalig, kein Abo nötig, keine laufende Gebühr | „einmalige Kostensache“ |
|
||||
| W6 | Verwaltung ab `setup` nutzbar | Shop ist zum Einbau fertig befüllt |
|
||||
| W7 | Bestellnummer zufällig `JJ-XXXXXX` | keine Zählung/Sperre nötig |
|
||||
| W8 | Händler-Rechtstexte als Muster + Pflichtbestätigung, keine Rechtsberatung | RDG, Verantwortung beim Verkäufer |
|
||||
| W9 | Vertragsschluss im Widget mit „Zahlungspflichtig bestellen“, Stripe nur Zahlung | § 312j Abs. 3 BGB sicher erfüllt |
|
||||
| W10 | Widerrufsfunktion (§ 356a BGB) im Widget + Standalone-Seite | seit 19.06.2026 Pflicht |
|
||||
| W11 | Käuferdaten 3 Jahre nach Bestelljahr anonymisieren, Shopdaten 90 Tage nach Abschalten löschen | Datensparsamkeit, Gewährleistung |
|
||||
| W13 | Hinweis auf die gesetzliche Gewährleistung (VO (EU) 2025/1960) in Kasse und Bestätigung | Pflicht seit 27.09.2026 |
|
||||
| W14 | Stripe Accounts v1 mit `controller` statt v2 | v1 weiter unterstützt, passt zu stripe-node 19, kein Plattform-Umbau |
|
||||
| W12 | Versand nur DE/AT, keine digitalen Produkte, keine Gutscheine | Umfang V1, Recht (digitale Inhalte, Mehrzweckgutscheine) |
|
||||
Reference in new issue
Block a user