1223 lines
109 KiB
Markdown
1223 lines
109 KiB
Markdown
# Kundenansicht neu, P4 Funktionsmodule: Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Die gemeinsamen Funktionsmodule (Terminplaner, News, Webshop, Buchungskalender, Öffnungszeiten live, Analyse) bekommen ihr weißes Aussehen unter `.kn` (`public/neu/module.css`) und verhalten sich bei `modell === 'abo'` ohne Preise und Bestellseiten: Einschalten statt Kaufen, Reiter in Etage 2. Die alte Ansicht verhält sich weiter exakt wie heute.
|
||
|
||
**Architecture:** Jede Verzweigung hängt an `modell === 'abo'` in der Antwort von `GET /api/funktionen`, die alle sechs Module beim Start laden. Den Einschalt-Ablauf von P3 (`zeigeEinschalten`) bekommen die Module als Init-Option `einschalten`; die gemeinsame Seite `renderEinschaltSeite` in `funktion-bestellen.js` zeigt Zusammenfassung und offene Punkte und mountet ihn. Die Reiter von Terminplaner, Webshop, Buchungskalender und Analyse zeichnet im Abo-Modell die Etage von P1; die Module melden sie per `kn:reiter` (Kurzlabels, wo nötig). `module.css` ersetzt `termine.css`, `shop.css`, `buchungen.css`, `zeiten.css`, `funktionen.css` (News) und `analyse.css` vollständig; ein Prüftest sichert Klassen-Abdeckung, Scope `.kn`, Palette und Verbote.
|
||
|
||
**Tech Stack:** Vanilla-JS-ES-Module ohne Build (`public/`), Tests mit `node --test` und `test/support/fakeDom.js` (nur `h()`/`textContent`), CSS, Sichtprüfung nur mit dem Werkzeug aus P0.
|
||
|
||
**Spec:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (Abschnitte 5.1, 5.4–5.6, C2, C9, C12, C15)
|
||
|
||
**Übersicht/Verträge:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht/docs/superpowers/plans/2026-10-16-kundenansicht-00-uebersicht.md` (gilt wörtlich)
|
||
|
||
**Worktree und Branch:** `/home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui`, Branch `kn/ui`. Start nach P3 (Tag `kn-p3`), Ende mit Tag `kn-p4`. Alle Pfade unten sind relativ zu diesem Worktree.
|
||
|
||
## Global Constraints
|
||
|
||
Alle Global Constraints und Verträge V1–V13 der Übersicht gelten (Aussehen, „du“, keine Preise für Funktionen, Hashes, Rechtlich fest, Testbefehle mit Speicherbremse, Commits mit Zeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`, Sicherheitsblock, kein Push). Paketeigene Zusätze:
|
||
|
||
- **Alte Ansicht unverändert.** Verzweigt wird nur mit `antwort.modell === 'abo'` (strikt, ohne trim oder lowercase). `public/dashboard.html`, `app.js` und alle alten CSS-Dateien bleiben unberührt. Bestehende Testdateien ändert dieses Paket nur an den zwei Pins der Dateiübersicht.
|
||
- **Quelle von `modell`.** Alle sechs Module laden beim Start `GET /api/funktionen` und lesen `modell` und `kundenart` dort. Die Unter-Reiter-Module (`termine-tab-*`, `shop-tab-*`, `buchungen-tab-*`, `zeiten-editor.js`, `analyse-cards.js`) brauchen kein `modell` und bleiben unverändert.
|
||
- **Pins, die neue Importzeilen verbieten** (Erweiterung nur über benannte Importe einer bestehenden Importzeile):
|
||
- `funktionen.js`: `termineView.test.js:315-320`, `shopWiring.test.js:40-42`, `termineFragebogenView.test.js:1033-1034` (genau zwei absolute Importe, ein relativer, kein `import(`, kein Wort `funktion-bestellen`).
|
||
- `funktion-bestellen.js`: `termineFragebogenView.test.js:994-1034` (nur `./termine-dom.js` und `./termine-view.js`, kein `import(`, kein Wort „zahlungspflichtig“, „details“, „summary“, kein `innerHTML`, `href` nur über `sicherLink`).
|
||
- `termine-fragebogen.js`: `termineFragebogenView.test.js:3072-3086` (acht Importe fest, kein `href`, kein `PATCH`).
|
||
- `zeiten.js`, `zeiten-bestellen.js`: `zeitenBestellen.test.js:841-865`, `zeitenView.test.js:390-399` (Importlisten fest, kein `import(`).
|
||
- `termine.js`: `termineView.test.js:373-394, 450-453` (nur relative statische Importe, je einmal `location.hash` und `history.replaceState`, einmal `api: {`, kein Text „Wartungs-Abo“, „gekündigt“, `'vertraege'`). Neue `termine*.js`-Dateien sind verboten (`termineView.test.js:324-329`).
|
||
- Die `history.replaceState`-Aufrufe der Rückkehr-Query in `termine.js`, `shop.js`, `zeiten.js`, `funktionen.js` bleiben (P1 stempelt den Verlauf danach neu).
|
||
- **Init-Optionen.** `initTermine`, `initNews`, `initShop`, `initBuchungen`, `initZeiten` nehmen zusätzlich `einschalten` (VF1). News und Analyse lesen die Projektwahl auch aus `root.dataset.wunschProjekt` (V2) und entfernen das Attribut.
|
||
- **Reiter (P1, Vertragsfrage 5).** Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und Analyse keine eigene Leiste, sondern melden `kn:reiter` (Spec 5.2 und 5.4: gemeinsame Mechanik „statt der vier eigenen Kopien“, Unterseiten-Reiter immer unten in Etage 2). Die Analyse meldet Übersicht, Heatmap und Einstellungen aus der Closure `mountCockpit` in `analyse.js` (VF2).
|
||
- **Neue Dateien außerhalb der Zuständigkeitstabelle:** `public/fn-reiter.js`, `test/support/modulAbo.js`, `test/kundenansichtNeuModul*.test.js`. `public/neu/module.css` legt P1 als Stub an; ab P4 gehört die Datei P4.
|
||
- **Texte im Abo-Modell** stehen wörtlich im Abschnitt „Texte“. Kein Preis, kein „Zahlungspflichtig“, kein „Behalten“, Du-Form.
|
||
- **CSS:** jede Regel unter `.kn`; Farben nur aus der Palette der Übersicht plus die drei Zettelfarben der News; Fokus `outline: 2px solid #111`; keine Verläufe, kein Glas, kein Dunkelmodus, keine Animationen außer `transition` bis 150 ms; `geraete.css` bleibt unberührt (Geräterahmen der Heatmap sind Abbildungen). Inhaltsfarben der Kunden (Widget-Akzentfarbe, Banner-Farbwahl der Analyse, Zettelfarben) bleiben.
|
||
- Die Annahmen der Vertragsfragen VF1–VF6 (Ende des Dokuments) gelten, bis der Controller anders entscheidet.
|
||
|
||
## Review Focus
|
||
|
||
1. **Alte Ansicht bricht:** `modell` fehlt, ist leer oder falsch geschrieben (`'Abo'`, `'abo '`): Bestellseite mit Preis, „Behalten“, Unternehmer-Haken, eigene Reiterleiste mit langen Labels bleiben wie heute. Tests: Task 1 und 3 bis 8 (je „ohne modell“), Task 15 (Bestand-Tests).
|
||
2. **Preis- und Zahlungswörter im Abo-Modell:** Reste aus alten Pfaden (`priceLabel`, `ustHinweis`, „Zahlung fortsetzen“, „Für 30 € behalten“, „299 €“) auf Einschalt-, Anfrage- und Statusflächen. Tests: `KEIN_PREIS` in Task 2 und 4 bis 8.
|
||
3. **Reiterleiste doppelt oder fehlend:** Im Abo-Modell erscheint sie in Modul und Etage zugleich oder in keinem von beiden; eine Ansicht ohne Reiter lässt die Reiter der vorigen stehen; `bei(key)` wechselt nicht. Tests: Task 1 (alle drei Module), Task 8 (Analyse, auch leere Liste), Task 3 und 5 (leere Liste).
|
||
4. **Privatkunde oder leere Kundenart im Webshop:** das Formular erscheint trotzdem, oder ein aktiver Bestands-Shop verschwindet. Privat und leer bekommen Hinweis plus Chat, nie das Formular; `state !== 'off'` bleibt bedienbar. Test: Task 5.
|
||
5. **Dunkle Reste im CSS:** eine vergessene Klasse behält Weiß-auf-Dunkel und wird auf Weiß unleserlich. Tests: Abdeckung je Präfix in Task 9–13, Gates in Task 14.
|
||
|
||
## Dateiübersicht
|
||
|
||
| Datei | Aufgabe | Task |
|
||
|---|---|---|
|
||
| `public/fn-reiter.js` (neu) | `reiterEintraege`, `meldeReiter`, `KURZLABELS` | 1 |
|
||
| `public/termine.js`, `public/shop.js`, `public/buchungen.js` | `modell` lesen, im Abo-Modell Reiter melden (`:49-56`, `:100-106`, `:322-394`; `:295-360`; `:39-41`, `:155-205`) | 1 |
|
||
| `public/funktion-bestellen.js` | `renderEinschaltSeite` (nach `:123`), Blöcke aus `baueSeite` (`:146-170`) herausziehen, `statusKarte` (`:306`, `:315`) mit `modell` | 2 |
|
||
| `public/termine.js`, `public/termine-fragebogen.js` | `einschalten`, `fertig` im Kontext, letzter Schritt (`:27-29`, `:80`, `:90`, `:98`, `:101-104`) | 3 |
|
||
| `public/funktionen.js` | News: Einschalt-Seite (`:474-515`), Projektwahl aus `dataset` | 4 |
|
||
| `public/shop.js`, `public/shop-anfrage.js` | Anfrage ohne Unternehmer, Satz, Privat-Hinweis, Einschalten (`:64-74`, `:195`, `:253`; `:37`, `:67-68`, `:115-120`) | 5 |
|
||
| `public/buchungen.js`, `public/buchungen-anfrage.js` | Anfrage ohne Preis, Einschalten (`:32-41`, `:124-130`; `:11`, `:20`, `:94`) | 6 |
|
||
| `public/zeiten.js`, `public/zeiten-bestellen.js` | Einschalten, kein Behalten (`:55`, `:62-73`, `:132`, `:161`, `:218-226`; `:105`, `:289`) | 7 |
|
||
| `public/analyse.js` | Aktivierung mit Linkzeile und Zielgruppen-Häkchen (`:22-49`, `:76-112`); Reiter als `kn:reiter` melden (`:22-49`, `mountCockpit` `:174-328`) | 8 |
|
||
| `public/neu/module.css` (Stub von P1) | alle Modul-Klassen unter `.kn` | 9–13 |
|
||
| `public/neu/bilder/analyse.webp` (P3, nur dieses Bild neu erzeugt) | Analyse in neuer Optik (P3 VF2) | 14 |
|
||
| `test/support/modulAbo.js` (neu) | Testhilfe | 1 |
|
||
| `test/kundenansichtNeuModul{Reiter,Einschalten,Termine,News,Shop,Buchungen,Zeiten,Analyse,Css}.test.js` (neu) | je Task der Namen | 1–9 |
|
||
| `test/shopWiring.test.js:31` (Pin) | Regex der Signatur `initShop(root, { projectId, einschalten } = {})` | 5 |
|
||
| `test/unterkunftWiring.test.js:130` (Pin) | Regex der Signatur `initBuchungen(root, { projectId, einschalten } = {})` | 6 |
|
||
| Werkzeug `/root/reports/kundenansicht-neu-2026-10-15/werkzeug/` (eigenes Git): `views.mjs`, `gates.mjs`, `fixtures/module.mjs` (neu, 17 Fälle), `fixtures/endpunkte-p4.mjs` (neu, Analyse-Endpunkte), Einträge in `fixtures/faelle.mjs`, `fixtures/index.mjs`, `fixtures/endpunkte.mjs`, `vertrag-endpunkte.mjs` | Ansichten, Gate `p4`, Fälle | 14 |
|
||
|
||
## Texte (Abo-Modell, wörtlich)
|
||
|
||
| Kürzel | Datei | Wortlaut |
|
||
|---|---|---|
|
||
| E1 | `funktion-bestellen.js`, `termine-fragebogen.js` | Überschrift der Seite mit Zusammenfassung und letzter Eintrag der Fortschrittsleiste: `Prüfen und einschalten` |
|
||
| E2 | `funktion-bestellen.js` | bei offenen Punkten: `Du kannst einschalten, sobald die offenen Punkte erledigt sind.` |
|
||
| E4 | `funktion-bestellen.js`, `funktionen.js` | ohne Funktion `einschalten`: `Das Einschalten ist gerade nicht verfügbar. Bitte lade die Seite neu oder schreib uns im Chat.` |
|
||
| E5 | `funktion-bestellen.js`, `zeiten-bestellen.js` | Faktenzeile der Statuskarte: `Eingeschaltet am` (statt `Bestellt am`) |
|
||
| F1 | `termine-fragebogen.js` | `Du siehst den Fragebogen in der Kundenansicht. Angaben kannst du ändern, Schritte bestätigen und einschalten kann nur der Kunde selbst.` |
|
||
| S1 | `shop-anfrage.js`, `buchungen-anfrage.js` | erster Stichpunkt statt der Preiszeile: `Im Wartungs-Abo enthalten.`; letzter Stichpunkt: `Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du einschalten.` |
|
||
| S2 | `shop-anfrage.js` | Satz über „Anfrage senden“: `Mit „Anfrage senden“ bestätigst du, dass du keine ausgeschlossenen Waren verkaufst (` + `AUSGESCHLOSSEN` + `).` |
|
||
| S3 | `shop.js` | Privat: Titel `Der Webshop ist für Betriebe`, Text `Für Privatpersonen gibt es den Webshop leider nicht. Stimmt das nicht? Schreib uns im Chat.`, dazu der Chat-Knopf |
|
||
| B1 | `shop.js`, `buchungen.js` | Gruppe der Zusammenfassung: Titel `Deine Anfrage` (in `shop.js` schon so) |
|
||
| Z2 | `zeiten-bestellen.js` | Abo-Ende in `setup`/`active`: `Dein Wartungs-Abo ist beendet. Öffnungszeiten live läuft noch bis <TT.MM.JJJJ> und schaltet sich dann ab.` |
|
||
| Z3 | `zeiten-bestellen.js` | abgeschaltet mit Daten: `Deine Eingaben bleiben[ bis <TT.MM.JJJJ>] gespeichert. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein.` |
|
||
| A1 | `analyse.js` | Linkzeile: `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung <avvVersion>).`, beide Wörter sind Links (`legal.lb`, `legal.avv`) |
|
||
| A2 | `analyse.js` | Zielgruppen-Zeile mit Häkchen: `Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.` |
|
||
| A3 | `analyse.js` | bei `reason === 'kein_abo'`: `Die Analyse ist im Hosting- und im Wartungs-Abo enthalten.` plus der Link `ZU_VERTRAEGE` |
|
||
|
||
## Tasks
|
||
|
||
### Task 1: Reiter im Abo-Modell (`kn:reiter`, C2) und Testhilfe
|
||
|
||
**Files:**
|
||
- Create: `public/fn-reiter.js`, `test/support/modulAbo.js`
|
||
- Modify: `public/termine.js` (`initTermine` `:49-56`, `ladeFunktionen` `:100-106`, `zeichneAnsicht`, `zeigeReiter` `:322-394`), `public/shop.js` (`zeichneAnsicht`, `zeigeReiter` `:295-360`), `public/buchungen.js` (`starte` `:39-41`, `zeichneAnsicht`, `zeigeReiter` `:155-205`)
|
||
- Test: `test/kundenansichtNeuModulReiter.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Ereignis `kn:reiter` der Etage (P1): `CustomEvent` mit `bubbles: true` und `detail: { eintraege: [{ key, label, kurz? }], aktiv, bei(key) }`; eine leere Liste heißt „keine Reiter“, dann füllt „Zurück“ die Zeile.
|
||
- Produces `public/fn-reiter.js` (keine Importe):
|
||
```js
|
||
export const KURZLABELS // { termine: { team: 'Team' }, shop: { uebersicht: 'Start', rechtliches: 'Recht' }, buchungen: { unterkuenfte: 'Preise' } }; die Analyse (Übersicht, Heatmap, Einstellungen) braucht keine Kurzlabels und hat deshalb keinen Eintrag, läuft aber durch dieselbe Funktion
|
||
export function reiterEintraege(modul, reiterListe) // [{ key, label, … }] → [{ key, label, kurz? }]; kurz nur mit Eintrag in KURZLABELS, andere Felder fallen weg
|
||
export function meldeReiter(el, { eintraege, aktiv = '', bei = () => {} }) // sendet kn:reiter aus el; ohne CustomEvent oder ohne el.dispatchEvent: false, sonst true
|
||
```
|
||
- Produces `test/support/modulAbo.js` (alle späteren Tasks nutzen es):
|
||
```js
|
||
export const mitDom // (fn) => async () => { fx = installFakeDom(); setzt globalThis.CustomEvent auf eine fakeDom-taugliche Klasse (type, detail, bubbles, target, ergebnisse, preventDefault, stopPropagation); try { await fn(fx); await fx.ruhe() } finally { fx.restore(); CustomEvent zurück } }
|
||
export function hoereAufKn(fx) // { reiter: [detail …] } – lauscht am document.body auf kn:reiter
|
||
export function routen(fx, tabelle) // ersetzt fetch; Schlüssel 'METHODE /pfad?query'; Wert: { status?, body } | Array (der Reihe nach, letzter gilt weiter) | (aufruf) => Wert; unbekannt: 404 und Eintrag in fx.unbekannt; jeder Aufruf in fx.fetchLog
|
||
export function einschaltenAttrappe() // { fn, aufrufe: [{ wurzel, key, projectId, viewAs, onFertig }], fertig(i = 0, ergebnis = {}) → ruft aufrufe[i].onFertig }; fn zeichnet nichts
|
||
export const KEIN_PREIS // /€|einmalig|Preis|Zahlungspflichtig|Bestellseite|Behalten/
|
||
export function htmlWurzel(stageSelektor) // für innerHTML-Module: { root, stage, kn }; root.dataset ist {}; root.querySelector(stageSelektor) liefert stage, root.querySelector('.an') die Attrappe `an` (addEventListener merkt die Handler in an.handlers, querySelector → null, querySelectorAll → []), sonst null; stage.els['#id'] erzeugt Attrappen { innerHTML, disabled, checked, dataset, handlers, textContent }; stage.setAttribute/getAttribute speichern in stage.attribute; root.dispatchEvent(ev) legt ev.detail der kn:reiter-Ereignisse in kn.reiter ab und gibt true zurück (die Analyse meldet ihre Reiter an root)
|
||
export async function ladeFunktionenModul() // public/funktionen.js laden, alle statischen Importe auf die Dateien in public/ umbiegen (Muster funktionenZeitenTile.test.js:12-18)
|
||
```
|
||
- Verhalten der drei Module bei `modell === 'abo'`: keine eigene Leiste (kein `role="tablist"`), das Panel trägt `aria-label` statt `aria-labelledby`; jeder Reiterwechsel meldet `meldeReiter(root, { eintraege: reiterEintraege(modul, REITER), aktiv: key, bei: waehleReiter })`; jedes Zeichnen ohne Reiter (Fragebogen, Info-, Anfrage- und Statuskarten, Vorlagenwahl) meldet die leere Liste vor dem Zeichnen. Termine und Buchungen lesen dazu `modell` in `s.modell` (Shop: `s.funktionen.modell`). Ohne `modell` ändert sich nichts. Die Analyse (Task 8) meldet ebenso aus der Closure `mountCockpit` in `analyse.js`, mit denselben Hilfen und Regeln (leere Liste vor jedem Zeichnen ohne Reiter, `bei` = `pickTab`); Verdrahtung und Test stehen dort, weil sie `modell` erst dort liest und mit `htmlWurzel` arbeitet.
|
||
|
||
- [ ] **Step 1: Voraussetzungen prüfen**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git tag --list 'kn-p*' && git status --short && free -m && ls public/neu`
|
||
Expected: Tags `kn-p0` bis `kn-p3`, `git status` leer, „available“ mindestens 2000 MB, `public/neu` enthält `dashboard.html`, `reiter.js`, `etage.js`, `funktion-einschalten.js` und den Stub `module.css`. Fehlt etwas davon: stoppen und dem Controller melden.
|
||
|
||
- [ ] **Step 2: Failing test `test/kundenansichtNeuModulReiter.test.js`**
|
||
|
||
```js
|
||
test('KURZLABELS: genau diese Einträge (C2)', () => {
|
||
assert.deepEqual(KURZLABELS, { termine: { team: 'Team' }, shop: { uebersicht: 'Start', rechtliches: 'Recht' }, buchungen: { unterkuenfte: 'Preise' } })
|
||
})
|
||
test('reiterEintraege: Kurzlabel nur mit Eintrag, fremde Felder fallen weg', () => {
|
||
const liste = [{ key: 'termine', label: 'Termine', render() {} }, { key: 'team', label: 'Team & Plätze', render() {} }]
|
||
assert.deepEqual(reiterEintraege('termine', liste), [{ key: 'termine', label: 'Termine' }, { key: 'team', label: 'Team & Plätze', kurz: 'Team' }])
|
||
assert.deepEqual(reiterEintraege('unbekannt', liste.slice(1)), [{ key: 'team', label: 'Team & Plätze' }])
|
||
assert.deepEqual(reiterEintraege('analyse', [{ key: 'heatmap', label: 'Heatmap' }]), [{ key: 'heatmap', label: 'Heatmap' }], 'Analyse: kein Kurzlabel nötig')
|
||
})
|
||
test('meldeReiter: das Ereignis steigt auf und trägt Einträge, aktiv und bei; ohne Element tut es nichts', mitDom(async (fx) => {
|
||
const k = hoereAufKn(fx); const el = h('div'); document.body.append(el); const bei = () => {}
|
||
assert.equal(meldeReiter(el, { eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }), true)
|
||
assert.deepEqual(k.reiter, [{ eintraege: [{ key: 'a', label: 'A' }], aktiv: 'a', bei }])
|
||
assert.equal(meldeReiter(null, { eintraege: [] }), false)
|
||
}))
|
||
```
|
||
Dazu eine Tabelle `FAELLE` (je Modul `init`, optional `vorher` = `vergissAuswahl`, `tabelle(modell)` für den Zustand `active`, erwartete Labels `alt` und Kurzlabels `kurz`) und zwei Tests je Modul. Fixtures: ein Projekt `p1` mit `features.<schlüssel>.state = 'active'`, `modell` nur im Körper, wenn gesetzt.
|
||
- `termine`: `GET /api/funktionen` mit `termine: { verfuegbar: true }`, `GET /api/termine/einstellungen?projectId=p1` mit `{ state: 'active', einstellung: {}, fehlt: [] }`; `alt` `['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen']`, `kurz` `['Termine', 'Leistungen', 'Team', 'Einstellungen']`
|
||
- `shop`: `shop: { verfuegbar: true }`, `features: { shop: {} }`, `GET /api/shop/status?projectId=p1` mit `{ verfuegbar: true, state: 'active', anfrage: null }`; `alt` `['Übersicht', 'Artikel', 'Bestellungen', 'Einstellungen', 'Rechtliches']`, `kurz` `['Start', 'Artikel', 'Bestellungen', 'Einstellungen', 'Recht']`
|
||
- `buchungen`: `unterkunft: { verfuegbar: true }`, `GET /api/unterkunft/status?projectId=p1` mit `{ state: 'active' }`; `alt` `['Anfragen', 'Kalender', 'Unterkünfte & Preise', 'Rechnungen', 'Einstellungen']`, `kurz` `['Anfragen', 'Kalender', 'Preise', 'Rechnungen', 'Einstellungen']`
|
||
|
||
```js
|
||
for (const f of FAELLE) {
|
||
test(`${f.name}, Abo: keine eigene Leiste, kn:reiter mit Kurzlabels, bei() wechselt`, mitDom(async (fx) => {
|
||
const k = hoereAufKn(fx); const root = await oeffne(fx, f, 'abo')
|
||
assert.equal(fx.finde(root, '[role="tablist"]'), null)
|
||
const erst = k.reiter.at(-1)
|
||
assert.deepEqual(erst.eintraege.map((e) => e.label), f.alt)
|
||
assert.deepEqual(erst.eintraege.map((e) => e.kurz ?? e.label), f.kurz)
|
||
const andere = erst.eintraege.find((e) => e.key !== erst.aktiv).key
|
||
erst.bei(andere); await fx.ruhe()
|
||
assert.equal(k.reiter.at(-1).aktiv, andere)
|
||
}))
|
||
test(`${f.name}, ohne modell (fehlt, „Abo“): eigene Leiste mit langen Labels, kein Ereignis`, mitDom(async (fx) => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const k = hoereAufKn(fx); const root = await oeffne(fx, f, modell)
|
||
assert.deepEqual(fx.alle(root, '[role="tab"]').map((t) => fx.text(t)), f.alt)
|
||
assert.equal(k.reiter.length, 0)
|
||
}
|
||
}))
|
||
}
|
||
```
|
||
`oeffne(fx, f, modell)` ruft `f.vorher?.()`, `routen(fx, f.tabelle(modell))`, hängt eine Wurzel an den Body, ruft `f.init(root, { projectId: 'p1' })` und `fx.ruhe()`. Zwei weitere Tests: (a) `REITER.map((r) => r.label)` von `shop.js` und `buchungen.js` bleiben unverändert (die Pins `buchungenUi.test.js:53`, `shopView.test.js:373`); (b) Shop im Abo-Modell im Zustand `off` (`features.shop.state` und `status.state` `'off'`, `kundenart: 'unternehmen'`) meldet zuletzt die leere Liste: `assert.deepEqual(k.reiter.at(-1).eintraege, [])`.
|
||
|
||
- [ ] **Step 3: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulReiter.test.js`
|
||
Expected: FAIL mit „Cannot find module“ (`fn-reiter.js` und `modulAbo.js` fehlen).
|
||
|
||
- [ ] **Step 4: Implement `public/fn-reiter.js` und `test/support/modulAbo.js` nach den Signaturen oben**
|
||
|
||
`routen` folgt dem Helfer in `termineFragebogenView.test.js:3115-3127`. `htmlWurzel` und `ladeFunktionenModul` folgen `funktionenTiles.test.js:84-104` und `funktionenZeitenTile.test.js:12-18`; die Attrappen `an`, `setAttribute` und `dispatchEvent` für die Analyse erweitern `fakeEl` und `fakeStage` aus `funktionenTiles.test.js:405-415`. `meldeReiter` baut `new CustomEvent('kn:reiter', { bubbles: true, detail })`.
|
||
|
||
- [ ] **Step 5: Reiter in den drei Modulen verdrahten**
|
||
|
||
In `termine.js`, `shop.js`, `buchungen.js`: neue Importzeile `import { meldeReiter, reiterEintraege } from './fn-reiter.js'`. Die Literale von `REITER` bleiben unverändert (Pin `termineView.test.js:364`). Verhalten wie im Abschnitt „Verhalten der drei Module“; im Abo-Modell werden Knöpfe und Leiste nicht gebaut (in `termine.js` lässt der vorhandene `filter(Boolean)` eine fehlende Leiste weg, in `shop.js` und `buchungen.js` die Leiste nicht in `replaceChildren` aufnehmen).
|
||
|
||
- [ ] **Step 6: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulReiter.test.js test/termineView.test.js test/shopView.test.js test/buchungenUi.test.js`
|
||
Expected: PASS (neue und bestehende Tests).
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/fn-reiter.js test/support/modulAbo.js test/kundenansichtNeuModulReiter.test.js public/termine.js public/shop.js public/buchungen.js && git commit -m "Module: im Abo-Modell Reiter mit Kurzlabels an die Etage melden (C2)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 2: Einschalt-Seite im Abo-Modell (`funktion-bestellen.js`)
|
||
|
||
**Files:**
|
||
- Modify: `public/funktion-bestellen.js` (neue Funktion nach `:123`; Gruppen- und Offen-Block aus `baueSeite` `:146-170` in zwei Hilfsfunktionen derselben Datei ziehen; `statusKarte` `:306`, `:315`)
|
||
- Test: `test/kundenansichtNeuModulEinschalten.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `h`, `fehlerKarte` aus `./termine-dom.js`, `zusammenfassung` aus `./termine-view.js` (beide schon importiert); `einschalten` = `zeigeEinschalten(platz, { key, projectId, onFertig, … })` aus `public/neu/funktion-einschalten.js` (P3, VF1). P3 lädt die Liste selbst, zeichnet Bestätigung und Knopf und ruft `onFertig({ feature, ticketWoid })` nach 200 und `onFertig({ feature: null, ticketWoid: null })` nach 409 `bereits_eingeschaltet`; alle anderen Fehler zeigt P3 selbst.
|
||
- Produces:
|
||
```js
|
||
export function renderEinschaltSeite(root, { ctx = {}, daten = {}, zumSchritt, key, zusammenfassung: gruppenFunktion, einschalten, nachEinschalten } = {})
|
||
// → Element section.kb-panel.tm-bestellen.tm-einschalten[data-key]
|
||
// daten: wie bei renderBestellen (einstellung, fragebogen, branchen, offen, kundenansicht): Quelle von Zusammenfassung und offenen Punkten
|
||
// einschalten(platz, { key, projectId: ctx.projectId, viewAs, onFertig }) mit viewAs = daten.kundenansicht === true || ctx.kundenansicht === true
|
||
// onFertig ist eine Closure mit Merker: nur der erste Aufruf wirkt und nur, wenn ctx.aktuell?.() nicht false ist; sie ruft nachEinschalten() (die Ansicht zeichnet sich neu)
|
||
// statusKarte({ ..., modell }): modell === 'abo' → Faktenzeile „Eingeschaltet am“ (E5)
|
||
```
|
||
Aufbau: gibt es Gruppen (`gruppenFunktion(daten.einstellung, daten.fragebogen, daten.branchen)`, bei `key === 'termine'` ohne eigene Funktion die `zusammenfassung` aus `termine-view.js`) oder offene Punkte (`daten.offen`), steht oben `h3` E1, dann die Gruppen mit „Ändern“ und die offenen Punkte mit „Zum Schritt“. Bei offenen Punkten folgt der Hinweis E2 und `einschalten` wird nicht gerufen. Sonst steht der Platz `div.tm-einschalten-platz[data-einschalten=key]` da, in den `einschalten` genau einmal zeichnet; fehlt `einschalten`, steht dort die `fehlerKarte` mit E4.
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulEinschalten.test.js`**
|
||
|
||
```js
|
||
function seite(optionen) {
|
||
const wurzel = h('div'); document.body.append(wurzel)
|
||
renderEinschaltSeite(wurzel, { ctx: { projectId: 'p1' }, daten: { offen: [], kundenansicht: false }, key: 'shop', ...optionen })
|
||
return wurzel
|
||
}
|
||
const GRUPPEN = () => [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: ['Was du verkaufst: Kleidung'] }]
|
||
|
||
test('mountet den Ablauf genau einmal in den Platz, mit key, projectId und viewAs; ohne Haken, Preis und Überschrift', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe(); const wurzel = seite({ einschalten: a.fn })
|
||
assert.equal(a.aufrufe.length, 1)
|
||
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['shop', 'p1', false])
|
||
assert.ok(a.aufrufe[0].wurzel === fx.finde(wurzel, '[data-einschalten="shop"]'))
|
||
assert.equal(fx.finde(wurzel, 'h3'), null)
|
||
assert.equal(fx.alle(wurzel, 'input').length, 0)
|
||
assert.doesNotMatch(fx.text(wurzel), KEIN_PREIS)
|
||
}))
|
||
test('Zusammenfassung mit E1; offene Punkte sperren den Ablauf (E2); „Ändern“ und „Zum Schritt“ rufen zumSchritt', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe(); const schritte = []
|
||
const wurzel = seite({ einschalten: a.fn, zusammenfassung: GRUPPEN, zumSchritt: (k) => schritte.push(k), daten: { offen: [{ schritt: 'zeiten', text: 'Bitte wähle das Bundesland.' }], kundenansicht: false } })
|
||
assert.equal(fx.text(fx.finde(wurzel, 'h3')), 'Prüfen und einschalten')
|
||
assert.equal(a.aufrufe.length, 0)
|
||
assert.match(fx.text(wurzel), /Du kannst einschalten, sobald die offenen Punkte erledigt sind\./)
|
||
await fx.klick(fx.finde(wurzel, '[data-aktion="zum-schritt"][data-schritt="zeiten"]'))
|
||
await fx.klick(fx.finde(wurzel, '[data-aktion="aendern"][data-schritt="anfrage"]'))
|
||
assert.deepEqual(schritte, ['zeiten', 'anfrage'])
|
||
}))
|
||
test('Kundenansicht: der Ablauf bekommt viewAs true', mitDom(async () => {
|
||
const a = einschaltenAttrappe(); seite({ einschalten: a.fn, daten: { offen: [], kundenansicht: true } })
|
||
assert.equal(a.aufrufe[0].viewAs, true)
|
||
}))
|
||
test('onFertig: höchstens einmal, nicht nach ctx.aktuell() === false', mitDom(async (fx) => {
|
||
let n = 0
|
||
const a = einschaltenAttrappe(); seite({ einschalten: a.fn, nachEinschalten: () => { n += 1 } })
|
||
a.fertig(0); a.fertig(0); await fx.ruhe()
|
||
assert.equal(n, 1)
|
||
const b = einschaltenAttrappe(); seite({ einschalten: b.fn, ctx: { projectId: 'p1', aktuell: () => false }, nachEinschalten: () => { n += 1 } })
|
||
b.fertig(0); await fx.ruhe()
|
||
assert.equal(n, 1)
|
||
}))
|
||
test('ohne Funktion einschalten: Fehlerkarte E4 statt Absturz', mitDom(async (fx) => {
|
||
const wurzel = seite({})
|
||
assert.match(fx.text(wurzel), /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
|
||
assert.ok(fx.finde(wurzel, '[role="alert"]'))
|
||
}))
|
||
test('statusKarte: Abo-Modell „Eingeschaltet am“, sonst „Bestellt am“', mitDom(async (fx) => {
|
||
const args = { projekt: { projectId: 'p1' }, bestellung: { state: 'setup', orderedAt: '2026-10-16T09:00:00.000Z', ticketWoid: '100150' } }
|
||
assert.match(fx.text(statusKarte({ ...args, modell: 'abo' })), /Eingeschaltet am\s*16\.10\.2026/)
|
||
assert.match(fx.text(statusKarte(args)), /Bestellt am\s*16\.10\.2026/)
|
||
}))
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulEinschalten.test.js`
|
||
Expected: FAIL mit „renderEinschaltSeite is not a function“.
|
||
|
||
- [ ] **Step 3: Gruppen- und Offen-Block herausziehen (Refactor ohne Änderung der Ausgabe)**
|
||
|
||
In `baueSeite` die Erzeugung von `gruppen` (`:150-159`) und `offenBlock` (`:161-170`) in zwei Funktionen derselben Datei verschieben, die `baueSeite` und `renderEinschaltSeite` benutzen. Danach sofort: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/termineFragebogenView.test.js test/funktionBestellenKey.test.js` Expected: PASS wie vor dem Schritt.
|
||
|
||
- [ ] **Step 4: Implement `renderEinschaltSeite` und `statusKarte({ modell })`**
|
||
|
||
Keine neuen Importe, kein `href`, keine Wörter aus der Pin-Liste (`details`, `summary`, „zahlungspflichtig“).
|
||
|
||
- [ ] **Step 5: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulEinschalten.test.js test/termineFragebogenView.test.js test/funktionBestellenKey.test.js`
|
||
Expected: PASS (neue und bestehende Tests).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/funktion-bestellen.js test/kundenansichtNeuModulEinschalten.test.js && git commit -m "Funktionsmodule: gemeinsame Einschalt-Seite für das Abo-Modell" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 3: Terminplaner im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/termine.js` (`initTermine` `:49-56`, `baueCtx` `:322-350`, `statusKarte`-Aufruf `:245-252`), `public/termine-fragebogen.js` (Import `:24`, `TITEL` `:27`, `KUNDENANSICHT` `:29`, `:80`, `:90`, `:98`, `bestellseite` `:101-104`)
|
||
- Test: `test/kundenansichtNeuModulTermine.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `renderEinschaltSeite` (Task 2), `s.modell` und `meldeReiter` (Task 1), Testhilfen; V5-Felder `modell`, `kundenart`.
|
||
- Produces: `initTermine(root, { projectId, einschalten } = {})`; `s.einschalten` in der Sitzung; `baueCtx` liefert zusätzlich `einschalten: s.einschalten` und `fertig: () => ladeNeu(s)`; der Fragebogen liest `ctx.s?.modell === 'abo'`.
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulTermine.test.js`**
|
||
|
||
Kopf: `STAND = bestellbarerStand({ now: new Date('2026-10-16T08:00:00.000Z') })` aus `test/support/fragebogenStand.js` (alle Schritte bestätigt, der Start ist also der letzte Schritt); `daten(extra)` = `{ state: 'off', einstellung: STAND.einstellung, fragebogen: STAND.fragebogen, version: 1, branchen: [], kundenansicht: false, abo: { ok: true }, offen: [], bestellung: null, ...extra }`; `liste(modell, state = 'off', feature = {})` = Antwort von `GET /api/funktionen` mit einem Projekt `p1` (`features.termine = { state, siteKey: 'abcdefghijklmnop', ...feature }`), `termine: { verfuegbar: true, bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist in 1–2 Wochen', hinweis: null }` und `modell` nur, wenn gesetzt; `BESTELLUNG` = `{ preis: '149 €', preisHinweis: 'einmalig, ohne Umsatzsteuer gemäß § 19 UStG', einrichtung: '', belehrung: null, dokumente: [], consent: { version: 'v-test', knopf: 'Zahlungspflichtig bestellen · 149 €', items: [{ id: 'beginn', text: 'Ich verlange den sofortigen Beginn.', haken: true }] } }`; `EINSTELLUNGEN` = `'GET /api/termine/einstellungen?projectId=p1'`; `oeffne(fx, tabelle, optionen)` hängt eine Wurzel an den Body, ruft `routen`, `initTermine(wurzel, { projectId: 'p1', ...optionen })` und `fx.ruhe()`.
|
||
|
||
```js
|
||
test('Abo, alles bestätigt: letzter Schritt E1 mountet den Ablauf, ohne Haken, Preisblock und Checkout; keine Reiter', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe(); const k = hoereAufKn(fx)
|
||
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten() } }, { einschalten: a.fn })
|
||
assert.equal(fx.text(fx.finde(wurzel, '.tm-fb-bestellen h3')), 'Prüfen und einschalten')
|
||
assert.match(fx.text(fx.finde(wurzel, '.tm-schritt[aria-current="step"]')), /Prüfen und einschalten/)
|
||
assert.deepEqual(a.aufrufe.map((x) => [x.key, x.projectId, x.viewAs]), [['termine', 'p1', false]])
|
||
assert.equal(fx.alle(wurzel, 'input[type="checkbox"]').length, 0, 'auch kein Haken „gesundheit“')
|
||
for (const sel of ['[data-aktion="bestellen"]', '.tm-bestellen-preis', '.tm-rechtstext', '.tm-bestellen-zustimmung']) assert.equal(fx.finde(wurzel, sel), null, sel)
|
||
assert.equal(fx.fetchLog.filter((x) => x.method === 'POST').length, 0)
|
||
assert.deepEqual(k.reiter.at(-1).eintraege, [])
|
||
}))
|
||
test('Abo, offene Punkte: kein Ablauf, Hinweis E2, „Zum Schritt“ führt zurück', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe()
|
||
const offen = [{ schritt: 'zeiten', text: 'Bitte wähle das Bundesland deines Betriebs.' }]
|
||
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten({ offen }) } }, { einschalten: a.fn })
|
||
assert.equal(a.aufrufe.length, 0)
|
||
assert.match(fx.text(wurzel), /Du kannst einschalten, sobald die offenen Punkte erledigt sind\./)
|
||
await fx.klick(fx.finde(wurzel, '[data-aktion="zum-schritt"][data-schritt="zeiten"]'))
|
||
assert.equal(fx.finde(wurzel, '.tm-schritt[aria-current="step"]').getAttribute('data-schritt'), 'zeiten')
|
||
}))
|
||
test('Abo: nach dem Einschalten lädt die Ansicht neu und zeigt die Statuskarte „In Einrichtung“ mit „Eingeschaltet am“', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe(); let n = 0
|
||
const wurzel = await oeffne(fx, {
|
||
'GET /api/funktionen': () => (n === 0 ? liste('abo') : liste('abo', 'setup', { orderedAt: '2026-10-16T09:00:00.000Z', ticketWoid: '100150' })),
|
||
[EINSTELLUNGEN]: () => ({ body: n === 0 ? daten() : daten({ state: 'setup' }) }),
|
||
}, { einschalten: a.fn })
|
||
n = 1; a.fertig(0); await fx.ruhe()
|
||
const karte = fx.finde(wurzel, '.tm-statuskarte[data-state="setup"]')
|
||
assert.match(fx.text(karte), /Eingeschaltet am\s*16\.10\.2026/)
|
||
assert.doesNotMatch(fx.text(karte), /Bestellt am|Zahlung/)
|
||
}))
|
||
test('Abo: ohne Funktion einschalten steht E4; in der Kundenansicht steht F1 und der Ablauf bekommt viewAs true', mitDom(async (fx) => {
|
||
const ohne = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten() } })
|
||
assert.match(fx.text(ohne), /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
|
||
const a = einschaltenAttrappe()
|
||
const ansicht = await oeffne(fx, { 'GET /api/funktionen': liste('abo'), [EINSTELLUNGEN]: { body: daten({ kundenansicht: true }) } }, { einschalten: a.fn })
|
||
assert.match(fx.text(ansicht), /Du siehst den Fragebogen in der Kundenansicht\. Angaben kannst du ändern, Schritte bestätigen und einschalten kann nur der Kunde selbst\./)
|
||
assert.equal(a.aufrufe[0].viewAs, true)
|
||
}))
|
||
test('Ohne modell (fehlt, leer, „Abo“, „abo “): unverändert Bestellseite mit Knopf des Registers, Ablauf wird nie gerufen', mitDom(async (fx) => {
|
||
for (const modell of [undefined, '', 'Abo', 'abo ']) {
|
||
const a = einschaltenAttrappe()
|
||
const wurzel = await oeffne(fx, { 'GET /api/funktionen': liste(modell), [EINSTELLUNGEN]: { body: daten({ bestellung: BESTELLUNG }) } }, { einschalten: a.fn })
|
||
assert.equal(a.aufrufe.length, 0, JSON.stringify(modell))
|
||
assert.equal(fx.text(fx.finde(wurzel, '[data-aktion="bestellen"]')), 'Zahlungspflichtig bestellen · 149 €')
|
||
assert.equal(fx.text(fx.finde(wurzel, '.tm-fb-bestellen h3')), 'Prüfen und bestellen')
|
||
}
|
||
}))
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulTermine.test.js`
|
||
Expected: FAIL (der letzte Schritt zeigt die Bestellseite, `a.aufrufe` ist leer).
|
||
|
||
- [ ] **Step 3: Implement `termine.js`**
|
||
|
||
`initTermine(root, { projectId, einschalten } = {})`; `einschalten` in `s.einschalten`; `baueCtx` ergänzt `einschalten` und `fertig: () => ladeNeu(s)`; der `statusKarte`-Aufruf übergibt `modell: s.modell`. Kein neues `location.hash`, `replaceState` oder `api: {`.
|
||
|
||
- [ ] **Step 4: Implement `termine-fragebogen.js`**
|
||
|
||
Import `{ renderBestellen, renderEinschaltSeite }` aus `./funktion-bestellen.js` (nur die benannten Importe erweitern). `bestellseite()` ruft bei `ctx.s?.modell === 'abo'` `renderEinschaltSeite(seite, { ctx, daten: ctx.daten, zumSchritt: (key) => zeige(key), key: 'termine', einschalten: ctx.einschalten, nachEinschalten: () => ctx.fertig() })`, sonst wie bisher `renderBestellen`. Titel der Leiste und des Zählers (`:90`, `:98`) und der Hinweis `:80` folgen E1 und F1 über zwei neue Konstanten `TITEL_ABO` und `KUNDENANSICHT_ABO`; die Map `TITEL` bleibt.
|
||
|
||
- [ ] **Step 5: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulTermine.test.js test/termineView.test.js test/termineFragebogenView.test.js`
|
||
Expected: PASS (neue und bestehende Tests).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/termine.js public/termine-fragebogen.js test/kundenansichtNeuModulTermine.test.js && git commit -m "Terminplaner: im Abo-Modell Einschalten statt Bestellen, ohne Häkchen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 4: News-Pinnwand im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/funktionen.js` (`initNews` `:474-512`, `renderBooking` `:515`)
|
||
- Test: `test/kundenansichtNeuModulNews.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `htmlWurzel('#news-stage')`, `ladeFunktionenModul`, `einschaltenAttrappe`, `KEIN_PREIS` (Task 1).
|
||
- Produces: `initNews(root, { projectId, einschalten } = {})`; im Zustand `off` und `modell === 'abo'` besteht die Seite nur aus dem Platz `div#news-einschalten`, in den `einschalten(platz, { key: 'news', projectId, viewAs: false, onFertig })` zeichnet (die Überschrift „News-Pinnwand einschalten“ kommt von P3); `onFertig` lädt `initNews(root, { einschalten })` einmal neu; `root.dataset.wunschProjekt` wählt das Projekt und wird entfernt. Keine neuen Importe (Pins `termineView.test.js:315-320`).
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulNews.test.js`**
|
||
|
||
Kopf:
|
||
|
||
```js
|
||
const { initNews } = await ladeFunktionenModul()
|
||
globalThis.window = { location: { hash: '#news', pathname: '/dashboard.html', search: '', href: '' }, history: { replaceState() {} } }
|
||
after(() => { delete globalThis.window })
|
||
const projekt = (id, name) => ({ projectId: id, projectName: name, features: { news: { state: 'off' } } })
|
||
async function lauf({ modell, projects = [projekt('p1', 'Muster')], dataset = {}, projectId = 'p1', einschalten } = {}) {
|
||
const calls = []
|
||
globalThis.fetch = async (url) => { calls.push(String(url)); return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects, features: { news: { priceLabel: '30 €' } }, consentText: 'Alt', setupEstimate: 'meist in 1–2 Wochen', ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG' }) } }
|
||
const { root, stage } = htmlWurzel('#news-stage')
|
||
Object.assign(root.dataset, dataset)
|
||
await initNews(root, { projectId, einschalten })
|
||
return { root, stage, calls }
|
||
}
|
||
|
||
test('Abo: Seite ohne Preis, Haken und Checkout; der Ablauf bekommt den Platz mit key news', async () => {
|
||
const a = einschaltenAttrappe(); const { stage } = await lauf({ modell: 'abo', einschalten: a.fn })
|
||
assert.doesNotMatch(stage.innerHTML, KEIN_PREIS)
|
||
assert.doesNotMatch(stage.innerHTML, /news-consent|news-order|30 €/)
|
||
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId, a.aufrufe[0].viewAs], ['news', 'p1', false])
|
||
assert.ok(a.aufrufe[0].wurzel === stage.els['#news-einschalten'])
|
||
})
|
||
test('Abo: nach onFertig lädt die Pinnwand neu', async () => {
|
||
const a = einschaltenAttrappe(); const { calls } = await lauf({ modell: 'abo', einschalten: a.fn })
|
||
a.fertig(0); await new Promise((r) => setImmediate(r))
|
||
assert.equal(calls.filter((u) => u === '/api/funktionen').length, 2)
|
||
})
|
||
test('Abo ohne Funktion einschalten: Satz E4 im Platz', async () => {
|
||
const { stage } = await lauf({ modell: 'abo' })
|
||
assert.match(stage.els['#news-einschalten'].textContent, /Das Einschalten ist gerade nicht verfügbar\. Bitte lade die Seite neu oder schreib uns im Chat\./)
|
||
})
|
||
test('Ohne modell (fehlt, „Abo“): unverändert Preis, Haken und Bestellknopf; der Ablauf wird nie gerufen', async () => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const a = einschaltenAttrappe(); const { stage } = await lauf({ modell, einschalten: a.fn })
|
||
assert.match(stage.innerHTML, /<strong>30 €<\/strong>/)
|
||
assert.match(stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 30 €</)
|
||
assert.equal(a.aufrufe.length, 0)
|
||
}
|
||
})
|
||
test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => {
|
||
const a = einschaltenAttrappe()
|
||
const { root } = await lauf({ modell: 'abo', projects: [projekt('p1', 'Muster'), projekt('p2', 'Muster 2')], dataset: { wunschProjekt: 'p2' }, projectId: '', einschalten: a.fn })
|
||
assert.equal(a.aufrufe[0].projectId, 'p2')
|
||
assert.equal(root.dataset.wunschProjekt, undefined)
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulNews.test.js`
|
||
Expected: FAIL (die Seite zeigt „30 €“ und `news-order`).
|
||
|
||
- [ ] **Step 3: Implement in `funktionen.js`**
|
||
|
||
`initNews(root, { projectId, einschalten } = {})`: Wunsch = `projectId || root.dataset?.wunschProjekt || ''`, Attribut entfernen (`root.dataset` kann in Attrappen fehlen), `selection.projectId` setzen. `modell` aus der Antwort mitlesen. Bei `modell === 'abo'` und Zustand `off` statt `renderBooking` eine neue Funktion `renderBookingAbo(stage, project, { einschalten, neuLaden })`: `stage.innerHTML = '<div id="news-einschalten"></div>'`; ist `einschalten` eine Funktion, wird sie mit dem Platz und `{ key: 'news', projectId, viewAs: false, onFertig }` aufgerufen (`onFertig` ruft `neuLaden()` einmal; `neuLaden` ist `() => initNews(root, { einschalten })`), sonst steht E4 im Platz (`textContent`). Kein Import, kein `import(`.
|
||
|
||
- [ ] **Step 4: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulNews.test.js test/funktionenTiles.test.js test/funktionenZeitenTile.test.js test/funktionenRecht.test.js test/shopWiring.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/funktionen.js test/kundenansichtNeuModulNews.test.js && git commit -m "News: im Abo-Modell Einschalten statt Bestellen, Projektwahl über dataset" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 5: Webshop im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/shop.js` (`initShop` `:64`, `starte` `:69-74`, `zeigeAnfrage` `:195`, `zeigeBestellseite` `:253`, Import `:18`), `public/shop-anfrage.js` (`renderAnfrageFormular` `:37`, `:53-54`, `:67-68`, `:91-92`, `:115-120`)
|
||
- Test: `test/kundenansichtNeuModulShop.test.js`; Pin `test/shopWiring.test.js:31`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `renderEinschaltSeite` (Task 2), `meldeReiter` (Task 1), Testhilfen; V5-Felder `modell`, `kundenart` der Antwort von `GET /api/funktionen`.
|
||
- Produces: `initShop(root, { projectId, einschalten } = {})`; `renderAnfrageFormular(root, { ..., modell })`. Im Abo-Modell sendet der Körper `anfrage` ohne `unternehmer` und mit `keineAusgeschlossenen: true`. Kundenart: nur `'unternehmen'` darf anfragen, alles andere (`'privat'`, leer, fehlend) gilt als privat (E4).
|
||
|
||
- [ ] **Step 1: Pin anpassen und failing test**
|
||
|
||
In `test/shopWiring.test.js:31` die Regex auf `/export function initShop\(root, \{ projectId, einschalten \} = \{\}\)/` ändern. Test `test/kundenansichtNeuModulShop.test.js`, Kopf:
|
||
|
||
```js
|
||
const ANFRAGE = { warenart: 'kleidung', beschreibung: 'Handgemachte Mützen und Schals.', artikelzahl: 'bis20', versand: true, abholung: false, unternehmer: true, keineAusgeschlossenen: true, nachricht: '' }
|
||
const funk = (state, kopf = {}) => ({ projects: [{ projectId: 'p1', projectName: 'Mein Laden', features: { shop: { state } } }], features: { shop: { priceLabel: '299 €', consent: { version: 'v', knopf: 'Zahlungspflichtig bestellen · 299 €', items: [] } } }, ustHinweis: 'einmalig, ohne Umsatzsteuer gemäß § 19 UStG', legal: { shop: {} }, shop: { verfuegbar: true }, ...kopf })
|
||
async function oeffne(fx, { kopf = {}, state = 'off', anfrage = null, einschalten } = {}) {
|
||
vergissAuswahl()
|
||
routen(fx, { 'GET /api/funktionen': { body: funk(state, kopf) }, 'GET /api/shop/status?projectId=p1': { body: { verfuegbar: true, state, ticketWoid: null, anfrage } }, 'POST /api/shop/anfrage': { body: { anfrage: {} } } })
|
||
const root = h('div'); document.body.append(root)
|
||
await initShop(root, { projectId: 'p1', einschalten })
|
||
await fx.ruhe()
|
||
return root
|
||
}
|
||
const ZUGESAGT = { status: 'angenommen', anfrage: ANFRAGE, at: '2026-10-05T10:30:00.000Z' }
|
||
```
|
||
|
||
```js
|
||
test('Abo, Betrieb: Anfrage ohne Unternehmer-Haken, Satz S2 über dem Knopf, kein Preis; Körper ohne unternehmer', mitDom(async (fx) => {
|
||
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'unternehmen' } })
|
||
assert.equal(fx.finde(root, '[data-feld="unternehmer"]'), null)
|
||
assert.equal(fx.finde(root, '[data-feld="keineAusgeschlossenen"]'), null)
|
||
const satz = fx.finde(root, '[data-satz="ausgeschlossen"]')
|
||
assert.equal(fx.text(satz), `Mit „Anfrage senden“ bestätigst du, dass du keine ausgeschlossenen Waren verkaufst (${AUSGESCHLOSSEN}).`)
|
||
const knoten = fx.alle(root, '*')
|
||
assert.ok(knoten.indexOf(satz) < knoten.indexOf(fx.finde(root, '[data-aktion="anfrage-senden"]')), 'Satz steht über dem Knopf')
|
||
assert.match(fx.text(fx.finde(root, '.sh-erklaerung')), /Im Wartungs-Abo enthalten\./)
|
||
assert.doesNotMatch(fx.text(fx.finde(root, '.sh-erklaerung')), KEIN_PREIS)
|
||
await fx.setze(fx.finde(root, '[data-feld="warenart"]'), 'kleidung')
|
||
await fx.setze(fx.finde(root, '[data-feld="beschreibung"]'), 'Handgemachte Mützen und Schals.')
|
||
await fx.setze(fx.finde(root, '[data-feld="artikelzahl"]'), 'bis20')
|
||
await fx.klick(fx.finde(root, '[data-feld="versand"]'))
|
||
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
|
||
assert.deepEqual(fx.fetchLog.find((a) => a.method === 'POST').body, { projectId: 'p1', anfrage: { warenart: 'kleidung', beschreibung: 'Handgemachte Mützen und Schals.', artikelzahl: 'bis20', versand: true, abholung: false, keineAusgeschlossenen: true, nachricht: '' } })
|
||
}))
|
||
test('Abo, privat oder Kundenart leer/fehlend: Hinweis S3 mit Chat, nie das Formular, kein POST, leere Reiterliste', mitDom(async (fx) => {
|
||
const k = hoereAufKn(fx)
|
||
for (const kundenart of ['privat', '', undefined]) {
|
||
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart } })
|
||
assert.equal(fx.finde(root, 'form'), null, String(kundenart))
|
||
assert.match(fx.text(root), /Der Webshop ist für Betriebe/)
|
||
assert.match(fx.text(root), /Für Privatpersonen gibt es den Webshop leider nicht\. Stimmt das nicht\? Schreib uns im Chat\./)
|
||
assert.ok(fx.finde(root, 'button[data-view="chat"]'))
|
||
}
|
||
assert.equal(fx.fetchLog.filter((a) => a.method === 'POST').length, 0)
|
||
assert.deepEqual(k.reiter.at(-1).eintraege, [])
|
||
}))
|
||
test('Abo, privat mit bestehendem Shop (active): die fünf Reiter bleiben', mitDom(async (fx) => {
|
||
const k = hoereAufKn(fx)
|
||
await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'privat' }, state: 'active' })
|
||
assert.equal(k.reiter.at(-1).eintraege.length, 5)
|
||
}))
|
||
test('Abo, Anfrage angenommen: Gruppe B1 und Ablauf key shop statt Bestellseite; onFertig lädt neu', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe()
|
||
const root = await oeffne(fx, { kopf: { modell: 'abo', kundenart: 'unternehmen' }, anfrage: ZUGESAGT, einschalten: a.fn })
|
||
assert.equal(a.aufrufe[0].key, 'shop')
|
||
assert.equal(fx.text(fx.finde(root, '.tm-bestellen-gruppe h4')), 'Deine Anfrage')
|
||
assert.equal(fx.finde(root, '[data-aktion="bestellen"]'), null)
|
||
a.fertig(0); await fx.ruhe()
|
||
assert.equal(fx.fetchLog.filter((x) => x.url === '/api/shop/status?projectId=p1').length, 2)
|
||
}))
|
||
test('Ohne modell (fehlt, „Abo“): Formular mit Unternehmer-Haken und 299 €, nach der Zusage die Bestellseite, der Ablauf wird nie gerufen', mitDom(async (fx) => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const a = einschaltenAttrappe(); const kopf = modell ? { modell } : {}
|
||
const formular = await oeffne(fx, { kopf, einschalten: a.fn })
|
||
assert.ok(fx.finde(formular, '[data-feld="unternehmer"]'))
|
||
assert.match(fx.text(formular), /299 € einmalig/)
|
||
const zugesagt = await oeffne(fx, { kopf, anfrage: ZUGESAGT, einschalten: a.fn })
|
||
assert.ok(fx.finde(zugesagt, '[data-aktion="bestellen"]'))
|
||
assert.equal(a.aufrufe.length, 0)
|
||
}
|
||
}))
|
||
```
|
||
(`AUSGESCHLOSSEN` kommt aus `../public/shop-anfrage.js`.)
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulShop.test.js`
|
||
Expected: FAIL (`[data-feld="unternehmer"]` ist da).
|
||
|
||
- [ ] **Step 3: Implement `shop-anfrage.js`**
|
||
|
||
Neue Option `modell`. Bei `'abo'`: Stichpunkte S1 statt Preiszeile, kein `ustHinweis`-Absatz, kein Feld `unternehmer`, statt des Haken-Feldes `keineAusgeschlossenen` ein `p.sh-hinweis[data-satz="ausgeschlossen"]` mit S2 direkt vor dem Absende-Knopf; der Körper lässt `unternehmer` weg und sendet `keineAusgeschlossenen: true`. Ohne `modell` bleibt alles wie bisher.
|
||
|
||
- [ ] **Step 4: Implement `shop.js`**
|
||
|
||
`initShop(root, { projectId, einschalten } = {})` reicht `einschalten` an `starte` (auch im Wiederholen-Rückruf) und legt es in `s.einschalten`. `zeigeAnfrage`: bei `s.funktionen?.modell === 'abo'` und `s.funktionen.kundenart !== 'unternehmen'` die `infoKarte` mit S3 und Chat statt Formular. Das Formular bekommt `modell: s.funktionen?.modell`. `zeigeBestellseite`: im Abo-Modell `renderEinschaltSeite(wurzel, { ctx, daten, key: 'shop', zusammenfassung: zusammenfassungVon(daten.anfrage), einschalten: s.einschalten, nachEinschalten: () => ladeNeu(s) })` (der Gruppentitel B1 steht in `zusammenfassungVon` schon). Reiter wie in Task 1. Import `renderEinschaltSeite` zur bestehenden Importzeile von `./funktion-bestellen.js` hinzufügen.
|
||
|
||
- [ ] **Step 5: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulShop.test.js test/shopView.test.js test/shopWiring.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/shop.js public/shop-anfrage.js test/kundenansichtNeuModulShop.test.js test/shopWiring.test.js && git commit -m "Webshop: im Abo-Modell Anfrage ohne Unternehmer-Haken, Privat-Hinweis, Einschalten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 6: Buchungskalender im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/buchungen.js` (`initBuchungen` `:32`, `starte` `:39-41`, `zeigeAnfrage` `:124-130`, Import `:8`), `public/buchungen-anfrage.js` (`anfrageListe` `:20`, `renderAnfrageFormular` `:33`, `:94-97`)
|
||
- Test: `test/kundenansichtNeuModulBuchungen.test.js`; Pin `test/unterkunftWiring.test.js:130`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `renderEinschaltSeite` (Task 2), `s.modell` und `meldeReiter` (Task 1), Testhilfen.
|
||
- Produces: `initBuchungen(root, { projectId, einschalten } = {})`; `renderAnfrageFormular(root, { ..., modell })`; neuer Export `anfrageZeilen(a)` in `buchungen-anfrage.js` → `[{ name, wert }]` (Zeilen der Liste, wie `shop-anfrage.js`; `[]` ohne Objekt); `anfrageListe` nutzt sie.
|
||
|
||
- [ ] **Step 1: Pin anpassen und failing test**
|
||
|
||
`test/unterkunftWiring.test.js:130` auf `/export function initBuchungen\(root, \{ projectId, einschalten \} = \{\}\)/` ändern. Test `test/kundenansichtNeuModulBuchungen.test.js`, Kopf:
|
||
|
||
```js
|
||
const ANFRAGE = { art: 'ferienwohnung', einheiten: 2, portale: ['booking'], nachricht: '' }
|
||
const ZUGESAGT = { status: 'angenommen', anfrage: ANFRAGE, at: '2026-10-05T10:30:00.000Z' }
|
||
async function oeffne(fx, { kopf = {}, anfrage = null, einschalten } = {}) {
|
||
vergissAuswahl()
|
||
routen(fx, {
|
||
'GET /api/funktionen': { body: { projects: [{ projectId: 'p1', projectName: 'Haus am See', features: { unterkunft: { state: 'off' } } }], unterkunft: { verfuegbar: true }, ...kopf } },
|
||
'GET /api/unterkunft/status?projectId=p1': { body: { state: 'off', anfrage } },
|
||
'POST /api/unterkunft/anfrage-funktion?projectId=p1': { status: 201, body: { anfrage: {} } },
|
||
})
|
||
const root = h('div'); document.body.append(root)
|
||
await initBuchungen(root, { projectId: 'p1', einschalten })
|
||
await fx.ruhe()
|
||
return root
|
||
}
|
||
```
|
||
|
||
```js
|
||
test('Abo: Anfrageformular nennt S1, kein Preis, sendet den Körper unverändert', mitDom(async (fx) => {
|
||
const root = await oeffne(fx, { kopf: { modell: 'abo' } })
|
||
const text = fx.text(fx.finde(root, '.sh-erklaerung'))
|
||
assert.match(text, /Im Wartungs-Abo enthalten\./)
|
||
assert.match(text, /Erst nach unserer Zusage kannst du einschalten\./)
|
||
assert.doesNotMatch(text, KEIN_PREIS)
|
||
await fx.setze(fx.finde(root, '[data-feld="art"]'), 'ferienwohnung')
|
||
await fx.setze(fx.finde(root, '[data-feld="einheiten"]'), '2')
|
||
await fx.klick(fx.finde(root, '[data-feld="portale.booking"]'))
|
||
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
|
||
assert.deepEqual(fx.fetchLog.find((x) => x.method === 'POST').body, ANFRAGE)
|
||
}))
|
||
test('Abo, angenommen: Gruppe B1 und Ablauf key unterkunft; kein Haken, kein Wort „Rechnung“; onFertig lädt neu', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe()
|
||
const root = await oeffne(fx, { kopf: { modell: 'abo' }, anfrage: ZUGESAGT, einschalten: a.fn })
|
||
assert.equal(a.aufrufe[0].key, 'unterkunft')
|
||
assert.equal(fx.text(fx.finde(root, '.tm-bestellen-gruppe h4')), 'Deine Anfrage')
|
||
assert.match(fx.text(root), /Art der Unterkunft: Ferienwohnung oder Ferienhaus/)
|
||
assert.equal(fx.alle(root, 'input[type="checkbox"]').length, 0)
|
||
assert.doesNotMatch(fx.text(root), /Rechnung/)
|
||
a.fertig(0); await fx.ruhe()
|
||
assert.equal(fx.fetchLog.filter((x) => x.url === '/api/unterkunft/status?projectId=p1').length, 2)
|
||
}))
|
||
test('Ohne modell (fehlt, „Abo“): Formular mit „199 € einmalig“, angenommen zeigt „Wir melden uns bei dir“, der Ablauf wird nie gerufen', mitDom(async (fx) => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const a = einschaltenAttrappe(); const kopf = modell ? { modell } : {}
|
||
assert.match(fx.text(await oeffne(fx, { kopf, einschalten: a.fn })), /199 € einmalig für den Einbau in deine Website\./)
|
||
assert.match(fx.text(await oeffne(fx, { kopf, anfrage: ZUGESAGT, einschalten: a.fn })), /Wir melden uns bei dir mit den nächsten Schritten zur Bestellung\./)
|
||
assert.equal(a.aufrufe.length, 0)
|
||
}
|
||
}))
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulBuchungen.test.js`
|
||
Expected: FAIL.
|
||
|
||
- [ ] **Step 3: Implement**
|
||
|
||
`buchungen-anfrage.js`: `anfrageZeilen` herausziehen; im Abo-Modell erster und letzter Stichpunkt S1 statt `${preis} einmalig …`. `buchungen.js`: `einschalten` in die Sitzung; `zeigeAnfrage` zeigt bei `angenommen` im Abo-Modell `renderEinschaltSeite(wurzel, { ctx: { projectId, aktuell }, daten: { offen: [], kundenansicht: false }, key: 'unterkunft', zusammenfassung: () => [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: anfrageZeilen(ansicht.anfrage).map(({ name, wert }) => `${name}: ${wert}`) }], einschalten: s.einschalten, nachEinschalten: () => zeigeProjekt(s) })`. Das Rechnungsauftrag-Häkchen bleibt allein in `buchungen-tab-einstellungen.js` (unverändert). Import `renderEinschaltSeite` zur bestehenden Importzeile von `./funktion-bestellen.js` hinzufügen.
|
||
|
||
- [ ] **Step 4: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulBuchungen.test.js test/buchungenUi.test.js test/unterkunftWiring.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/buchungen.js public/buchungen-anfrage.js test/kundenansichtNeuModulBuchungen.test.js test/unterkunftWiring.test.js && git commit -m "Buchungskalender: im Abo-Modell Anfrage ohne Preis und Einschalten nach der Zusage" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 7: Öffnungszeiten live im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/zeiten.js` (`behaltenMoeglich` `:55`, `initZeiten` `:62-73`, `ladeListe` `:132`, `baueCtx` `:161`, `zeichneProjekt` `:173-232`), `public/zeiten-bestellen.js` (Import `:23`, neue Funktion nach `:105`, `zeitenStatusKarte` `:289`)
|
||
- Test: `test/kundenansichtNeuModulZeiten.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `renderEinschaltSeite` (Task 2; nur der benannte Import in der bestehenden Zeile `:23`), Testhilfen.
|
||
- Produces: `initZeiten(root, { projectId, einschalten } = {})`; `s.modell`, `s.einschalten` (auch in `ladeListe`); `baueCtx` liefert `einschalten`; `renderZeitenEinschalten(root, { ctx, projekt })` (Export von `zeiten-bestellen.js`; ruft `renderEinschaltSeite` mit `key: 'zeiten'`, `daten: { offen: [], kundenansicht: ctx.kundenansicht }`, `einschalten: ctx.einschalten`, `nachEinschalten: () => ctx.fertig({ danke: 'eingeschaltet' })`; die Überschrift „Öffnungszeiten live einschalten“ kommt von P3); `zeitenStatusKarte({ ..., modell })`. Im Abo-Modell gibt es keinen Behalten-Weg: weder Knopf noch `wunschWeg='behalten'` noch `zeigeBehalten`.
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulZeiten.test.js`**
|
||
|
||
Kopf:
|
||
|
||
```js
|
||
const EDITOR = { state: 'active', daten: null, version: 0, kundenansicht: false, vorschau: null, feiertage: [], googleText: '' } // Antwort von GET /api/zeiten/p1
|
||
const feature = (state, extra = {}) => ({ state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null, unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra })
|
||
const liste = (modell, state = 'off', extra = {}, angebot = {}) => ({ body: { ...(modell ? { modell } : {}), projects: [{ projectId: 'p1', projectName: 'Website p1', liveUrl: '', features: { news: { state: 'off' }, zeiten: feature(state, extra) }, zeitenStatus: null, zeitenLeer: false }], features: { zeiten: { priceLabel: '30 €', consent: { version: 'v', knopf: 'Zahlungspflichtig bestellen · 30 €', items: [] }, leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } } }, ustHinweis: 'ohne Umsatzsteuer gemäß § 19 UStG', zeiten: { verfuegbar: true, inklusive: true, grund: null, preis: '30 €', einschalten: { consent: { version: 'v', knopf: 'Einschalten', items: [] }, leistungsbeschreibung: { version: '2026-10-05', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } }, belehrung: 'Belehrung', ...angebot } } })
|
||
async function oeffne(fx, antwort, { einschalten, dataset = {} } = {}) {
|
||
routen(fx, { 'GET /api/funktionen': antwort, 'GET /api/zeiten/p1': { body: EDITOR } })
|
||
const root = h('div'); document.body.append(root)
|
||
Object.assign(root.dataset, dataset)
|
||
await initZeiten(root, { projectId: 'p1', einschalten }); await fx.ruhe()
|
||
return root
|
||
}
|
||
const ALT_ENDE = { aboEndeAt: '2026-09-01T00:00:00.000Z' }, KEIN_ABO = { inklusive: false, grund: 'kein_abo' }
|
||
```
|
||
|
||
```js
|
||
test('Abo, off: Einschalt-Seite mit key zeiten, ohne Haken, Preis und Behalten; nach onFertig die Danke-Zeile', mitDom(async (fx) => {
|
||
const a = einschaltenAttrappe(); let n = 0
|
||
const root = await oeffne(fx, () => (n === 0 ? liste('abo') : liste('abo', 'setup')), { einschalten: a.fn })
|
||
assert.deepEqual([a.aufrufe[0].key, a.aufrufe[0].projectId], ['zeiten', 'p1'])
|
||
assert.equal(fx.alle(root, 'input[type="checkbox"]').length, 0)
|
||
assert.equal(fx.finde(root, '[data-aktion="behalten"], [data-aktion="bestellen"], [data-aktion="einschalten"]'), null)
|
||
assert.doesNotMatch(fx.text(root), KEIN_PREIS)
|
||
n = 1; a.fertig(0); await fx.ruhe()
|
||
assert.match(fx.text(fx.finde(root, '.zt-status')), /Öffnungszeiten live ist eingeschaltet\./)
|
||
}))
|
||
test('Abo: Abo-Ende in active nennt die Frist (Z2) und „Eingeschaltet am“, ohne Preis und ohne Behalten-Knopf', mitDom(async (fx) => {
|
||
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', orderedAt: '2026-08-01T10:00:00.000Z', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {})
|
||
assert.match(fx.text(karte), /Dein Wartungs-Abo ist beendet\. Öffnungszeiten live läuft noch bis 01\.10\.2026 und schaltet sich dann ab\./)
|
||
assert.match(fx.text(karte), /Eingeschaltet am\s*01\.08\.2026/)
|
||
assert.equal(fx.finde(karte, '[data-aktion="behalten"]'), null)
|
||
assert.doesNotMatch(fx.text(karte), KEIN_PREIS)
|
||
}))
|
||
test('Abo: abgeschaltet mit Daten (Z3), ohne Behalten-Knopf', mitDom(async (fx) => {
|
||
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2099-12-31' }, angebot: { ...KEIN_ABO, preis: '30 €' }, modell: 'abo' }, {})
|
||
assert.match(fx.text(karte), /Deine Eingaben bleiben bis 31\.12\.2099 gespeichert\. Mit einem neuen Wartungs-Abo schaltet sich Öffnungszeiten live wieder ein\./)
|
||
assert.equal(fx.finde(karte, '[data-aktion="behalten"]'), null)
|
||
assert.doesNotMatch(fx.text(karte), KEIN_PREIS)
|
||
}))
|
||
test('wunschWeg „behalten“: im Abo-Modell ohne Wirkung, ohne modell die Behalten-Seite und der Knopf „Für 30 € behalten“', mitDom(async (fx) => {
|
||
const wunsch = { dataset: { wunschWeg: 'behalten' } }
|
||
const abo = await oeffne(fx, liste('abo', 'active', ALT_ENDE, KEIN_ABO), wunsch)
|
||
assert.equal(fx.finde(abo, '[data-variante="behalten"]'), null)
|
||
assert.equal(fx.finde(abo, '[data-aktion="behalten"]'), null)
|
||
const alt = await oeffne(fx, liste(undefined, 'active', ALT_ENDE, KEIN_ABO), wunsch)
|
||
assert.ok(fx.finde(alt, '[data-variante="behalten"]'))
|
||
const karte = zeitenStatusKarte({ projekt: { projectId: 'p1', zeitenLeer: false }, feature: { state: 'active', ...ALT_ENDE }, angebot: { ...KEIN_ABO, preis: '30 €' } }, {})
|
||
assert.equal(fx.text(fx.finde(karte, '[data-aktion="behalten"]')), 'Für 30 € behalten')
|
||
}))
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulZeiten.test.js`
|
||
Expected: FAIL.
|
||
|
||
- [ ] **Step 3: Implement `zeiten-bestellen.js`**
|
||
|
||
`renderZeitenEinschalten` nach `renderZeitenBestellen`. `zeitenStatusKarte`: bei `modell === 'abo'` der Block „Abo-Ende“ (`frist && lage === 'bezahlen'`) als Info-Box mit Z2 (Frist aus `behaltenFrist`), der Block „abgeschaltet mit Daten“ (`daten && lage === 'bezahlen'`) als Info-Box mit Z3 (`tagText`); die Fälle `lage === 'inklusive' | 'abo'` bleiben; Faktenzeile E5. Keine Preis-, Behalten- oder Rechnungs-Elemente.
|
||
|
||
- [ ] **Step 4: Implement `zeiten.js`**
|
||
|
||
`initZeiten(root, { projectId, einschalten } = {})`; `s.modell` (auch in `ladeListe`), `s.einschalten`; `baueCtx` ergänzt `einschalten`; `zeichneProjekt`: Zustand `off` im Abo-Modell ruft `renderZeitenEinschalten(kopf, { ctx, projekt })`; der Aufruf von `zeitenStatusKarte` übergibt `modell`; `behaltenMoeglich(projekt, angebot, modell)` liefert im Abo-Modell `false`. Keine neue Importzeile: `renderZeitenEinschalten` kommt in die benannten Importe von `./zeiten-bestellen.js`.
|
||
|
||
- [ ] **Step 5: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulZeiten.test.js test/zeitenView.test.js test/zeitenBestellen.test.js test/zeitenEditor.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/zeiten.js public/zeiten-bestellen.js test/kundenansichtNeuModulZeiten.test.js && git commit -m "Öffnungszeiten live: im Abo-Modell Einschalten und kein Behalten-Weg" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 8: Analyse im Abo-Modell
|
||
|
||
**Files:**
|
||
- Modify: `public/analyse.js` (`initAnalyse` `:22-49`, `renderAnalyseActivation` `:76-112`, `mountCockpit` `:174-328`; neue relative Importzeile `./fn-reiter.js`)
|
||
- Test: `test/kundenansichtNeuModulAnalyse.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `htmlWurzel('#an-stage')` (mit `an`, `setAttribute`, `kn.reiter`), `KEIN_PREIS`, `meldeReiter`, `reiterEintraege` (Task 1); Antwortfeld `modell`.
|
||
- Produces: `renderAnalyseActivation(stage, project, reload, { modell } = {})` (vierter Parameter optional); im Abo-Modell Linkzeile A1 statt des langen Häkchen-Satzes, ein Häkchen `#an-zielgruppe` mit A2 (sperrt den Knopf `#an-on` wie `#an-avv`, zusätzlich bei `controller.ok !== true`); Text A3 bei `kein_abo`. Der Körper von `POST /api/funktionen/analyse/activate` ist im Abo-Modell `{ projectId, accentColor, consent: { accepted: true, avvVersion }, audienceConfirmed: true }` (VF4; der heutige Server ignoriert das Feld). `root.dataset.wunschProjekt` wählt das Projekt (V2) und wird entfernt.
|
||
- **Reiter in der Etage** (Spec 5.2 und 5.4, VF2): Im Abo-Modell zeichnet `initAnalyse` die Leiste `tabsHtml` nicht (kein `role="tablist"`, kein `.an-tabbtn`); das Panel `#an-stage` trägt `role="tabpanel"` und `aria-label` mit dem Label des aktiven Reiters statt `aria-labelledby`. `mountCockpit({ …, abo, root })` meldet in `showTab()` nach `paintTabs()` `meldeReiter(root, { eintraege: reiterEintraege('analyse', ANALYSE_REITER), aktiv: tabNow(), bei: pickTab })` mit `ANALYSE_REITER = [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }]` (Reihenfolge und Wortlaut wie `tabsHtml`; `paintTabs` setzt im Abo-Modell `aria-label` statt `aria-labelledby`). Jeder Zweig ohne Cockpit (Einschalten, abgeschaltet, nicht verfügbar, unbekannt, keine Website) meldet vor dem Zeichnen die leere Liste, damit die Etage nicht die Reiter der vorigen Ansicht behält; das Cockpit ersetzt sie gleich danach. Die Kopfleiste (Website wählen, Zeitraum) bleibt im Inhalt, `analyse-cards.js` bleibt unverändert. Ohne `modell === 'abo'` bleibt alles wie heute: Leiste `tabsHtml`, `aria-labelledby`, kein Ereignis.
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulAnalyse.test.js`**
|
||
|
||
Kopf (nach `funktionenTiles.test.js:405-428`):
|
||
|
||
```js
|
||
import { analyseSel } from '../public/analyse-view.js'
|
||
const { initAnalyse } = await import('../public/analyse.js')
|
||
globalThis.document = { querySelectorAll: () => [] } // pickTab ruft hideChartTip
|
||
after(() => { delete globalThis.document })
|
||
const LEGAL = { avv: '/rechtliches/avv-website-analyse-2026-10-02.html', lb: '/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html' }
|
||
const CTRL = { ok: true, company: 'Muster GmbH', address: 'Weg 1, 12345 Ort', email: 'info@muster.de' }
|
||
const offProject = (id = 'p1', analyse = {}) => ({ projectId: id, projectName: `Muster ${id}`, analyse: { state: 'off', eligible: true, accentColor: '#111111', avvVersion: '2026-10-02', legal: LEGAL, controller: CTRL, ...analyse } })
|
||
const aktivProjekt = (id = 'p1') => offProject(id, { state: 'active' })
|
||
const KEIN_ABO = { eligible: false, reason: 'kein_abo', reasonText: 'Alter Satz des Servers' }
|
||
async function lauf({ modell, projects = [offProject()], dataset = {}, projectId = 'p1', tab = 'uebersicht' } = {}) {
|
||
const posts = []
|
||
globalThis.fetch = async (url, opts) => {
|
||
if (opts?.method === 'POST') posts.push({ url: String(url), body: JSON.parse(opts.body) })
|
||
if (String(url).startsWith('/api/funktionen/analyse/stats')) return { ok: false, status: 503, json: async () => ({}) } // das Cockpit zeigt die Fehlerkarte; mehr braucht der Reitertest nicht
|
||
return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects }) }
|
||
}
|
||
const { root, stage, kn } = htmlWurzel('#an-stage')
|
||
Object.assign(root.dataset, dataset)
|
||
analyseSel.tab = tab
|
||
await initAnalyse(root, { projectId })
|
||
return { root, stage, posts, kn }
|
||
}
|
||
```
|
||
|
||
```js
|
||
test('Abo: Linkzeile A1 mit beiden Links, ein Häkchen mit A2, kein langer Zustimmungssatz, keine Preise', async () => {
|
||
const { stage } = await lauf({ modell: 'abo' })
|
||
const html = stage.innerHTML
|
||
assert.match(html, /Es gelten <a href="[^"]*"[^>]*>Leistungsbeschreibung<\/a> und <a href="[^"]*"[^>]*>Auftragsverarbeitung<\/a> \(Fassung 2026-10-02\)\./)
|
||
assert.ok(html.includes(`href="${LEGAL.lb}"`) && html.includes(`href="${LEGAL.avv}"`))
|
||
assert.match(html, /Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren\./)
|
||
assert.doesNotMatch(html, /Ich beauftrage WEBklar|id="an-avv"/)
|
||
assert.doesNotMatch(html, KEIN_PREIS)
|
||
})
|
||
test('Abo: Knopf gesperrt ohne Zielgruppen-Häkchen und bei unvollständigem Verantwortlichen; Körper mit audienceConfirmed', async () => {
|
||
const { stage, posts } = await lauf({ modell: 'abo' })
|
||
const box = stage.els['#an-zielgruppe']; const knopf = stage.els['#an-on']
|
||
box.checked = true; box.handlers.change(); assert.equal(knopf.disabled, false)
|
||
box.checked = false; box.handlers.change(); assert.equal(knopf.disabled, true)
|
||
box.checked = true; box.handlers.change(); await knopf.handlers.click()
|
||
assert.deepEqual(posts, [{ url: '/api/funktionen/analyse/activate', body: { projectId: 'p1', accentColor: '#111111', consent: { accepted: true, avvVersion: '2026-10-02' }, audienceConfirmed: true } }])
|
||
const ohne = await lauf({ modell: 'abo', projects: [offProject('p1', { controller: { ok: false, company: '', address: '', email: '', missing: ['firma'] } })] })
|
||
ohne.stage.els['#an-zielgruppe'].checked = true; ohne.stage.els['#an-zielgruppe'].handlers.change()
|
||
assert.equal(ohne.stage.els['#an-on'].disabled, true)
|
||
})
|
||
test('Abo: kein_abo nennt Hosting- und Wartungs-Abo mit Link zu Verträge (A3), nicht den Satz des Servers', async () => {
|
||
const { stage } = await lauf({ modell: 'abo', projects: [offProject('p1', KEIN_ABO)] })
|
||
assert.match(stage.innerHTML, /Die Analyse ist im Hosting- und im Wartungs-Abo enthalten\./)
|
||
assert.match(stage.innerHTML, /data-view="vertraege"/)
|
||
assert.doesNotMatch(stage.innerHTML, /Alter Satz des Servers/)
|
||
})
|
||
test('Ohne modell (fehlt, „Abo“): der lange Zustimmungssatz mit #an-avv und bei kein_abo der Satz des Servers', async () => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const a = await lauf({ modell })
|
||
assert.match(a.stage.innerHTML, /id="an-avv"/)
|
||
assert.match(a.stage.innerHTML, /Ich beauftrage WEBklar/)
|
||
const b = await lauf({ modell, projects: [offProject('p1', KEIN_ABO)] })
|
||
assert.match(b.stage.innerHTML, /Alter Satz des Servers/)
|
||
}
|
||
})
|
||
test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => {
|
||
const { root, stage } = await lauf({ modell: 'abo', projects: [offProject('p1'), offProject('p2')], dataset: { wunschProjekt: 'p2' }, projectId: '' })
|
||
assert.match(stage.innerHTML, /Analyse für Muster p2 einschalten/)
|
||
assert.equal(root.dataset.wunschProjekt, undefined)
|
||
})
|
||
test('Abo, aktive Analyse: keine eigene Leiste, kn:reiter mit den drei Reitern, Panel mit aria-label; bei() wechselt und meldet den neuen aktiven Reiter', async () => {
|
||
assert.equal(typeof CustomEvent, 'function', 'meldeReiter braucht CustomEvent (Node 20: global)')
|
||
const { root, stage, kn } = await lauf({ modell: 'abo', projects: [aktivProjekt()] })
|
||
assert.doesNotMatch(root.innerHTML, /role="tablist"|an-tabbtn|aria-labelledby/)
|
||
assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-label="Übersicht"/)
|
||
const erst = kn.reiter.at(-1)
|
||
assert.deepEqual(erst.eintraege, [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }])
|
||
assert.equal(erst.aktiv, 'uebersicht')
|
||
erst.bei('heatmap'); await new Promise((r) => setImmediate(r))
|
||
assert.equal(kn.reiter.at(-1).aktiv, 'heatmap')
|
||
assert.equal(stage.attribute['aria-label'], 'Heatmap')
|
||
assert.equal(analyseSel.tab, 'heatmap')
|
||
})
|
||
test('Abo ohne Cockpit (Einschalten, abgeschaltet, nicht verfügbar, keine Website): leere Reiterliste, „Zurück“ füllt die Zeile', async () => {
|
||
for (const projects of [[offProject()], [offProject('p1', { state: 'disabled' })], [offProject('p1', KEIN_ABO)], []]) {
|
||
const { kn } = await lauf({ modell: 'abo', projects })
|
||
assert.deepEqual(kn.reiter.at(-1)?.eintraege, [], JSON.stringify(projects.map((p) => p.analyse.state)))
|
||
}
|
||
})
|
||
test('Ohne modell (fehlt, „Abo“): eigene Leiste mit denselben drei Labels, aria-labelledby, kein kn:reiter', async () => {
|
||
for (const modell of [undefined, 'Abo']) {
|
||
const { root, kn } = await lauf({ modell, projects: [aktivProjekt()] })
|
||
assert.deepEqual([...root.innerHTML.matchAll(/role="tab"[^>]*>([^<]+)</g)].map((m) => m[1]), ['Übersicht', 'Heatmap', 'Einstellungen'])
|
||
assert.match(root.innerHTML, /role="tablist"/)
|
||
assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-labelledby="an-tab-uebersicht"/)
|
||
assert.equal(kn.reiter.length, 0)
|
||
}
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulAnalyse.test.js`
|
||
Expected: FAIL.
|
||
|
||
- [ ] **Step 3: Implement in `analyse.js`**
|
||
|
||
`initAnalyse(root, { projectId } = {})` liest `root.dataset?.wunschProjekt` und `modell` und reicht `{ modell }` an `renderAnalyseActivation`; die Texte A1–A3 über kleine Konstanten; die Abbildung im Abo-Modell ersetzt nur den `<label class="fn-consent">`-Block, den Körper der Aktivierung und den `kein_abo`-Satz, alles andere (Farbwahl, Banner-Vorschau, Verantwortlicher) bleibt. `a.legal` bleibt die Quelle der Links (Pin `avvPage.test.js:28-34`), kein `confirm(`.
|
||
|
||
Reiter: neue Importzeile `import { meldeReiter, reiterEintraege } from './fn-reiter.js'` und die Konstante `ANALYSE_REITER` neben `TABS`. `initAnalyse` liest `abo = modell === 'abo'`, meldet bei `abo` vor dem Zeichnen die leere Liste (jeder Zweig, auch die frühe Rückkehr ohne nutzbare Website), lässt `tabsHtml` weg und setzt das Panel-Attribut. `mountCockpit` bekommt `abo` und `root`; `showTab()` ruft nach `paintTabs()` eine Closure `melde()` (`abo && meldeReiter(root, …)`, `bei: pickTab`). Klick- und Tastatur-Handler bleiben unverändert: der Teaser „Heatmap“ der Übersicht ruft ebenfalls `pickTab`, `showTab()` meldet dann den neuen aktiven Reiter an die Etage.
|
||
|
||
- [ ] **Step 4: Run tests to verify they pass**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulAnalyse.test.js test/analyseView.test.js test/avvPage.test.js test/funktionenTiles.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/analyse.js test/kundenansichtNeuModulAnalyse.test.js && git commit -m "Analyse: im Abo-Modell Linkzeile und Zielgruppen-Häkchen, Hosting berechtigt, Reiter in der Etage" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 9: `module.css` Gerüst, Prüftest und gemeinsame Bausteine
|
||
|
||
**Files:**
|
||
- Modify: `public/neu/module.css` (Stub von P1)
|
||
- Test: `test/kundenansichtNeuModulCss.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `public/neu/tokens.css` und die übrigen `public/neu/*.css` (P1–P3, nur lesen); alte CSS-Dateien in `public/` als Klassenquelle.
|
||
- Produces: `module.css` mit Abschnitt 0 (Alt-Klassen, Fokus, Handy, reduzierte Bewegung). Der Prüftest hat die Liste `FERTIG` (Präfixe, für die schon die Abdeckung gilt); Task 10–13 hängen ihre Präfixe an.
|
||
|
||
**Gestaltungsregeln** (gelten für Task 9–13; alle Selektoren beginnen mit `.kn `):
|
||
|
||
| Altes Muster | Neu |
|
||
|---|---|
|
||
| Karte (`.kb-panel`, `*-abschnitt`, `*-info`, `fn-booking`) | Abschnitt: ohne Fläche, Rahmen, Radius; `padding: 20px 0`; ab dem zweiten Abschnitt obere Haarlinie `#ebeef1`; `h3` 15px/600 |
|
||
| Hinweis (`tm-box`, `sh-box`, `-info`, `-warn`) | Zeile `#f8f9fb`, Radius 6px, 14px, ohne Rahmen; `-warn`: Fläche `#fcedb9`, Schrift `#a82c00` |
|
||
| Meldung (`tm-meldung-ok/-fehler`, `sh-meldung-*`) | grüne Pille `#d7f7c2`/`#006908`, rote `#ffe7f2`/`#b3093c`; Feldfehler Schrift `#c0123c` |
|
||
| Reiterleisten (`an-tabs`, `tm-reiterleiste`, `sh-reiterleiste`) | Unterstrich-Reiter: Flex, `overflow-x: auto`, untere Haarlinie; Reiter 14px/600 `#5e6678`, aktiv `#111` mit 2px Unterstrich `#111`; Handy: 44px hoch. Die Leisten von Terminplaner, Webshop, Buchungskalender und Analyse zeichnet im Abo-Modell die Etage; ihre Klassen bleiben trotzdem gestaltet (Abdeckungstest, die alte Ansicht nutzt sie weiter) |
|
||
| Segment, Chip (`tm-seg`, `tm-chip`) | Ring-Schatten, aktiv Fläche `#111` und Schrift `#fff` |
|
||
| `.kb-btn-primary`, `.abo-btn` | Fläche `#111`, Schrift `#fff`, Radius 6px, 36px (Handy 44px), `:disabled` 40 % Deckkraft |
|
||
| `.kb-btn-outline`, `.kb-btn` | Fläche `#fff`, Ring-Schatten `0 0 0 1px rgba(60, 66, 87, .16), 0 1px 1px rgba(0, 0, 0, .06)`, hover `#f6f8fa` |
|
||
| Pillen (`tm-pille-*`, `sh-pille-*`, `fn-pill-*`) | ohne Rand: `wait`/`setup` orange (`#fcedb9`/`#a82c00`), `ok`/`on` grün, `off` grau (`#ebeef1`/`#414552`), `bad` rot, Info blau (`#e0f2fe`/`#075985`) |
|
||
| Felder (input, select, textarea) | Fläche `#fff`, Ring-Schatten, Radius 6px, 36px (Handy 44px); Fokus siehe Abschnitt 0 |
|
||
| Listen und Kacheln (`tm-eintrag`, `sh-artikel`, `bk-zeile`) | Zeilen mit Haarlinie, kein Kasten; Hover `#f6f8fa` |
|
||
| Rückfrage (`tm-frage-box`, `sh-frage-box`) | Fläche `#f8f9fb`, Radius 12px, kein Rahmen |
|
||
| Belegt/frei/angefragt/gesperrt (Kalender) | belegt `#111` mit `#fff`, angefragt orange Pille, gesperrt grau Pille, frei `#fff` mit Haarlinie |
|
||
|
||
- [ ] **Step 1: Failing test `test/kundenansichtNeuModulCss.test.js`**
|
||
|
||
Hilfen im Testfile: `lies(datei)` (liest aus `public/`), `regeln(css): { selektor, koerper }[]` (Kommentare entfernt, verschachtelte `@media`-Regeln flach, `@keyframes` ohne Schritte), `selektoren(css): string[]` (Kommalisten aufgespalten, Klammer-bewusst für `:is()` und `:not()`), `klassen(css, praefix): Set<string>` (aus den Selektoren), `neuCss()` = Inhalt aller `public/neu/*.css`, `MODUL()` = `public/neu/module.css`, `jsModule()` = Inhalt aller `public/*.js` und `public/neu/*.js`.
|
||
|
||
```js
|
||
const FERTIG = [] // Präfixe mit geprüfter Abdeckung; Task 10–13 ergänzen sie
|
||
const UNGENUTZT = new Set([]) // alte Klassen, die in keiner JS-Datei vorkommen; der Test prüft das
|
||
const ALT = { tm: 'termine.css', sh: 'shop.css', bk: 'buchungen.css', zt: 'zeiten.css', an: 'analyse.css', hs: 'analyse.css', nw: 'funktionen.css' }
|
||
const FN_NEWS = ['fn-project', 'fn-booking', 'fn-price', 'fn-estimate', 'fn-desc-link', 'fn-consent', 'fn-thanks', 'fn-status', 'fn-facts', 'fn-pill', 'fn-pill-setup']
|
||
const ALT_KLASSEN = ['kb-panel', 'kb-btn', 'kb-btn-primary', 'kb-btn-outline', 'kb-empty', 'kb-link', 'kb-checks', 'abo-actions', 'abo-btn', 'hidden', 'muted', 'error']
|
||
const PALETTE = new Set(['#fff', '#ffffff', '#1a1f36', '#5e6678', '#8a93a3', '#ebeef1', '#d8dde3', '#f6f8fa', '#f8f9fb', '#111', '#111111', '#414552', '#e0f2fe', '#075985', '#fcedb9', '#a82c00', '#efeafe', '#5b21b6', '#d7f7c2', '#006908', '#ffe7f2', '#b3093c', '#c0123c'])
|
||
const ZETTEL = /#fff6d6|#ddeffb|#ffe1d9/i
|
||
const RING = new Set(['60,66,87,.16', '60,66,87,0.16', '0,0,0,.06', '0,0,0,0.06'])
|
||
const HANDY = '@media (max-width: 880px), (pointer: coarse) and (max-height: 500px)'
|
||
|
||
test('module.css: jede Regel steht unter .kn und hängt nicht an der alten Seite', () => {
|
||
for (const s of selektoren(MODUL())) assert.match(s, /^\.kn(\s|[.:#[>~+]|$)/, s)
|
||
assert.doesNotMatch(MODUL(), /#view-|--kb-|\.kb-page/)
|
||
})
|
||
test('module.css: Verbote (Lila, Teal, Verlauf, Glas, Dunkel, Gewicht 800, Animation)', () => {
|
||
assert.doesNotMatch(MODUL(), /#9d3dfe|#d4b4ff|157,\s*61,\s*254|#2dd4bf|#5eead4|gradient|backdrop-filter|color-scheme:\s*dark|prefers-color-scheme|font-weight:\s*(800|900)|@keyframes/)
|
||
})
|
||
test('module.css: Farben nur aus der Palette der Übersicht; Zettelfarben nur in .nw-paper-*; rgba nur der Ring-Schatten', () => {
|
||
for (const { selektor, koerper } of regeln(MODUL())) {
|
||
for (const [, hex] of koerper.matchAll(/(#[0-9a-fA-F]{3,8})\b/g)) assert.ok(PALETTE.has(hex.toLowerCase()) || (ZETTEL.test(hex) && /\.nw-paper-/.test(selektor)), `${selektor}: ${hex}`)
|
||
for (const [, wert] of koerper.matchAll(/rgba?\(([^)]*)\)/g)) assert.ok(RING.has(wert.replace(/\s/g, '')), `${selektor}: rgba(${wert})`)
|
||
}
|
||
})
|
||
test('module.css: Fokus schwarz, Handy-Regeln wörtlich mit 44px, reduzierte Bewegung', () => {
|
||
const css = MODUL()
|
||
assert.match(css, /:focus-visible[^{]*\{[^}]*outline:\s*2px solid (#111|var\(--kn-[a-z-]+\))/)
|
||
assert.ok(css.includes(HANDY) && /min-height:\s*44px/.test(css.slice(css.indexOf(HANDY))))
|
||
assert.match(css, /prefers-reduced-motion:\s*reduce/)
|
||
})
|
||
test('module.css: jedes var(--kn-…) ist in public/neu/*.css definiert', () => {
|
||
const definiert = new Set([...neuCss().matchAll(/(--kn-[a-z0-9-]+)\s*:/g)].map((m) => m[1]))
|
||
for (const [, name] of MODUL().matchAll(/var\((--kn-[a-z0-9-]+)/g)) assert.ok(definiert.has(name), name)
|
||
})
|
||
test('Alt-Klassen der Module sind unter .kn gestaltet (in einer Datei von public/neu)', () => {
|
||
const sel = selektoren(neuCss()).filter((s) => s.startsWith('.kn'))
|
||
for (const k of ALT_KLASSEN) assert.ok(sel.some((s) => new RegExp(`\\.${k}(?![a-z0-9-])`).test(s)), k)
|
||
})
|
||
test('UNGENUTZT enthält nur Klassen, die kein JS nennt', () => {
|
||
for (const k of UNGENUTZT) assert.ok(!jsModule().includes(k), k)
|
||
})
|
||
for (const praefix of FERTIG) test(`Abdeckung ${praefix}: jede Klasse der alten CSS-Datei ist unter .kn gestaltet`, () => {
|
||
const alt = praefix === 'fn' ? FN_NEWS : [...klassen(lies(ALT[praefix]), praefix)]
|
||
const neu = klassen(neuCss(), praefix)
|
||
assert.deepEqual(alt.filter((k) => !neu.has(k) && !UNGENUTZT.has(k)).sort(), [])
|
||
})
|
||
```
|
||
Für das Präfix `fn` gilt nur die Liste `FN_NEWS`; die Kachel-Klassen (`fn-tile*`, `fn-tiles`, `fn-count`, `fn-ico`) gehören P3. In `ALT` steht kein Eintrag für `fn`.
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: FAIL (Fokus-Regel, Handy-Block und Alt-Klassen fehlen im Stub von P1).
|
||
|
||
- [ ] **Step 3: Abschnitt 0 in `public/neu/module.css` schreiben**
|
||
|
||
Kopfkommentar mit der Abschnittsliste (0 gemeinsame Bausteine, 1 Terminplaner, 2 Webshop und Buchungen, 3 Öffnungszeiten, 4 News, 5 Analyse). Abschnitt 0: die Alt-Klassen aus `ALT_KLASSEN` nach den Gestaltungsregeln, soweit noch nicht in einer anderen Datei von `public/neu/` unter `.kn` definiert (`command grep -a -n "kb-panel" public/neu/*.css` je Klasse; keine Doppeldefinition, VF3); Fokus-Regel für `button, input, select, textarea, summary, a` mit `outline: 2px solid #111; outline-offset: 2px`; der Block `HANDY` mit `min-height: 44px` für Knöpfe und Felder; `@media (prefers-reduced-motion: reduce)` setzt `transition: none`. Token-Namen (`var(--kn-…)`) nur aus `tokens.css`.
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: PASS (Abdeckungstests laufen noch nicht, `FERTIG` ist leer).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Gerüst, Prüftest und gemeinsame Bausteine unter .kn" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 10: `module.css` Terminplaner (`tm-`)
|
||
|
||
**Files:**
|
||
- Modify: `public/neu/module.css` (Abschnitt 1), `test/kundenansichtNeuModulCss.test.js` (`FERTIG`)
|
||
|
||
**Interfaces:**
|
||
- Consumes: Gestaltungsregeln aus Task 9; alte Klassenquelle `public/termine.css` (auch die `tm-`-Bausteine von Shop, Buchungen und Öffnungszeiten).
|
||
- Produces: Abdeckung `tm`.
|
||
|
||
- [ ] **Step 1: Präfix in den Test aufnehmen**
|
||
|
||
In `test/kundenansichtNeuModulCss.test.js` `FERTIG = ['tm']`.
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: FAIL; „Abdeckung tm“ listet die fehlenden Klassen.
|
||
|
||
- [ ] **Step 3: Abschnitt 1 schreiben**
|
||
|
||
Alle `tm-`-Klassen der Liste. Entscheidungen, die die Regeltabelle nicht abdeckt:
|
||
- Fortschrittsleiste (`tm-schritte`, `tm-schritt*`): Zahlenkreis 28px, erledigt `#111` mit `#fff`, aktuell Ring `#111`; am Handy zeigt `tm-schritt-stand` den Zähler („Schritt n von 8“) und `tm-schritt-titel` ist nur noch für Screenreader da (visuell versteckt).
|
||
- Bestellseite (`tm-bestellen*`) und `tm-einschalten`: Abschnitte ohne Kasten; `tm-rechtstext` bleibt ein scrollbarer Bereich (`max-height`, `overflow-y: auto`, `white-space: pre-wrap`, Fläche `#f8f9fb`); `tm-einschalten-platz` ohne eigenes Aussehen (P3 gestaltet den Ablauf).
|
||
- Statuskarte (`tm-statuskarte`): Abschnitt, Pille oben, Fakten als zweispaltige Liste (`dt` `#5e6678`).
|
||
- Terminliste und Editoren (`tm-eintrag`, `tm-editor-box`, `tm-formular-box`): Zeilen mit Haarlinie, Editor klappt darunter ohne Rahmen auf.
|
||
- `tm-sr` bleibt als Screenreader-Klasse unverändert.
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Terminplaner und gemeinsame Bestellbausteine (tm-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 11: `module.css` Webshop und Buchungskalender (`sh-`, `bk-`)
|
||
|
||
**Files:**
|
||
- Modify: `public/neu/module.css` (Abschnitt 2), `test/kundenansichtNeuModulCss.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Gestaltungsregeln; Klassenquellen `public/shop.css`, `public/buchungen.css`.
|
||
- Produces: Abdeckung `sh` und `bk`.
|
||
|
||
- [ ] **Step 1: `FERTIG = ['tm', 'sh', 'bk']`**
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: FAIL; „Abdeckung sh“ und „Abdeckung bk“ listen die fehlenden Klassen.
|
||
|
||
- [ ] **Step 3: Abschnitt 2 schreiben**
|
||
|
||
- Shop: `sh-reiterleiste` und `sh-reiter` wie `an-tabs` (der Buchungskalender nutzt sie mit); Anfrage (`sh-anfrage`, `sh-form`, `sh-erklaerung`, `sh-gruppe`): Formular einspaltig, Stichpunkte der Erklärung `#5e6678`; `sh-hinweis[data-satz]` (Satz über dem Knopf) 14px `#1a1f36` mit Abstand 12px zum Knopf; Zahlen `tabular-nums`.
|
||
- Buchungen: `bk-zeile` als Listenzeile mit Haarlinie; `bk-posten-*` am Handy zwei Spalten (wie die alte Regel); Kalender nach der Zeile „belegt/frei/angefragt/gesperrt“ der Tabelle.
|
||
- Artikelbilder und Vorschauen behalten ihre Seitenverhältnisse, der Hintergrund ist `#f6f8fa`.
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Webshop und Buchungskalender (sh-, bk-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 12: `module.css` Öffnungszeiten und News (`zt-`, `fn-`, `nw-`)
|
||
|
||
**Files:**
|
||
- Modify: `public/neu/module.css` (Abschnitte 3 und 4), `test/kundenansichtNeuModulCss.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Gestaltungsregeln; Klassenquellen `public/zeiten.css`, `public/funktionen.css` (nur `FN_NEWS` und `nw-`).
|
||
- Produces: Abdeckung `zt`, `fn` (Liste `FN_NEWS`), `nw`.
|
||
|
||
- [ ] **Step 1: `FERTIG = ['tm', 'sh', 'bk', 'zt', 'fn', 'nw']`**
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: FAIL; „Abdeckung zt“, „fn“, „nw“ listen die fehlenden Klassen.
|
||
|
||
- [ ] **Step 3: Abschnitte 3 und 4 schreiben**
|
||
|
||
- Öffnungszeiten (`zt-*`): Editor als Tageszeilen mit Haarlinie, `zt-hinweis` und `zt-status` als Abschnitt; Zeitfelder 44px am Handy.
|
||
- News: `fn-booking`, `fn-status`, `fn-facts` als Abschnitt; `fn-thanks` grüne Pille; `fn-pill-setup` orange Pille; `nw-`-Formular einspaltig mit Haarlinien. Vorschau-Zettel (`nw-paper`, `nw-polaroid`) behalten Drehung und Papierfarben (`.nw-paper-sun|sky|coral|white` setzen `--paper` auf `#fff6d6`, `#ddeffb`, `#ffe1d9`, `#fff`); der Schatten wird der Ring-Schatten; Auswahl der Zettelfarbe (`nw-sw[aria-pressed='true']`) mit Außenlinie `#111`.
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Öffnungszeiten und News (zt-, fn-, nw-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 13: `module.css` Analyse (`an-`, `hs-`)
|
||
|
||
**Files:**
|
||
- Modify: `public/neu/module.css` (Abschnitt 5), `test/kundenansichtNeuModulCss.test.js`
|
||
|
||
**Interfaces:**
|
||
- Consumes: Gestaltungsregeln; Klassenquelle `public/analyse.css`; `analyse-cards.js` (Klassenvertrag im Kopf der Datei).
|
||
- Produces: Abdeckung `an` und `hs`. Die Analyse bleibt über `.an` mit eigenen Variablen gebaut; sie werden hell belegt.
|
||
|
||
- [ ] **Step 1: `FERTIG = ['tm', 'sh', 'bk', 'zt', 'fn', 'nw', 'an', 'hs']`**
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: FAIL; „Abdeckung an“ und „hs“ listen die fehlenden Klassen.
|
||
|
||
- [ ] **Step 3: Abschnitt 5 schreiben**
|
||
|
||
`.kn .an` belegt die Variablen neu: `--an-fg: #1a1f36`, `--an-fg2` und `--an-muted: #5e6678`, `--an-faint: #8a93a3` (nur für Nebensächliches), `--an-line: #ebeef1`, `--an-line2: #d8dde3`, `--an-card: #fff`, `--an-acc` und `--an-acc-strong: #111`, `--an-acc-soft: #f6f8fa`, `--an-on-acc: #fff`, `--an-up-bg: #d7f7c2` (Schrift `#006908`), `--an-down: #b3093c`, `--an-down-bg: #ffe7f2`, `--an-prev: #8a93a3`, `--an-warn: #a82c00`, `--an-bar-fill: #ebeef1`. Diagramme: Linie und Punkte `#111`, Fläche `#f6f8fa`, Vergleichslinie `#8a93a3` gestrichelt, Balken `#111` auf `#ebeef1`. Die Banner-Vorschau (`an-bn*`) bildet die Kundenseite ab: Fläche `#fff` mit Ring-Schatten; ihre Knopffarbe kommt inline aus dem Kunden-Akzent. Heatmap-Rahmen (`hs-*`) bekommen Fläche `#f6f8fa`; `geraete.css` bleibt unberührt. Keine `color-scheme: dark`, kein Verlauf in `--an-bar-fill`.
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js`
|
||
Expected: PASS (alle Abdeckungen).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css test/kundenansichtNeuModulCss.test.js && git commit -m "module.css: Analyse hell (an-, hs-)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
|
||
### Task 14: Sichtprüfung mit dem Werkzeug aus P0 und Bild der Analyse
|
||
|
||
**Files:**
|
||
- Modify (Werkzeug-Git, `KW` = `/root/reports/kundenansicht-neu-2026-10-15/werkzeug`): `views.mjs`, `gates.mjs`, `fixtures/faelle.mjs` und `fixtures/index.mjs` (nur Einträge für die neuen Fälle), `fixtures/endpunkte.mjs` (ein Import), `vertrag-endpunkte.mjs` (Einträge der zwei Analyse-Endpunkte); Create: `fixtures/module.mjs` (17 Fälle), `fixtures/endpunkte-p4.mjs`. Nicht angefasst werden `test/backend.test.mjs` (Pin „Fälle in fester Reihenfolge“) und `vorschau/boot.js` (VF5).
|
||
- Modify (Portal-Worktree, bei Befund): `public/neu/module.css`; neu erzeugt: `public/neu/bilder/analyse.webp`
|
||
|
||
**Interfaces:**
|
||
- Consumes (P0, P1, P3): Ansichtsfelder `{ id, fall, adresse, geraete, warte, schritte?(seite), bewegung?, ignorieren? }`; `GATES.p<N> = { checks, ansichten }` mit `ALWAYS_ON`, `SEITEN_PRUEFUNGEN = ['dark-surface', 'low-contrast', 'overflow-x', 'lila']`; Geräte `handy` 390×844, `tablet` 820×1180, `desktop` 1440×900; Läufer `KN_STRANG_ENV=<KW>/tmp/strang-ui.env <KW>/shots.sh <Ausgabe> --gate p4`; die Fälle `betrieb-wartung` und `privat-frist` und die Modul-Endpunkte `termine`, `shop`, `unterkunft`, `zeiten`, `news` (Formen laut Karte `woms-vorschau.md` B4); Bild-Skript `bash <KW>/run-browser.sh <KW>/bilder/erzeuge.mjs --ziel <Ordner> [key …]` (P3 Task 8), das für `analyse` die Ansicht `kn-modul-analyse` braucht. Einen Fall `bilder` gibt es in keinem Plan: P3 arbeitet mit `betrieb-wartung`, die Analyse-Fälle legt dieser Task an. Auch die Endpunkte der aktiven Analyse (`analyse.stats`, `analyse.heatmap-link`) fehlen in P0 und P3 und kommen hier dazu.
|
||
- Produces: `GATES.p4 = { checks: [...ALWAYS_ON, ...SEITEN_PRUEFUNGEN], ansichten: ['kn-modul-*'] }` grün; 17 neue Fälle `m-*` hinter den vierzehn von P0 in `FALL_NAMEN`; 20 Ansichten, je auf Handy, Tablet und Desktop:
|
||
|
||
| id (`kn-modul-…`) | Fall, Adresse | Zeigt und prüft |
|
||
|---|---|---|
|
||
| `termine-einschalten` | `m-termine-fragebogen`, `#termine?projekt=p1` | letzter Schritt „Prüfen und einschalten“; keine Leiste im Inhalt; Etage ohne Reiter, „Zurück“ füllt die Zeile |
|
||
| `termine-aktiv` | `m-termine-aktiv`, `#termine?projekt=p1` | Etage mit vier Reitern, Handy „Team“, Desktop „Team & Plätze“; die Zeile scrollt bei 320 px, kein seitlicher Überlauf der Seite |
|
||
| `termine-setup`, `termine-aus` | `m-termine-setup`, `m-termine-aus` | Statuskarte „Eingeschaltet am“; Hinweis zum Abo-Ende |
|
||
| `shop-anfrage`, `shop-privat`, `shop-angenommen`, `shop-aktiv` | `m-shop-anfrage`, `privat-frist`, `m-shop-angenommen`, `m-shop-aktiv` | Satz über „Anfrage senden“; Privat-Hinweis mit Chat; Zusammenfassung und Ablauf; fünf Reiter, Handy „Start“ und „Recht“ |
|
||
| `buchungen-anfrage`, `buchungen-angenommen`, `buchungen-aktiv` | `m-buchungen-anfrage`, `m-buchungen-angenommen`, `m-buchungen-aktiv` | kein Preis; fünf Reiter, Handy „Preise“ |
|
||
| `zeiten-einschalten`, `zeiten-aktiv`, `zeiten-abo-ende`, `zeiten-abgeschaltet` | `m-zeiten-aus`, `m-zeiten-aktiv`, `m-zeiten-abo-ende`, `m-zeiten-abgeschaltet` | Ablauf ohne Haken; Z2 ohne Behalten-Knopf; Z3 |
|
||
| `news-einschalten`, `news-editor` | `m-news-aus`, `betrieb-wartung` | Platz des Ablaufs; Editor mit Zettelvorschau in den drei Zettelfarben |
|
||
| `analyse-aktivieren`, `analyse`, `analyse-heatmap` | `m-analyse-aus`, `m-analyse-aktiv`, `m-analyse-aktiv` | Linkzeile und ein Häkchen; Übersicht (Quelle des Bildes für P3) mit Diagramm schwarz auf Weiß und Etage mit drei Reitern (Handy und Desktop „Übersicht“, „Heatmap“, „Einstellungen“), keine Leiste im Inhalt; Heatmap (`schritte`: Tipp auf den Etagen-Reiter „Heatmap“): Reiter aktiv, Geräterahmen |
|
||
|
||
Jede Ansicht prüft zusätzlich: `getComputedStyle(document.body).backgroundColor` ist `rgb(255, 255, 255)`, im Modul-Inhalt kein Element mit `role="tablist"` (auch nicht in der Analyse), Tippflächen der Knöpfe am Handy mindestens 44 px hoch.
|
||
|
||
- [ ] **Step 1: Fälle, Ansichten und Gate anlegen**
|
||
|
||
`fixtures/module.mjs` liefert die 17 Fälle `m-*` (aus `betrieb-wartung` abgeleitet, nur der Modul-Endpunkt und `GET /api/funktionen` mit `modell: 'abo'` ändern sich): die 16 der Tabelle und `m-analyse-aktiv` (Analyse `active` im Projekt, mit Besucher- und Heatmap-Daten: Statistik über 30 Tage mit Reihe, Seiten, Herkunft, Ländern, Geräten und Kontakten in der Form von `stats()` in `test/analyseCards.test.js:17-40`). Die Namen werden an `FALL_NAMEN` angehängt (17 hinter den vierzehn von P0), weil `views.test` aus P0 `FALL_NAMEN.includes(fall)` verlangt; `fixtures/faelle.mjs` und `fixtures/index.mjs` bekommen dafür je einen Import. `fixtures/endpunkte-p4.mjs` legt `analyse.stats` (`GET /api/funktionen/analyse/stats`, Antwort aus `daten` des Falls) und `analyse.heatmap-link` (`POST /api/funktionen/analyse/heatmap-link`, `server/routes/funktionen.js:519` und `:540`) an; `heatmap-link` antwortet mit `{ url: '/vorschau/demo-baeckerei.html?__wk_heat=x&__wk_embed=1' }` (same-origin, sonst meldet das Gate `external-request`). In `vertrag-endpunkte.mjs` stehen beide als `NICHT_PRUEFBAR` mit dem Grund „Analytics-Dienst läuft nicht über die Testnachbauten“, wie bei P3. `views.mjs` bekommt die 20 Ansichten, `gates.mjs` das Gate `p4`. Ausgangslage: `printf 'export WT=/home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui\n' > /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/strang-ui.env`, falls P1 sie nicht schon angelegt hat. Keine eigene Browser-Instanz.
|
||
|
||
Danach die Werkzeug-Tests, ohne dass Pin oder `boot.js` angefasst wurden: `cd /root/reports/kundenansicht-neu-2026-10-15/werkzeug && timeout 900 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/backend.test.mjs test/vertrag.test.mjs test/gates.test.mjs test/vorschau-bau.test.mjs` Expected: PASS. Schlägt der Pin „Fälle in fester Reihenfolge“ oder der `boot.js`-Test („alle vierzehn Fälle genannt“) nur wegen der angehängten Namen fehl: nicht selbst ändern, stoppen und VF5 dem Controller melden.
|
||
|
||
- [ ] **Step 2: Erster Lauf**
|
||
|
||
Run: `free -m && cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && KN_STRANG_ENV=/root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/strang-ui.env /root/reports/kundenansicht-neu-2026-10-15/werkzeug/shots.sh /root/reports/kundenansicht-neu-2026-10-15/werkzeug/tmp/shots-p4 --gate p4`
|
||
Expected: „available“ über 2000 MB; der Lauf listet Befunde (`dark-surface`, `low-contrast`, `overflow-x`, `lila`, `unknown-api`, `step-failed`), solange `module.css` nicht fertig geprüft ist. Danach die Aufnahmen für Handy und Desktop mit dem Read-Werkzeug ansehen: kein Lila oder Türkis, Haarlinien statt Kästen, Reiterzeile in der Etage.
|
||
|
||
- [ ] **Step 3: Befunde beheben**
|
||
|
||
CSS-Befunde in `public/neu/module.css` (Abschnitt der Klasse), Fixture-Befunde im Werkzeug. Danach `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/kundenansichtNeuModulCss.test.js` Expected: PASS.
|
||
|
||
- [ ] **Step 4: Gate erneut laufen lassen**
|
||
|
||
Run: wie Step 2.
|
||
Expected: `0 Befunde`, Exit 0, für alle 20 Ansichten und alle Geräte.
|
||
|
||
- [ ] **Step 5: Bild der Analyse neu erzeugen (P3 VF2)**
|
||
|
||
`erzeuge.mjs analyse` nimmt die Ansicht `kn-modul-analyse` mit dem Fall `m-analyse-aktiv` (Step 1).
|
||
|
||
Run: `bash /root/reports/kundenansicht-neu-2026-10-15/werkzeug/run-browser.sh /root/reports/kundenansicht-neu-2026-10-15/werkzeug/bilder/erzeuge.mjs --ziel /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui/public/neu/bilder analyse && ls -l /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui/public/neu/bilder/analyse.webp`
|
||
Expected: Die Datei liegt da, kleiner als 122880 Byte (960×600 prüft P3s `test/knFunktionenStatik.test.js`: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/knFunktionenStatik.test.js` PASS). Weicht die Form des Skripts im P3-Plan ab, gilt die Form dort.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug add views.mjs gates.mjs fixtures/module.mjs fixtures/endpunkte-p4.mjs fixtures/endpunkte.mjs fixtures/faelle.mjs fixtures/index.mjs vertrag-endpunkte.mjs && git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug commit -m "Ansichten, Fälle und Gate p4 der Funktionsmodule"
|
||
cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git add public/neu/module.css public/neu/bilder/analyse.webp && git commit -m "module.css: Korrekturen aus der Sichtprüfung, Bild der Analyse in neuer Optik" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||
```
|
||
Gab es keine CSS-Befunde, steht im zweiten Commit nur das Bild.
|
||
|
||
### Task 15: Selbstprüfung
|
||
|
||
**Files:** keine Änderungen.
|
||
|
||
**Interfaces:** Consumes alle Ergebnisse aus Task 1–14. Produces den Tag `kn-p4`.
|
||
|
||
- [ ] **Step 1: Speicher prüfen**
|
||
|
||
Run: `free -m`
|
||
Expected: „available“ mindestens 2000 MB, sonst warten und dem Controller melden.
|
||
|
||
- [ ] **Step 2: Alle neuen Testdateien**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 1800 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/kundenansichtNeuModulReiter.test.js test/kundenansichtNeuModulEinschalten.test.js test/kundenansichtNeuModulTermine.test.js test/kundenansichtNeuModulNews.test.js test/kundenansichtNeuModulShop.test.js test/kundenansichtNeuModulBuchungen.test.js test/kundenansichtNeuModulZeiten.test.js test/kundenansichtNeuModulAnalyse.test.js test/kundenansichtNeuModulCss.test.js`
|
||
Expected: alle Tests PASS, 0 fail.
|
||
|
||
- [ ] **Step 3: Bestehende Modul-Tests**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && timeout 2400 nice -n 10 node --max-old-space-size=1024 --test --test-concurrency=1 test/termineView.test.js test/termineFragebogenView.test.js test/funktionBestellenKey.test.js test/shopView.test.js test/shopWiring.test.js test/buchungenUi.test.js test/unterkunftWiring.test.js test/zeitenView.test.js test/zeitenBestellen.test.js test/zeitenEditor.test.js test/funktionenTiles.test.js test/funktionenZeitenTile.test.js test/funktionenRecht.test.js test/analyseView.test.js test/avvPage.test.js`
|
||
Expected: alle PASS, 0 fail. Die Pins `shopWiring.test.js:31` und `unterkunftWiring.test.js:130` sind die einzigen geänderten Zeilen bestehender Tests.
|
||
|
||
- [ ] **Step 4: Verträge abhaken**
|
||
|
||
- [ ] V2: `initNews` und `initAnalyse` lesen `root.dataset.wunschProjekt` (Task 4, 8); die Module behalten ihre Klassen (Task 9–13, Abdeckungstest).
|
||
- [ ] V5: Shop-Anfrage ohne `unternehmer`, mit `keineAusgeschlossenen: true` (Task 5); Terminplaner ohne Häkchen `gesundheit`, Einschalten statt Checkout (Task 3); Analyse über `analyse/activate` (Task 8).
|
||
- [ ] P1 Vertragsfrage 5: Reiter von Terminplaner, Webshop, Buchungskalender als `kn:reiter` mit `kurz`, leere Liste ohne Reiter (Task 1, 3, 5); Reiter der Analyse ebenso, ohne eigene Leiste (Task 8, Gate `p4` ohne Ausnahme).
|
||
- [ ] Global Constraints: keine Preise (`KEIN_PREIS` in Task 2 und 4–8), Du-Form (Pins der Bestandstests), Medienabfrage und 44px (Task 9), Palette (Task 9).
|
||
- [ ] Vertragsfragen VF1–VF6 stehen dem Controller zur Entscheidung; die Annahmen sind in den Tasks umgesetzt.
|
||
|
||
- [ ] **Step 5: Alte Ansicht und Dateiliste prüfen**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git diff --stat kn-p3 HEAD -- public/dashboard.html public/app.js public/login.html public/termine.css public/shop.css public/buchungen.css public/zeiten.css public/funktionen.css public/analyse.css public/geraete.css public/kundenbereich.css`
|
||
Expected: leere Ausgabe.
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git diff --name-only kn-p3 HEAD | sort`
|
||
Expected genau diese 26 Zeilen: `public/analyse.js`, `public/buchungen-anfrage.js`, `public/buchungen.js`, `public/fn-reiter.js`, `public/funktion-bestellen.js`, `public/funktionen.js`, `public/neu/bilder/analyse.webp`, `public/neu/module.css`, `public/shop-anfrage.js`, `public/shop.js`, `public/termine-fragebogen.js`, `public/termine.js`, `public/zeiten-bestellen.js`, `public/zeiten.js`, `test/kundenansichtNeuModulAnalyse.test.js`, `test/kundenansichtNeuModulBuchungen.test.js`, `test/kundenansichtNeuModulCss.test.js`, `test/kundenansichtNeuModulEinschalten.test.js`, `test/kundenansichtNeuModulNews.test.js`, `test/kundenansichtNeuModulReiter.test.js`, `test/kundenansichtNeuModulShop.test.js`, `test/kundenansichtNeuModulTermine.test.js`, `test/kundenansichtNeuModulZeiten.test.js`, `test/shopWiring.test.js`, `test/support/modulAbo.js`, `test/unterkunftWiring.test.js`.
|
||
|
||
- [ ] **Step 6: Einbindung, Sauberkeit, Tag**
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && command grep -a -n "module.css" public/neu/dashboard.html; command grep -a -n "einschalten" public/neu/huelle.js; git status --short; ls -l /dev/null`
|
||
Expected: eine Zeile mit `<link rel="stylesheet" href="/neu/module.css">`; mindestens eine Zeile in `huelle.js`, die `einschalten` an die Module übergibt (fehlt sie: nicht selbst ändern, VF1 melden); `git status --short` leer; `/dev/null` zeigt `crw-rw-rw-`.
|
||
|
||
Run: `cd /home/webklar/apps/kundenbereich.webklar.com/dev-kn-ui && git tag kn-p4 && git -C /root/reports/kundenansicht-neu-2026-10-15/werkzeug tag kn-p4`
|
||
Expected: kein Fehler; `git tag --list 'kn-p*'` nennt `kn-p4`.
|
||
|
||
## Vertragsfragen an den Controller
|
||
|
||
**VF1 Einschalt-Ablauf (P1, P3, P4).** P3 liefert `zeigeEinschalten(platz, { key, projectId, onFertig, daten?, viewAs?, … })` (P3 Task 4) und erwartet die Übergabe als Init-Option `einschalten` an `initTermine`, `initNews`, `initShop`, `initBuchungen`, `initZeiten`; P4 nutzt genau diese Form (`einschalten(platz, { key, projectId, viewAs, onFertig })`). P1 Task 11 ruft die Module bisher nur als `init*(wurzel)` auf. Vorschlag wie in P3 VF1: P1 ergänzt in `huelle.js` `init*(wurzel, { einschalten: (platz, o) => import('/neu/funktion-einschalten.js').then((m) => m.zeigeEinschalten(platz, o)) })`. Ohne das zeigen die Module im Abo-Modell die Fehlerkarte E4 (Task 15 prüft es). Grund für die Übergabe statt `import()` in den Modulen: Pins verbieten dort neue Importe und `import(` (Datei:Zeile in den Global Constraints), und die gemeinsamen Module sollen keine `/neu/`-Datei kennen.
|
||
|
||
**VF2 Reiter (P1 und P4).** P4 setzt P1s Vertragsfrage 5 um: Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und Analyse keine eigene Leiste, sondern melden `kn:reiter` (leere Liste ohne Reiter). Spec 5.4 verlangt die gemeinsame Mechanik „statt der vier eigenen Kopien“, Spec 5.2 und C2 verlangen Unterseiten-Reiter immer unten in Etage 2; eine Analyse mit Reitern im Inhalt und nur „Zurück“ in der Etage würde das Muster „unten Ebene, oben Ansicht“ brechen. Die Analyse meldet deshalb aus der Closure `mountCockpit` (`bei: pickTab`), und der Test läuft mit den Attrappen von `htmlWurzel`, die Statistik-Anfrage antwortet 503 (Task 8). Die frühere Ausnahme „die Analyse behält ihre Leiste“ ist gestrichen, ebenso „außer `analyse`“ im Gate `p4`. `kn:titel` sendet P4 nicht (der Titel kommt aus der Routentabelle von P1). Die Kurzlabels (Team, Start, Recht, Preise) sind Vorschläge; die Analyse braucht keine (Übersicht, Heatmap, Einstellungen); die Vorschau prüft sie (C2).
|
||
|
||
**VF3 Alt-Klassen.** `kb-panel`, `kb-btn`, `kb-btn-primary`, `kb-btn-outline`, `kb-empty`, `kb-link`, `kb-checks`, `abo-actions`, `abo-btn`, `hidden`, `muted`, `error` stehen heute in `kundenbereich.css` und `login.css`, die die neue Seite nicht lädt; in den Plänen von P0 bis P3 definiert sie niemand. Vorschlag: `module.css` Abschnitt 0 definiert sie, außer eine Datei von `public/neu/` hat es schon getan (Task 9 prüft per `grep`; der Prüftest akzeptiert jede Datei dort). P2 prüft für seine 1:1-Seiten, ob es dieselben Klassen nutzt, und stützt sich dann auf Abschnitt 0.
|
||
|
||
**VF4 Analyse-Aktivierung im Abo-Modell (P5).** P4 sendet im Abo-Modell zusätzlich `audienceConfirmed: true` (P5 Vertragsfrage 8 plant dafür den Fehlercode `zielgruppe_fehlt`); der heutige Server ignoriert das Feld. Der Nachweis in `featureConsents` (Zeilen A1 und A2, Knopftext `Analyse einschalten`, Kundenart) bleibt Sache von P5.
|
||
|
||
**VF5 Werkzeug (P0, P3, P4).** Task 14 nimmt die Form aus P0, P1 und P3 an (`views.mjs`, `gates.mjs` mit `GATES.p4`, `shots.sh … --gate p4`, `erzeuge.mjs`) und ergänzt 17 Fälle `m-*` (16 Modul-Fälle und `m-analyse-aktiv`) in einer neuen Datei `fixtures/module.mjs`, eingetragen in `fixtures/faelle.mjs` und `fixtures/index.mjs` (Dateien von P0, im Werkzeug-Git), dazu die Analyse-Endpunkte in `fixtures/endpunkte-p4.mjs` (Konvention aus P0 Vertragsfrage 5). Außerdem erzeugt P4 `public/neu/bilder/analyse.webp` neu (P3 VF2; sonst Besitz von P3).
|
||
**Regel: Pin und `boot.js` bleiben unberührt.** P4 ändert weder den Pin „Fälle in fester Reihenfolge“ in `KW/test/backend.test.mjs` noch `KW/vorschau/boot.js`. Das setzt voraus, dass P0 (und P7, das ebenfalls Fälle anhängt) den Pin auf die vierzehn Basisnamen begrenzt und `boot.js` die Fallliste nicht fest einträgt; sonst schlagen `backend.test.mjs` und `vorschau-bau.test.mjs` nach dem Anhängen der 17 Namen fehl, und Task 14 Step 1 stoppt und meldet es. Bitte alles bestätigen und die Regel in P0 und P7 gleich ziehen.
|
||
|
||
**VF6 Hinweis für P10.** Der Abdeckungstest `kundenansichtNeuModulCss.test.js` liest die alten CSS-Dateien als Klassenquelle. Löscht P10 sie, ersetzt es die Quelle durch den Stand der Klassenliste (Ausgabe von `klassen(...)` je Präfix, einmal als `test/support/modulKlassen.json` abgelegt) und passt `FN_NEWS` an. Die Alt-Zweige der Module (Bestellseiten, „Behalten“, lange Reiterleisten) entfernt P10 erst nach der Umschaltung.
|