Files
tickte-system/docs/superpowers/plans/2026-10-05-weisses-design-p6-kunden-unterseiten.md
T

612 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WOMS „Weißes Design“ P6: Kunden-Unterseiten – Umsetzungsplan
> **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:** Chat, Gutschriften, Abo-Angebote und E-Mail-Speicher sind hell und kastenlos. Die Chat-Liste besteht aus Haarlinien-Zeilen, die drei Listen sind Tabellen mit Spaltenpriorität, das Angebots-Formular ist ein Dialog. Der Chat wechselt bei 860 px zwischen Liste und Verlauf. `admin-areas.css` ist am Ende gelöscht, weil P4 die Kundenseite davon gelöst hat und P6 die letzten Nutzer umbaut.
**Architecture:**
- `chat.css` wird neu geschrieben. Jede Regel für Elemente im Chat-Container trägt das Präfix `.woms-chat` und ist strikt spezifischer als die Fremdregel des Portal-Bausteins. Das Seitenlayout (`.chat-page` usw.) bleibt ohne Präfix.
- Die Listen nutzen `Tabs`, `.table` mit `.hide-md`/`.hide-sm`, `Badge`, `EmptyState`. `CreditDetail` wird `.two-col` mit `Card`-Abschnitten und `.kv`. `OfferForm` wird ein `Dialog`.
- Neue reine Helfer liegen in `src/lib` und sind per Test gesichert. Das Screenshot-Werkzeug bekommt den echten Portal-Chat-Baustein, damit die Chat-Regeln gegen das Fremd-CSS geprüft werden.
**Tech Stack:** React 18, react-router-dom 6, reines CSS, `node --test`, Prüf- und Screenshot-Werkzeug aus P0 (`env.sh`, `check.sh`, `shots.sh`).
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“, „Übergangsschicht KOMPAT“, Paket P6, „Seiten ohne Prototyp-Vorlage“, „Texte“, „Was gleich bleibt“, Risiken (Chat-Composer, Vorschlagslisten). Anhang `…-umbaukarte.md`: Abschnitt 5 (P6), 7.2, 8.1 Punkte 13, 14, 19. Globale Regeln: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht, besonders „Befehle und Prüfungen“, „CSS-Reihenfolge“, „Parallele Arbeit an WOMS“, „Paketstart und Abnahme-Reihenfolge“ und „Bausteine benutzen“. Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Für P6 kommt hinzu:
- **Voraussetzung:** Tag `design-p5` ist gesetzt. P4 hat `CustomerFunktionen`, `AboStatusCard` und `KundenansichtButton` umgebaut, alle `.funktionen-*`-Regeln nach `global.css` verlegt und die beiden Dateien von `admin-areas.css` gelöst (keine `.area-*`-Klasse, kein Import). Task 1 prüft das.
- **Dateien:** nur `CustomerChatPage`, `CreditsPage`, `OffersPage`, `StorageTasksPage`, `src/components/{chat,credits,offers}/*`, `src/styles/{chat,admin-areas,global}.css`, `src/lib/{credit,offer,storage}Format.js` samt Tests und `$W`. Nie anfassen: `loadChatComposer.js` samt Test, die Container in `ChatThread.jsx`, das Portal-Repo (nur Lesezugriff für den Snapshot in Task 2), die P4-Dateien.
- **Ladereihenfolge:** `main.jsx` importiert `App` vor `global.css`, also steht `chat.css` im Bundle vor `global.css`, und das Portal-CSS lädt zur Laufzeit zuletzt. Gleiche Spezifität verliert immer: `.btn` aus `global.css` überstimmt ein `.chat-back { display: none }`. Seitenregeln für Elemente mit globaler Klasse brauchen höhere Spezifität (`.chat-page .chat-back`), nie `!important`.
- **Texte:** Erlaubt sind nur „← Alle Unterhaltungen“ (Pfeil statt Icon), „Speichert …“ und „Wird geladen …“ (statt Spinner). Alles andere bleibt wörtlich, auch Spaltenköpfe, Hinweisabsätze und alle `confirm`/`alert`-Texte.
- **Hygiene und Inventur:** In den zehn P6-Dateien gibt es danach kein `react-icons`, kein `size="sm"`, kein `style={{` (heute 26). Treffer je Datei für `window.confirm`, `window.alert`, `busyRef`, `data-storage-done`, `id="calc-*"`, `id="credit-*"` bleiben: `CreditDetail.jsx` 16 (1 confirm, 9 busyRef, 6 IDs), `OfferList.jsx` 4, `OfferForm.jsx` 1, `StorageTasksPage.jsx` 3.
**Entscheidungen** (die Spec ist dazu still):
- **Entscheidung:** Die Prüf-Zeichen `✓`, `!`, `i` der „Automatischen Prüfung“ bleiben als Badge-Text. Die Umbaukarte sagt „ohne Glyphen“, aber die Spec „Texte“ erlaubt keine Textänderung dafür, und sonst trüge nur die Farbe den Status.
- **Entscheidung:** Die Filter sind `Tabs` ohne Zähler (die Spec nennt keine, es gibt keine neuen Abfragen).
- **Entscheidung:** Tabellenzeilen sind nicht klickbar (heute auch nicht), kein `tr.row` und damit kein `isRowActivation` (`src/lib/rowClick.js` aus P2). Aktionen stehen als `button.link` bzw. `.link-danger` in der letzten Zelle. Die offene Gutschrift trägt `aria-current="true"`.
- **Entscheidung:** Spaltenpriorität (immer / `hide-sm` / `hide-md`):
- Gutschriften: Kunde, Status, Aktion / Eingang, Rechnung / Leistung, Bezahlt bis.
- Angebote: Kunde, Angebot, Status mit Aktionen / Gültig bis / Angelegt.
- Speicher: Kunde, Aufgabe, Aktion / Fällig / Bestellt.
- **Entscheidung:** Der Chat-Zähler ist eine kleine Zahl in `--accent`, fett (wie `.nav-count.hot`), keine Pille. Die aktive Zeile hat `aria-current="true"` und `--accent-soft`.
- **Entscheidung:** Beim Portal-Composer bleibt die Geometrie (46 px, runde Knöpfe). Überschrieben werden Farben, Ring statt Rand, Hover, Fokus, Deaktiviert und unter 640 px wieder `font-size: 16px` im Eingabefeld (das heutige `font: inherit` hebt die Portal-Regel gegen das iOS-Zoomen auf). Der Datei-Chip ist weiß mit Ring, weil die Blase selbst `--bg-hover` ist. Die Anhang-Menü-Icons bleiben.
- **Entscheidung:** Lightbox und Kamera bleiben unberührt dunkel (am `body`, vom Präfix nicht erreichbar). Der Kamera-Knopf „Foto verwenden“ bleibt im Portal-Lila und geht als „bewusst offen“ in die Übergabe.
- **Entscheidung:** `OfferForm` bleibt ohne `<form>` (also kein `Dialog as="form"`, kein Enter-Submit, wie heute). Beide Fußknöpfe tragen trotzdem `type="button"`. „Abbrechen“ ist beim Speichern gesperrt, passend zu „ד und Esc.
- **Entscheidung:** `admin-areas.css` wird in P6 gelöscht. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt und `CustomerFunktionen` und `AboStatusCard` von der Datei gelöst. Die P6-Seiten verlieren ihre `.area-*`-, `.credit-*`-, `.offer-*`- und `.storage-row`-Klassen und den Import. Danach hat die Datei keinen Nutzer mehr (Task 11).
- **Entscheidung:** Kleine Ergänzungen stehen in einem Block „Kunden-Unterseiten (P6)“ am Ende von `global.css`. `table-wrap` gilt nur für die drei P6-Tabellen, damit P2 bis P5 unberührt bleiben.
## Review Focus
Aus der Übersicht gehören zu P6:
- **Punkt 2, lange Inhalte am Handy:** Kundenname mit 60 Zeichen in allen Listen, E-Mail mit 50 Zeichen ohne Leerzeichen, URL mit 80 Zeichen im Chat, Angebotstext und interne Notiz je 200 Zeichen. `overflow-x` bei 390 px in allen P6-Ansichten.
- **Punkt 3, Teilausfall:** Das Szenario `fehler-portal` lässt alle `/api/team/*`-Anfragen mit 500 antworten. Die vier Seiten zeigen `.msg.err` und bleiben bedienbar, „Erneut versuchen“ lädt neu.
- **Punkt 4, nur Tastatur:** Tabellenaktion per Tab erreichbar. Esc schließt erst die Vorschlagsliste, dann den Dialog. Tab bleibt im Dialog, „ד und Esc sind beim Speichern gesperrt. Doppelklick auf „Genehmigen & gutschreiben“ löst einen POST aus, der 300-ms-Debounce einen Berechnungs-POST.
- **Punkt 5, viele Einträge:** `chat-viel` (150 Unterhaltungen, die Liste scrollt in sich) und `gutschriften-viel` (120 Anträge).
Dazu gilt für P6:
- **Fremd-CSS gewinnt nicht:** `chat-verlauf` liest berechnete Stile und vergleicht sie mit Sollwerten (Task 4). `.chat-back` ist bei 1440 px unsichtbar, bei 390 px sichtbar.
- **Vorschlagsliste im Dialog** ist vollständig erreichbar, der längste Speichern-Text passt bei 390 px in die Fußzeile.
- **Zwischenstufe 1000 px:** Die Ansichten `*-tablet` zeigen die Spaltenpriorität.
---
### Task 1: Ausgangslage und Lib-Helfer
**Files:**
- Modify: `src/lib/creditFormat.js`, `src/lib/offerFormat.js`, `src/lib/storageFormat.js` (neue Exporte, Stand vor P6)
- Test (neu): `tests/unit/creditFormat.test.mjs`, `tests/unit/offerFormat.test.mjs`, `tests/unit/storageFormat.test.mjs`
**Interfaces:**
- Consumes: `fmtDate`, `fmtDateTime`, `formatCents` aus `src/lib/format.js`, `isOverdue`.
- Produces:
- `creditActionLabel(status) → 'Prüfen' | 'Ansehen'`
- `creditStatusNote(r) → string`
- `checkGlyph(status) → '✓' | '!' | 'i'`
- `checkTone(status) → 'ok' | 'warn' | 'info' | 'muted'`
- `filterOffers(offers, filter) → offers[]`
- `offerRowActions(o) → { sendLabel: '' | 'E-Mail senden' | 'Erneut senden', canWithdraw: boolean }`
- `offerMailLine(o) → null | { tone: 'muted' | 'warn', text }`
- `storageDue(t, now) → { tone, label, sub, subMuted }`
- [ ] **Step 1: Paketstart und Ausgangslage prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git status --short && git fetch origin && git merge-base --is-ancestor design-p5 HEAD && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `git status --short` zeigt keine geänderten Dateien, `# fail 0`. Die Testzahl wird als N5 notiert. Schlägt `merge-base` fehl, fehlt `design-p5` im Branch: stoppen und melden.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/CustomerChatPage.jsx src/pages/CreditsPage.jsx src/pages/OffersPage.jsx src/pages/StorageTasksPage.jsx src/components/chat src/components/credits src/components/offers src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js src/styles/chat.css src/styles/admin-areas.css src/styles/global.css`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, TP2 zuerst nach `test` zu bringen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|ui-btn|FaSpinner" src/components/customer/KundenansichtButton.jsx src/components/customer/AboStatusCard.jsx src/components/customer/CustomerFunktionen.jsx; rg -n "admin-areas|\barea-" src/components/customer; rg -n "funktionen-" src/styles/admin-areas.css`
Expected: keine Treffer. Ein Treffer heißt, dass P4 `CustomerFunktionen` oder `AboStatusCard` nicht vollständig von `admin-areas.css` gelöst hat: stoppen und melden, die P4-Dateien werden in P6 nicht umgebaut.
- [ ] **Step 2: Failing Tests schreiben** (Leerzeichen in Beträgen ist U+00A0)
`creditFormat.test.mjs` (4 Tests):
- `creditStatus` pinnt die Tonnamen: `offen` → `{ tone: 'warn', label: 'Zu prüfen' }`, `rueckfrage` → `muted`/„Rückfrage beim Kunden“, `genehmigt` → `ok`/„Genehmigt“, `abgelehnt` → `danger`/„Abgelehnt“, `'neu'` → `muted`/`'neu'`.
- `creditActionLabel`: `offen`, `rueckfrage` → `'Prüfen'`, sonst `'Ansehen'`.
- `creditStatusNote`: `{ status: 'genehmigt', amountCents: 1250, appliedAt: '2026-10-01T10:00:00Z' }` → `'12,50 €'`, ohne `appliedAt` → `'12,50 € · nicht gebucht'`, `{ status: 'offen', amountCents: 1250 }` → `''`.
- `checkGlyph`/`checkTone`: `ok` → `'✓'`/`'ok'`, `warn` → `'!'`/`'warn'`, `info` → `'i'`/`'info'`, `undefined` → `'i'`/`'muted'`.
`offerFormat.test.mjs` (4 Tests):
- `offerStatus`: `offen` → `warn`/„offen“, `aktiv` → `ok`/„aktiv“, `abgelaufen` → `muted`/„abgelaufen“, `zurueckgezogen` → `muted`/„zurückgezogen“.
- `filterOffers` mit Status `offen, aktiv, abgelaufen, zurueckgezogen` (ids `1` bis `4`): `'open'` → `['1','2']`, `'all'` → alle, `null` → `[]`, Eingabe unverändert.
- `offerRowActions`: `{ status: 'offen', mailSendable: true }` → `{ sendLabel: 'E-Mail senden', canWithdraw: true }`, mit `notifiedAt` → `'Erneut senden'`, `{ status: 'aktiv', mailSendable: true }` → `canWithdraw: false`, `{ status: 'abgelaufen', mailSendable: false }` → `{ sendLabel: '', canWithdraw: true }`, `{ status: 'zurueckgezogen' }` → `{ sendLabel: '', canWithdraw: false }`.
- `offerMailLine`: `{ notifiedAt: '2026-09-30T10:05:00.000Z' }` → `{ tone: 'muted', text: 'E-Mail gesendet 30.09.26, 12:05' }`, `{ mailSendable: true }` → `{ tone: 'warn', text: 'E-Mail nicht verschickt' }`, `{ status: 'offen', customerEmail: '' }` → `{ tone: 'warn', text: 'Keine E-Mail-Adresse hinterlegt' }`, `{ status: 'abgelaufen', customerEmail: '' }` und `{ status: 'offen', customerEmail: 'a@b.de' }` → `null`.
`storageFormat.test.mjs` (2 Tests, `t0 = Date.parse('2026-10-05T08:00:00Z')`):
- `storageDue`: `{ kind: 'einrichten', createdAt: '2026-10-01T08:30:00.000Z' }` → `{ tone: 'warn', label: 'jetzt einrichten', sub: 'bestellt 01.10.26, 10:30', subMuted: true }`. `{ kind: 'senken', dueAt: '2026-10-04T00:00:00.000Z' }` → `{ tone: 'warn', label: 'zum Periodenende', sub: '04.10.2026', subMuted: false }`, mit `dueAt: '2026-10-20T00:00:00.000Z'` → `tone: 'muted'`, `sub: '20.10.2026'`.
- `isOverdue`: vergangenes `dueAt` → `true`, zukünftiges oder fehlendes → `false`.
- [ ] **Step 3: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs`
Expected: FAIL (`… is not a function`).
- [ ] **Step 4: Implementieren**
Die Regeln sind die heutigen Inline-Ausdrücke, herausgezogen: `creditActionLabel` aus `CreditList.jsx` Z. 52, `creditStatusNote` Z. 48, `checkGlyph` und `checkTone` aus `CreditDetail.jsx` Z. 11 und 211, `filterOffers` aus `OfferList.jsx` Z. 54, `offerRowActions` und `offerMailLine` Z. 93 bis 109 (Reihenfolge der Mail-Zeile: `notifiedAt`, `mailSendable`, `offen` ohne Adresse, sonst `null`), `storageDue` aus `StorageTasksPage.jsx` Z. 57 bis 59. Zeichenketten bleiben wörtlich.
- [ ] **Step 5: Tests grün und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests`
Expected: Exit 0, `# pass` = N5 + 10, `# fail 0`.
```bash
git add src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs
git commit -m "Design P6: reine Helfer fuer Gutschrift-, Angebots- und Speicherzeilen mit Tests" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Werkzeug: echter Chat-Baustein, Fixtures, Ansichten, Gate p6
**Files** (unter `$W/`, außerhalb des Repos, kein Commit): Create `fixtures/portal/{chat-composer.css,chat-composer.js,README.md}`. Modify `backend.mjs`, `fixtures/standard.mjs`, `checks.mjs`, `views.mjs`, `gates.mjs`.
**Interfaces:**
- Consumes: `createBackend`, `VIEWS`, `GATES`, `runChecks` aus P0, Gate `p5`. `ctx.backend.calls` (Liste `{ method, url, body }` aller beantworteten Anfragen).
- Produces: Szenario `fehler-portal`, die Ansichten unten, Gate `p6`.
- [ ] **Step 1: Portal-Baustein als Snapshot**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cp /home/webklar/apps/kundenbereich.webklar.com/build/public/chat-composer.{css,js} "$W/fixtures/portal/" && sha256sum "$W"/fixtures/portal/chat-composer.*`
Expected: css `b34fbaeb72922ddfbd57f1074e58e58dffc560cd1e9a31c98b12e66487885647`, js `ebb79581f9b307b303877f169fa5546bb98b4fc66320c40ae8fd78c965bace23`. Die README nennt Quelle, Datum 2026-10-05 und beide Hashes.
- [ ] **Step 2: Backend erweitern**
- `https://portal.test/chat-composer.css` und `.js` liefern den Snapshot (statt der leeren Dateien aus P0).
- Anhang-Adressen unter `https://portal.test/api/team/files/*` liefern ein 1×1-PNG bzw. 200 ohne Inhalt.
- `POST …/chats/:id/attachments` antwortet im Szenario `fehler-portal` und in `chat-anhang-fehler` mit 500.
- `POST …/offers` und `POST …/takeover-credits/:id/decision` antworten nach 800 ms.
- Szenario `fehler-portal`: jede Anfrage an `https://portal.test/api/team/*` bekommt 500 im Fehlerformat des Szenarios `fehler`. Deren Konsolenmeldungen gelten als absichtlich.
- [ ] **Step 3: Fixtures ergänzen** (Schlüssel nach dem Muster der vorhandenen, Formen laut Lesecode in den `*Format.js` und JSX-Dateien)
| Bereich | Fälle |
|---|---|
| Chats | 8 Unterhaltungen, 3 mit `unreadFromCustomer` (1, 12, 120), ein 60-Zeichen-Name, ein `lastText` mit 80-Zeichen-URL. Satz `viel` mit 150. |
| Verlauf Kunde A | 7 Nachrichten: Kundentext, Teamtext (`senderName` „Kenso Grimm“), Bild (`kind: 'image'`, 640×480), PDF (`Angebot-Kuehn-2026.pdf`, 183000 Byte), Sprachnachricht (`kind: 'voice'`, `durationMs: 23000`), Teamtext mit 80-Zeichen-URL, langer Kundentext. |
| Gutschriften | 5 Anträge (`offen`, `rueckfrage`, `genehmigt` gebucht, `genehmigt` ungebucht, `abgelehnt`), einer mit 60-Zeichen-Name und langem `services`/`provider`/`domain`. Satz `viel` mit 120. `getCredit` für einen offenen und einen ungebuchten genehmigten Antrag: 3 `requiredChecks`, `autoChecks` mit `ok`/`warn`/`info` (mit `detail`), `calc`, laufendes Abo, 1 weiterer Antrag, 2 Dateien, 3 Verlaufseinträge. `POST …/calc` → `{ calc }`. |
| Angebote | 6 Angebote: `offen` mit `mailSendable` ohne `notifiedAt`, `offen` ohne E-Mail, `aktiv` mit `scope: 'laufend'`, `abgelaufen` mit `mailSendable` und `notifiedAt`, `zurueckgezogen`, eins mit 60-Zeichen-Name, 50-Zeichen-E-Mail und 200-Zeichen-`internalNote`. `getOfferContext` für einen Kunden ohne Abo und einen mit laufendem Wartungs-Abo (`upcomingCharges`). |
| Speicher | `tasks`: `einrichten`, `senken` überfällig, `senken` später, einer `partial` mit `error`, einer mit 60-Zeichen-Name. `failed` mit 2 Einträgen, `allocatedGb` über `warnGb`. |
- [ ] **Step 4: Prüf-Ausnahmen erweitern**
- In `checks.mjs`: `dark-surface` nimmt zusätzlich `.woms-chat :is(.chat-send, .chat-audio-play, .chat-file-ext, .chat-tray-remove, .chat-tray-retry, .chat-tray-progress)` aus. Der Szenario-Name `fehler-portal` gilt für `console-error` wie `fehler` (erwartete 500-Meldungen ausgenommen).
- `icon-svg` ist dauerhaft für alles unter `.woms-chat` ausgenommen (Sprechblasen mit Anhängen zeichnet derselbe fremde Baustein). Das steht im Gate `p6` (Step 5) als `ignore`, nicht in `checks.mjs`.
- [ ] **Step 5: Ansichten und Gate eintragen**
`views.mjs` bekommt die Hilfe `tablet(page)`: ab 800 px Breite `setViewportSize({ width: 1000, height: 800 })`, sonst nichts. Die Schritte haben die Signatur `steps(page, ctx)`, Anfragen liest man aus `ctx.backend.calls`. Prüfungen in Schritten nutzen `assert` aus `node:assert/strict`. Eine Ausnahme erzeugt den Verstoß `step-failed` mit Ansicht und Meldung (das Gate endet mit Exit 1). Die Kundenseiten-Ansichten tragen die IDs der Übersicht (`kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`), es gibt keine zweite Ansicht für dieselbe Reiter-URL.
| ID | Pfad / Schritte | Prüfung im Schritt |
|---|---|---|
| `chat-verlauf` | `/customers/chat?kunde=<A>` | `.chat-back` bei 1440 `display: none`, bei 390 sichtbar. `.chat-side` bei 390 ausgeblendet. Stil-Sollwerte aus Task 4. |
| `chat-menue`, `chat-anhang-fehler` | wie `chat-verlauf`; Klick `.chat-attach-btn` bzw. `setInputFiles` (`x.pdf`) auf `.chat-composer input[type=file]:not([accept])` | `.chat-menu` sichtbar bzw. `.chat-tray-item.is-error` und `.chat-composer-note` sichtbar |
| `chat-tablet`, `gutschriften-tablet`, `angebote-tablet` | Ansicht mit `tablet` | Chat: Liste und Verlauf nebeneinander, `.chat-input-row` ohne Überlauf. Tabellen: `.hide-md`-Spalten aus, `.hide-sm`-Spalten sichtbar |
| `chat-viel`, `gutschriften-viel` | Satz `viel` | `.chat-list.scrollHeight > clientHeight` |
| `chat-fehler`, `gutschriften-fehler`, `angebote-fehler`, `speicher-fehler` | Szenario `fehler-portal` | `.msg.err` sichtbar; bei Chat und Speicher Klick auf „Erneut versuchen“, die Seite lebt |
| `gutschriften-antrag` | `/customers/gutschriften?antrag=<offen>` | – |
| `gutschriften-sperre` | wie oben; `page.on('dialog', d => d.accept())`; 3 Kästchen, `#credit-note` füllen, Doppelklick „Genehmigen & gutschreiben“; in `#calc-amount` 3 Ziffern mit 50 ms Abstand | nach 1500 ms genau 1 POST `…/decision` und 1 POST `…/calc` in `ctx.backend.calls` (`method`, `url`) |
| `angebote-dialog`, `angebote-vorschlag`, `angebote-laufend` | Klick „Neues Angebot“; im Kunden-Feld „Mu“ tippen; Kunde mit laufendem Abo wählen | `[role=dialog]` sichtbar; letzter `.autocomplete-item` per `elementFromPoint` erreichbar; bei 390 px `.dialog-foot.scrollWidth <= clientWidth` |
| `angebote-tastatur` | `page.on('dialog', d => d.accept())`. Bis zu 15 Mal Tab, bis der Fokus in `table` liegt. Dialog öffnen, Kunden-Feld tippen, Esc, Esc. Erneut öffnen, 30 Mal Tab. Kunde wählen, „Angebot anlegen“, während der 800 ms Esc | Fokus erreicht die Tabelle. Erstes Esc schließt nur die Liste, zweites den Dialog. Fokus bleibt im Dialog. Beim Speichern ist `.dialog-close` `disabled` und der Dialog nach Esc noch da |
Dazu die vorhandenen Ansichten `chat`, `gutschriften`, `angebote`, `speicher`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`. In `gates.mjs` entsteht `p6` nach dem Schema der Übersicht: das Gate `p5` plus `icon-svg` für die Ansichten von P6 und aller früheren Pakete.
```js
const P6_VIEWS = ['chat', 'chat-verlauf', 'chat-menue', 'chat-anhang-fehler', 'chat-tablet', 'chat-viel', 'chat-fehler',
'gutschriften', 'gutschriften-tablet', 'gutschriften-viel', 'gutschriften-fehler', 'gutschriften-antrag', 'gutschriften-sperre',
'angebote', 'angebote-tablet', 'angebote-fehler', 'angebote-dialog', 'angebote-vorschlag', 'angebote-laufend', 'angebote-tastatur',
'speicher', 'speicher-fehler', 'kunde-chat', 'kunde-abo', 'kunde-gutschriften', 'kunde-funktionen']
const prev = GATES.p5.scoped['icon-svg']
GATES.p6 = {
...GATES.p5,
scoped: { ...GATES.p5.scoped, 'icon-svg': { views: [...new Set([...prev.views, ...P6_VIEWS])], ignore: [...(prev.ignore ?? []), '.woms-chat'] } },
}
```
`step-failed`, `external-request` und `console-error` sind wie in jedem Gate aktiv, `dark-surface`, `low-contrast` und `overflow-x` kommen über `GATES.p5.checks` für alle Ansichten.
- [ ] **Step 6: Rot-Beweis**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t2" --only chat-verlauf,angebote --gate p6; echo "exit $?"`
Expected: `exit 1` mit `icon-svg` mindestens in `angebote` (`FaPlus`). Das Bild `chat-verlauf` zeigt den echten Composer mit Anhängen.
### Task 3: Globale Ergänzungen
**Files:**
- Modify: `src/styles/global.css` (neuer Block am Ende)
**Interfaces:**
- Produces: `.table.table-wrap td` (lange Wörter brechen), `.table tr[aria-current="true"] td`, `.dialog-foot { flex-wrap: wrap }`, `.autocomplete .input { width: 100% }` (P7 baut auf die beiden letzten).
- [ ] **Step 1: Block anlegen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "aria-current|dialog-foot|\.autocomplete \.input" src/styles/global.css`
Expected: zeigt, welche Regeln P1 bis P5 schon angelegt haben. Der Block steht am Dateiende, hinter allen anderen Regeln, damit er bei gleicher Spezifität gewinnt. Unter `/* ===== Kunden-Unterseiten (P6) ===== */` stehen nur die fehlenden:
- `.table.table-wrap td { overflow-wrap: anywhere }`
- `.table tr[aria-current="true"] td { background: var(--bg-subtle) }`
- `.dialog-foot { flex-wrap: wrap }`
- `.autocomplete .input { width: 100% }`
- [ ] **Step 2: Bauen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
Expected: Exit 0, die Bundle-Größen werden ausgegeben.
```bash
git add src/styles/global.css
git commit -m "Design P6: kleine Tabellen-, Dialog- und Autocomplete-Ergaenzungen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 4: `chat.css` neu, Fehlerzeile im Verlauf
**Files:**
- Modify: `src/styles/chat.css` (komplett neu), `src/components/chat/ChatThread.jsx` (Import Z. 2, Fehlerzeile Z. 120 bis 125)
- Test: `tests/unit/chatCss.test.mjs` (neu)
**Interfaces:**
- Consumes: Tokens der Spec (`--bg`, `--bg-hover`, `--bg-subtle`, `--grey-bg`, `--text*`, `--line*`, `--accent*`, `--red-fg`, `--danger`, `--radius*`, `--ring`, `--shadow-pop`), `.msg.err`, `Button`.
- Produces: `.woms-chat` mit `--chat-bubble-in`, `--chat-bubble-out`, `--chat-track`; Seitenklassen `.chat-page`, `.chat-side`, `.chat-list`, `.chat-list-item`, `.chat-list-top`, `.chat-list-last`, `.chat-list-time`, `.chat-unread`, `.chat-main`, `.chat-back`.
- [ ] **Step 1: Failing Test schreiben**
`chatCss.test.mjs` liest `chat.css` als Quelltext (Vorbild `adminRoute.test.mjs`), entfernt Kommentare und zerlegt per `/([^{}]+)\{/g` in Selektoren (ohne `@…`, Kommas gesplittet). 5 Tests:
1. Jeder Selektor mit `/\.chat-(bubble|meta|messages|notice|input-row|icon-btn|send|menu|tray|rec|audio|att|file|composer|drop-active)\b/` beginnt mit `.woms-chat` (`deepEqual(bad, [])`).
2. Kein `!important`.
3. `match(/@media \(max-width: 860px\)/)` und `doesNotMatch(/720px/)`.
4. `ChatThread.jsx` enthält unverändert `className="woms-chat" ref={zoneRef}`, `className="chat-messages" ref={boxRef}` und `<div ref={composerRef} />`.
5. `match(/\.chat-page \.chat-back\s*\{[^}]*display:\s*none/)`.
- [ ] **Step 2: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs`
Expected: FAIL bei 3 (alter 720-px-Wert) und 5. Test 4 besteht schon.
- [ ] **Step 3: `chat.css` neu schreiben**
Der Kopfkommentar nennt: Das Portal-CSS lädt zur Laufzeit zuletzt, Gleichstand verliert, deshalb ist jede Regel strikt spezifischer und es gibt kein `!important`. Alle P1-Variablen und -Fixes aus P1 Task 9 Step 2 bleiben unverändert. Neu sind nur `--chat-bubble-in: var(--bg-hover)`, `--chat-bubble-out: var(--grey-bg)` und `--chat-track: rgba(26,31,54,.2)`. Maße, Abstände, Schriftgrößen und Radien folgen dem Prototyp und den Tokens der Spec und entstehen beim Bauen. Verbindlich sind die Spezifitäten und Abweichungen der Tabellen sowie die Messwerte aus Step 6.
Überschreibungen des Portal-Bausteins. Die Spalte „Eigen > Fremd“ nennt die Spezifität der eigenen Regel vor der überstimmten Portal-Regel:
| Ziel | Selektor (Eigen > Fremd) | Abweichung vom Portal-Stil |
|---|---|---|
| Container, Verlauf | `.woms-chat`, `.woms-chat .chat-messages` | Flex-Spalte, `min-width: 0`. Verlauf scrollt in sich (`max-height: 60vh`), ohne Rand und Hintergrund. |
| Blase, Zeit | `.woms-chat .chat-bubble` (0,2,0 > 0,1,0), `.chat-bubble.mine`, `.chat-meta`, `.chat-notice` | Fremde Blase `--chat-bubble-in`, eigene `--chat-bubble-out` (rechts), kein Rand. `max-width: 78%`, `overflow-wrap: anywhere`. Zeit und Hinweis 12 px in `--text-2`. |
| Datei-Chip | `.woms-chat .chat-att-file` (0,2,0 > 0,1,0), `:hover` (0,3,0 > 0,2,0) | `var(--bg)`, `border: 0`, `box-shadow: var(--ring)`. Hover `--bg-hover`. |
| Audio, Fokus | `.chat-audio-bar` (0,2,0 > 0,1,0); `.chat-att-image:focus-visible`, `.chat-audio-bar:focus-visible` (0,3,0 > 0,2,0); `.chat-bubble.mine .chat-audio-play` (0,4,0 > 0,3,0); `.chat-audio-dl:hover` (0,3,0 > 0,2,0); `.chat-audio.is-error .chat-audio-time` (0,4,0 > 0,3,0) | Leiste `--chat-track`. Fokusring `2px solid var(--accent)` statt weiß. Abspielen `--chat-accent` mit `--chat-on-accent`. Download-Hover `--accent-soft`. Fehlerzeit `--red-fg`. |
| Eingabefeld | `.woms-chat .chat-input-row textarea` (0,2,1 > 0,1,1), `:focus` (0,3,1), Media `max-width: 640px` | `border: 0`, `box-shadow: var(--ring)`, `font: inherit`. Fokus `0 0 0 1px var(--accent), 0 0 0 4px var(--accent-ring)` ohne `outline`. Handy `font-size: 16px` (gegen das iOS-Zoomen). Höhe und Radius des Portals bleiben. |
| Icon-Knöpfe, Senden | `.chat-icon-btn` (0,2,0 > 0,1,0), `:hover:not(:disabled)` (0,4,0 > 0,3,0), `[aria-expanded="true"]` (0,3,0 > 0,2,0), `.chat-icon-btn:disabled`, `.chat-send:disabled` (0,3,0 > 0,2,0), `.chat-send.is-busy:disabled` (0,4,0 > 0,3,0) | `border: 0`, `box-shadow: var(--ring)`, `var(--bg)`. Hover `--bg-hover`. Offen `--accent-soft` mit 1-px-Akzentring. Deaktiviert `opacity: .55; cursor: not-allowed`, beschäftigt `opacity: .8; cursor: progress`. Senden bleibt über die P1-Variablen schwarz mit weißer Schrift. |
| Aufnahme | `.chat-rec .chat-icon-btn` (0,3,0 > 0,2,0), `:hover` (0,4,0 > 0,3,0); `.chat-rec-stop`, `.chat-rec-wave` (hinter der Icon-Knopf-Regel), `.chat-rec-dot` (0,2,0 > 0,1,0) | Knöpfe ohne Ring und Hintergrund, Hover `--accent-soft`. Farbe bzw. Punkt `--danger`. |
| Anhang-Menü | `.chat-menu` (0,2,0 > 0,1,0); `.chat-menu button` (0,2,1 > 0,1,1), `:hover`, `:focus-visible` (0,3,1 > 0,2,1) | Menü ohne Rand, mit `--shadow-pop`. Hover und Fokus `--bg-hover`, Fokus zusätzlich `outline: 2px solid var(--accent); outline-offset: -2px` (die Fremdregel setzt `outline: none`). |
| Vorschau, Hinweise | `.chat-tray-item` (0,2,0 > 0,1,0); `.chat-tray-item.is-error .chat-file-size` (0,4,0 > 0,3,0); `.chat-composer-note` (0,2,0 > 0,1,0), `.is-info` (0,3,0 > 0,2,0) | Vorschau `--bg-hover` mit transparentem Rand. Die rote Fehlervariante der Fremdregel (gleiche Spezifität, später geladen) bleibt absichtlich. Fehlertexte `--red-fg`, Info `--text-2`. |
| Ablegen | `.woms-chat.chat-drop-active::after` (0,2,1 > 0,1,1) | helle Fläche `rgba(255,255,255,.92)`, Schrift `--text`, Rand `--text-3` |
Seitenlayout ohne Präfix. Es folgt dem Prototyp, verbindlich sind:
- `.chat-page` ist ein Grid mit 320 px für die Liste und `minmax(0, 1fr)` für den Verlauf. Bei `max-width: 1100px` ist die Liste 260 px breit.
- `.chat-side` und `.chat-main` haben `min-width: 0`. Zwischen ihnen steht eine senkrechte Haarlinie (`--line`).
- `.chat-list` scrollt in sich (`max-height: 70vh; overflow-y: auto`). `.chat-list-item` ist eine Haarlinien-Zeile ohne Kasten und Rand, Hover `--bg-subtle`, `[aria-current="true"]` `--accent-soft`.
- `.chat-list-last` kürzt mit Ellipsis. `.chat-unread` ist eine kleine Zahl in `--accent`, fett, 12 px, ohne Pille.
- `.chat-page .chat-back { display: none }`, Spezifität (0,2,0) > `.btn` (0,1,0).
- Bei `max-width: 860px` (nicht 720 px): einspaltig. `.chat-page.has-thread .chat-side` und `.chat-page:not(.has-thread) .chat-main` sind `display: none`, Seite und Haupt ohne Rand und Innenabstand, `.chat-page .chat-back { display: inline-flex }`, `.woms-chat .chat-bubble { max-width: 90% }`.
- [ ] **Step 4: Fehlerzeile im Verlauf**
In `ChatThread.jsx` bleibt der Zeilencontainer `flex items-center gap-2 wrap`. Der Text wird `<span className="msg err" role="alert">`, der Knopf `<Button onClick={…}>Erneut versuchen</Button>` ohne Icon. Der Import `react-icons/fa6` entfällt. Container, Effekte und `chat-notice muted` bleiben.
- [ ] **Step 5: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs tests/unit/loadChatComposer.test.mjs`
Expected: PASS.
- [ ] **Step 6: Gegen das echte Fremd-CSS prüfen**
`chat-verlauf` bekommt einen Schritt, der `getComputedStyle` liest und mit `assert.equal` vergleicht:
- `.chat-bubble.theirs` `background-color` = `rgb(246, 248, 250)`, `.chat-bubble.mine` = `rgb(235, 238, 241)`.
- `.chat-send` `background-color`/`color` = `rgb(17, 17, 17)`/`rgb(255, 255, 255)`.
- `.chat-attach-btn` `border-top-width` = `0px`, `box-shadow` ≠ `none`.
- `.chat-att-file` `background-color` = `rgb(255, 255, 255)`, `border-top-width` = `0px`.
- `.chat-audio-bar` `background-color` = `rgba(26, 31, 54, 0.2)`.
- `.chat-input-row textarea` `border-top-width` = `0px`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t4" --only chat-verlauf,chat-menue,chat-anhang-fehler --gate p6; echo "exit $?"`
Expected:
- Schritt-Prüfungen bestehen (kein `step-failed`). Der Bericht enthält höchstens `icon-svg` am alten Zurück-Pfeil von `CustomerChatPage` (Task 5). Nur dann ist `exit 1` hier erlaubt.
- `chat-verlauf` bei 1440 px: Blasen hellgrau, Senden schwarz mit weißer Schrift, Icon-Knöpfe weiß mit Ring, Datei-Chip weiß.
- `chat-anhang-fehler`: rote Fehlerzeile und rot umrandete Vorschau.
- [ ] **Step 7: Commit**
```bash
git add src/styles/chat.css src/components/chat/ChatThread.jsx tests/unit/chatCss.test.mjs
git commit -m "Design P6: chat.css neu (Praefix .woms-chat, Umschaltung bei 860 px), Fehlerzeile ohne Icon" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Chat-Seite
**Files:**
- Modify: `src/pages/CustomerChatPage.jsx` (Z. 3, 61 bis 110), `src/styles/global.css` (KOMPAT-Regel `.side-count` löschen)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Button`, `EmptyState`, `AutocompleteInput` (Klasse `input`), `.msg.err`, Klassen aus Task 4, `lastLine`, `sortChats`.
- Produces: Seite und Adresse `/customers/chat?kunde=<id>` unverändert.
- [ ] **Step 1: Seite umbauen**
- `react-icons`, `size="sm"` und `Card pad={false}` um die Liste entfallen. Die Liste ist `.chat-list` direkt unter dem Suchfeld.
- Das Suchfeld bleibt `AutocompleteInput` („Neue Nachricht an Kunde…“, `onPick` öffnet nur die volle Beschriftung).
- Fehlerzeile: `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“. Der Leerzustand „Noch keine Unterhaltungen“ bleibt `EmptyState`.
- Jede Unterhaltung ist ein `button.chat-list-item` mit `aria-current={c.customerId === kunde ? 'true' : undefined}`. Zeile 1 (`.chat-list-top`): `<strong>` Name und `.chat-list-time`. Zeile 2: `.chat-list-last` mit `lastLine(c)` und bei `unreadFromCustomer > 0` `span.chat-unread`.
- Der Zurück-Knopf steht vor der Karte: `<Button className="chat-back" onClick={() => setParams({})}>← Alle Unterhaltungen</Button>`, darunter `<Card title={nameOf(kunde)}><ChatThread … /></Card>`. Ohne Auswahl bleibt die Karte mit `EmptyState` („Unterhaltung links auswählen“ / „Oder oben eine neue Nachricht an einen Kunden beginnen.“) und ohne Titel. Eine `Card` ohne Titel und ohne Aktionen rendert kein `h2` (Übersicht).
- [ ] **Step 2: `.side-count` löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "side-count" src`
Expected: nur noch die KOMPAT-Regel in `global.css`. Sie wird gelöscht (P6 ist ihr letzter Nutzer), danach 0 Treffer.
- [ ] **Step 3: Prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t5" --only chat,chat-verlauf,chat-tablet,chat-viel,chat-fehler,kunde-chat --gate p6`
Expected: Exit 0. Bei 390 px zeigt `chat` nur die Liste ohne Überlauf, `chat-verlauf` nur den Verlauf mit „← Alle Unterhaltungen“. Bei 1440 px zwei Spalten mit senkrechter Trennlinie, ohne Zurück-Knopf. `chat-tablet`: Liste 260 px.
- [ ] **Step 4: Commit**
```bash
git add src/pages/CustomerChatPage.jsx src/styles/global.css
git commit -m "Design P6: Chat-Liste als Haarlinien-Zeilen, Zaehler und Zurueck-Pfeil als Text, side-count entfaellt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Gutschriften-Liste
**Files:**
- Modify: `src/pages/CreditsPage.jsx` (Z. 10 bis 12), `src/components/credits/CreditsView.jsx` (Z. 25 bis 29), `src/components/credits/CreditList.jsx` (Z. 2, 7, 24 bis 62)
**Interfaces:**
- Consumes: `Tabs`, `Badge`, `EmptyState`; `.table`, `.table-wrap`, `.hide-sm`, `.hide-md`, `.right`, `.num`, `.cell-title`, `.cell-sub`, `.link`, `.loading`, `.msg.err`, `.table-foot`; Helfer aus Task 1.
- Produces: `CreditList({ customerId, selectedId, onSelect, reloadKey })` mit unveränderten Props.
- [ ] **Step 1: `CreditList` umbauen**
- Filter `<Tabs tabs={[{ id: 'open', label: 'Offen' }, { id: 'all', label: 'Alle' }]} …/>`. State, Effekt und `listCredits` bleiben.
- Laden `div.loading` „Wird geladen …“, Fehler `div.msg.err[role=alert]`, leer `EmptyState` mit „Keine offenen Anträge.“ bzw. „Noch keine Anträge.“.
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute. `<br />` plus `area-muted` wird `.cell-sub`, der Kundenname `.cell-title`. Klassen auf `th` und `td`: Eingang `hide-sm`, Leistung / bisher bei `hide-md`, Rechnung `hide-sm right num`, Bezahlt bis `hide-md`.
- Statuszelle: `Badge` und, wenn nicht leer, `.cell-sub` mit `creditStatusNote(r)`. Letzte Zelle ohne Kopftext: `<button type="button" className="link" onClick={() => onSelect(r.id)}>{creditActionLabel(r.status)}</button>`.
- Die Zeile der offenen Gutschrift trägt `aria-current="true"`. Imports `FaSpinner`, `Card`, `admin-areas.css` und die zwei Inline-Styles entfallen.
- [ ] **Step 2: `CreditsView` und `CreditsPage`**
`CreditsView`: Das `div` mit `marginBottom` um `CreditDetail` entfällt, die Liste steht in `<div className={selected ? 'mt-3' : undefined}>`. `CreditsPage`: Der Absatz „Übernahmegutschrift: …“ bleibt wörtlich als `<p className="table-foot">` ohne Inline-Style.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t6" --only gutschriften,gutschriften-tablet,gutschriften-viel,gutschriften-fehler,kunde-gutschriften --gate p6`
Expected: Exit 0. 390 px: Kunde, Status, Aktion, kein Überlauf mit dem 60-Zeichen-Namen. 1000 px: Eingang und Rechnung sichtbar, Leistung und Bezahlt bis aus. 1440 px: alle sieben Spalten.
```bash
git add src/pages/CreditsPage.jsx src/components/credits/CreditsView.jsx src/components/credits/CreditList.jsx
git commit -m "Design P6: Gutschriften-Liste als Tabelle mit Reitern und Spaltenprioritaet" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `CreditDetail`
**Files:**
- Modify: `src/components/credits/CreditDetail.jsx` (Imports Z. 1 bis 9, Markup Z. 116 bis 284)
**Interfaces:**
- Consumes: `Card`, `Badge`, `Button`, `Field`; `.two-col`, `.kv`, `.field-row`, `.check`, `.list-row`, `.cell-sub`, `.prose`, `.msg`, `.loading`, `.link`; `checkGlyph`, `checkTone`.
- Produces: `CreditDetail({ id, onClose, onChanged, onOpen })` unverändert.
- [ ] **Step 1: Logik unverändert**
Nicht angefasst werden `load`, `figures`, `setFigure` (300-ms-Timer), `decide` (`decisionError`, `window.confirm`, `busyRef`), `apply`, `canApprove`, `amountTouched`. Neu ist nur der Zustand `busyAction` (`''`, `'approve'`, `'question'`, `'reject'`, `'apply'`), den `decide` und `apply` neben `busy` setzen und zurücksetzen, damit der gedrückte Knopf „Speichert …“ bzw. „Sendet …“ zeigen kann.
- [ ] **Step 2: Markup umbauen**
- **Kopf:** `<Card title={`Antrag ${r.ref} · ${c.name || c.companyName}`} actions={<Button onClick={onClose}>Schließen</Button>}>`, darunter `p.muted` „eingegangen … “ mit `Badge` und `p.msg.ok[role=status]` für `message`.
- **Aufbau:** `div.two-col` mit zwei Spalten-`div`. Jede Gruppe ist ein `<Card title="…">` mit dem heutigen `h4`-Text. Den Abstand zwischen den Karten einer Spalte liefert `.page section + section` aus P1, P6 baut dafür keine eigene Regel. Die Reihenfolge der Abschnitte bleibt: links Antrag, Grund / Nachricht, Nachweise, Verlauf, rechts Kunde & Vertrag, Weitere Anträge, Automatische Prüfung, Prüfung durch dich, Berechnung bzw. Entscheidung.
- **Ersetzungen:**
- `dl.area-kv` wird `dl.kv`.
- Mehrzeiler (`<br />` plus `area-muted`) werden `.cell-sub`.
- Nachweise, Verlauf, Weitere Anträge und Prüfpunkte werden `.list-row`. Text aus `r.message` und Verlaufstexten ist `.prose`.
- Alle Links (`domain`, Nachweise) bekommen `className="link"`. Die Nummer in „Weitere Anträge“ ist `<button type="button" className="link" onClick={() => onOpen?.(o.id)}>` statt `<a href="#">`.
- Prüfpunkte tragen `<Badge tone={checkTone(k.status)}>{checkGlyph(k.status)}</Badge>`.
- „In Stripe öffnen“ ist `Button as="a" …` ohne Icon, `KundenansichtButton` ohne `variant`.
- **Formular:** Die manuellen Punkte stehen als `div.field` mit je einem `label.check`. „Berechnung“ besteht aus zwei `.field-row` mit `Field label htmlFor` (IDs `calc-amount`, `calc-months`, `calc-paid-until`, `calc-from`, Klassen `input`/`select`). `calcText` ist `p.strong`, der Hinweis `p.muted.small`. „Gutschrift (€)“ (`#credit-amount`) und das Nachrichtenfeld (`textarea#credit-note.textarea`, `maxLength={4000}`, langes Label wörtlich) sind `Field`.
- **Knopfreihe** `div.flex.items-center.gap-3.wrap`:
- `Button variant="primary"` „Genehmigen & gutschreiben“ (`disabled={!canApprove}`),
- `Button` „Rückfrage senden“,
- `Button variant="danger"` „Ablehnen“.
- Bei `busyAction` zeigt der gedrückte Knopf „Speichert …“ (genehmigen, ablehnen) bzw. „Sendet …“ (Rückfrage).
- „Jetzt in Stripe gutschreiben“ ist `variant="primary"` mit „Speichert …“ bei `apply`.
- **Meldungen:** `actionError` als `p.msg.err[role=alert]`. Ladefehler `<Card><p className="msg err" role="alert">…</p><Button onClick={onClose}>Schließen</Button></Card>` (ohne Titel und Aktionen, also ohne `h2`). Laden `div.loading`.
- `react-icons`, `admin-areas.css` und alle 9 Inline-Styles entfallen.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t7" --only gutschriften-antrag,gutschriften-sperre --gate p6`
Expected: Exit 0, die Sperr-Prüfung besteht (1 POST `…/decision`, 1 POST `…/calc`). `gutschriften-antrag` bei 1440 px zweispaltig mit genau einem schwarzen Knopf und „Ablehnen“ als rotem Text, bei 390 px einspaltig ohne Überlauf.
```bash
git add src/components/credits/CreditDetail.jsx
git commit -m "Design P6: Gutschrift-Detail zweispaltig mit Abschnitten und kv-Listen, Verhalten unveraendert" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: Abo-Angebote: Seite, Ansicht, Liste
**Files:**
- Modify: `src/pages/OffersPage.jsx` (Z. 10), `src/components/offers/OffersView.jsx` (Z. 2, 14 bis 34), `src/components/offers/OfferList.jsx` (Z. 2, 7, 54 bis 118)
**Interfaces:**
- Consumes: `Tabs`, `Badge`, `Button`, `EmptyState`; Klassen wie Task 6; `filterOffers`, `offerRowActions`, `offerMailLine`.
- Produces: `OfferList({ customerId, reloadKey, onMessage, onChanged })` und `OffersView({ customerId, customerName })` unverändert.
- [ ] **Step 1: `OfferList` umbauen**
- Tabs „Offen & aktiv“ (`open`) und „Alle“ (`all`), `shown = filterOffers(offers, filter)`. Laden, Fehler und Leerzustand wie bei den Gutschriften („Keine offenen oder aktiven Angebote.“ / „Noch keine Angebote.“).
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute, Mehrzeiler als `.cell-sub`. Klassen: Angelegt `hide-md`, Gültig bis `hide-sm`. Stripe-Links `a.link`.
- Statuszelle: `Badge`, `.cell-sub` „seit …“ / „wartet auf Abschluss“, „Stripe: Coupon · Abo“, danach `offerMailLine(o)` als `.cell-sub` (`warn` als `span.msg.warn`).
- Aktionen unter den Status-Zeilen, `div.flex.gap-3.wrap.mt-1`, nur wenn `sendLabel` oder `canWithdraw`:
- `<button type="button" className="link" title="Angebots-E-Mail an … senden" onClick={() => sendMail(o)} disabled={busyId === o.id}>{sendLabel}</button>`
- `Button variant="danger"` „Zurückziehen“
- `withdraw` und `sendMail` bleiben unverändert (2 `confirm`, 2 `alert`).
- Imports `FaSpinner`, `Card`, `admin-areas.css` und die drei Inline-Styles entfallen.
- [ ] **Step 2: `OffersView` und `OffersPage`**
`OffersView`: Meldung als `<p className="msg ok mb-2" role="status">`, dann `<div className="toolbar"><Button variant="primary" …>Neues Angebot</Button></div>` (ohne `FaPlus`, auch bei offenem Dialog sichtbar), dann `OfferList`, am Ende `{showForm && <OfferForm … />}` ohne Stil-`div`. `OffersPage`: Der Absatz „Ein Angebot legt sofort …“ bleibt wörtlich als `<p className="table-foot">`.
- [ ] **Step 3: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t8" --only angebote,angebote-tablet,angebote-fehler,kunde-abo --gate p6`
Expected: Exit 0. 390 px: Kunde, Angebot, Status, die 50-Zeichen-E-Mail bricht, „E-Mail senden“ und „Zurückziehen“ sind erreichbar. 1440 px: alle fünf Spalten.
```bash
git add src/pages/OffersPage.jsx src/components/offers/OffersView.jsx src/components/offers/OfferList.jsx
git commit -m "Design P6: Angebote-Liste als Tabelle mit Reitern, Aktionen als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: `OfferForm` als Dialog
**Files:**
- Modify: `src/components/offers/OfferForm.jsx` (Imports Z. 1 bis 8, Markup Z. 85 bis 143)
**Interfaces:**
- Consumes: `Dialog({ title, onClose, busy, footer, children })`, `Field`, `Button`, `AutocompleteInput` (Prop `id`), `.field-row`, `.check`, `.msg.err`.
- Produces: `OfferForm({ fixedCustomerId, fixedCustomerName, onCreated, onCancel })` unverändert. `onCancel` ist zugleich `onClose` des Dialogs.
- [ ] **Step 1: Markup als Dialog**
Kontext-Effekt, `evaluation`, `save` (`window.confirm(offerConfirm(…))`), `suggestions` und `onPick` bleiben. Der Rückgabewert wird `<Dialog title="Neues Angebot" onClose={onCancel} busy={busy} footer={…}>` ohne `<form>`. Felder in der heutigen Reihenfolge und mit den heutigen Labeltexten, alle als `Field` mit neuen IDs `offer-*` für die Label-Bindung:
- „Kunde“: mit `fixedCustomerId` `<p className="strong">{fixedCustomerName || 'Kunde'}</p>`, sonst `AutocompleteInput id="offer-customer"`.
- `.field-row` „Produkt“ (`select.select`) und „Preis pro Monat im Angebot (€)“ (`input.input`).
- `.field-row` „für“ (`select.select`) und „Angebot gültig bis“ (`input.input type="date"`).
- „Hinweis für den Kunden …“ und „Interne Notiz …“ (`input.input`, `maxLength` 500 bzw. 2000).
- `label.check` „Kunden per E-Mail über das Angebot informieren“.
- Vorschau: Fehlerzeilen `p.msg.err`, Vorschauzeilen als `p`, die Platzhaltertexte wörtlich. Bei `error` `p.msg.err[role=alert]`.
`footer`: `Button variant="primary" type="button"` (`onClick={save}`, `disabled={!evaluation || evaluation.errors.length > 0 || busy}`) mit `busy ? 'Speichert …' : evaluation?.saveLabel || 'Angebot anlegen'` und `Button type="button"` „Abbrechen“ (`onClick={onCancel}`, `disabled={busy}`). `FaSpinner`, `Card`, `admin-areas.css`, die vier Inline-Styles und die Klasse `offer-form` entfallen.
- [ ] **Step 2: Clipping und Tastatur prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t9" --only angebote-dialog,angebote-vorschlag,angebote-laufend,angebote-tastatur,kunde-abo --gate p6`
Expected: Exit 0, alle Schritt-Prüfungen bestehen. `angebote-vorschlag`: Liste vollständig erreichbar. `angebote-laufend` bei 390 px: Der lange Speichern-Text bricht in eine zweite Zeile der Fußzeile. `angebote-tastatur`: Esc schließt erst die Liste, dann den Dialog, beim Speichern wirken „ד und Esc nicht.
Besteht die Clipping-Prüfung nicht, bekommt `.dialog-body .autocomplete-list` im Block aus Task 3 `max-height: 200px` (nie `!important`, die Komponente bleibt unberührt).
- [ ] **Step 3: Commit**
```bash
git add src/components/offers/OfferForm.jsx
git commit -m "Design P6: Angebots-Formular als Dialog (Esc, Sperre beim Speichern, Vorschlagsliste geprueft)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
War die Abhilfe nötig, kommt `src/styles/global.css` ausdrücklich in dieses `git add`.
### Task 10: E-Mail-Speicher
**Files:**
- Modify: `src/pages/StorageTasksPage.jsx` (Imports Z. 1 bis 8, Markup Z. 38 bis 98)
**Interfaces:**
- Consumes: `PageHeader`, `Card`, `Badge`, `Button`, `EmptyState`; `.table.table-wrap`, `.hide-sm`, `.hide-md`, `.cell-sub`, `.link`, `.msg`, `.list-row`, `.table-foot`, `.loading`; `storageDue`.
- Produces: Seite `/customers/email-speicher` unverändert.
- [ ] **Step 1: Markup umbauen**
- Fehler `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“ (`onClick={load}`). Laden `div.loading` „Wird geladen …“.
- Zusammenfassung `<p className={summary.warn ? 'msg warn mb-2' : 'muted mb-2'}>`.
- Bei null Aufgaben `EmptyState` „Keine offenen Speicher-Aufgaben.“, sonst `table.table.table-wrap` mit den heutigen Spalten und Zellinhalten, Mehrzeiler als `.cell-sub`. Klassen: Fällig `hide-sm`, Bestellt `hide-md`.
- Fällig-Zelle: `Badge tone={due.tone}` mit `due.label` und `due.sub` (`.cell-sub`, wenn `due.subMuted`). Kundenname `Link className="link"` auf `/customers/${t.customerId}`. Die Teilfehler-Zeile `partialText(t)` ist `span.msg.err`.
- Letzte Zelle ohne Kopftext: `<button type="button" className="link" data-storage-done={t.id} onClick={() => done(t)} disabled={busyId === t.id}>{busyId === t.id ? 'Speichert …' : 'Erledigt'}</button>`.
- „Gescheiterte Änderungen (letzte 30 Tage)“ bleibt `Card title`, die Zeilen sind `.list-row` (Text `failedText` als `span.muted`). Der lange Hinweisabsatz bleibt wörtlich als `<p className="table-foot">`.
- `done`, `window.confirm('Stripe-Menge von Hand korrigiert?')` und `window.alert` bleiben. `react-icons`, `admin-areas.css` und beide Inline-Styles entfallen.
- [ ] **Step 2: Prüfen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t10" --only speicher,speicher-fehler --gate p6`
Expected: Exit 0. 390 px: Kunde, Aufgabe und „Erledigt“ ohne Überlauf. 1440 px: alle fünf Spalten, Fällig als Badge mit Datum.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "data-storage-done" src/pages/StorageTasksPage.jsx`
Expected: `1`.
```bash
git add src/pages/StorageTasksPage.jsx
git commit -m "Design P6: E-Mail-Speicher als Tabelle, Erledigt als Textlink mit data-storage-done" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 11: `admin-areas.css` löschen, KOMPAT-Reste
**Files:**
- Delete: `src/styles/admin-areas.css`
- Modify: `src/styles/global.css` (KOMPAT-Zeilen `.credit-cols a`, `.area-rows a`)
**Interfaces:**
- Consumes: `$W/css-klassen.mjs` aus P1. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt, die Tasks 6 bis 10 haben die Importzeilen der P6-Dateien entfernt.
- Produces: keine Datei `admin-areas.css`, keinen Import und keine Klasse aus ihr.
- [ ] **Step 1: Restnutzer ermitteln**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "\barea-|credit-(row|cols|files|checks|history|manual|calc|field)|offer-(row|form)|storage-row" src --glob '!*.css'; rg -n "admin-areas" src`
Expected: keine Treffer, auch nicht in `CustomerFunktionen.jsx` und `AboStatusCard.jsx`. Ein Treffer in einer P6-Datei wird dort behoben. Ein Treffer in einer P4-Datei heißt, dass P4 sie nicht vollständig gelöst hat: stoppen und melden. Das Muster enthält bewusst kein `funktionen-`, denn diese Klassen leben in `global.css`.
- [ ] **Step 2: Datei löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/styles/admin-areas.css && rg -n "admin-areas" src`
Expected: keine Treffer.
- [ ] **Step 3: KOMPAT-Reste löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "credit-cols|area-rows|side-count" src/styles`
Expected: genau die beiden Zeilen in `global.css` (KOMPAT „Links in Bestandsklassen“) für `.credit-cols a` und `.area-rows a`. Beide werden gelöscht, `.side-count` ist schon seit Task 5 weg. Danach liefert derselbe Befehl keine Treffer.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.join('\n'))"`
Expected: nur die in P1 erlaubten Namen (`p-4`, `btn-sm`, `status-filter`, `supporters-editor`, `timeline`, Bootstrap-Namen in `WorksheetList.jsx`). Meldet das Skript die gelöschte Datei als fehlend, wird sie in der Dateiliste von `css-klassen.mjs` (unter `$W`) gestrichen.
- [ ] **Step 4: Bauen und Commit**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t11" --only kunde-abo,kunde-gutschriften,kunde-funktionen,gutschriften,angebote,speicher --gate p6`
Expected: Exit 0. Die Kundenseiten-Reiter `kunde-abo` und `kunde-funktionen` sehen aus wie nach P4 (Vergleich mit `$W/shots/p4`), die drei Listen wie nach Task 6, 8 und 10.
```bash
git status --short
git add src/styles/global.css
git commit -m "Design P6: admin-areas.css geloescht, KOMPAT-Reste entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
`git status --short` vor dem Commit darf nur `D src/styles/admin-areas.css` (vom `git rm` schon vorgemerkt) und `M src/styles/global.css` zeigen. Die Löschung steht damit im selben Commit.
### Task 12: Gate P6, Review, Tag, Übergabe
- [ ] **Step 1: Rebase und Ausgangswerte**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
Expected: Exit 0, `# fail 0`, `# pass` = N5 + 15 (10 Lib-Tests, 5 `chatCss`). Liegen neue Commits auf `test`, ist die Zahl entsprechend höher und wird notiert. Die Bundle-Größen aus der Ausgabe werden mit P5 verglichen, das CSS sollte kleiner sein (`admin-areas.css` ist weg).
- [ ] **Step 2: Vollständiger Gate-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6" --gate p6`
Expected: Exit 0, in allen Ansichten kein Verstoß gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request`, `step-failed`. In den P6-Ansichten dazu kein `icon-svg` außerhalb von `.woms-chat`.
- [ ] **Step 3: Inventur und Hygiene**
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && for f in src/components/credits/CreditDetail.jsx src/components/offers/OfferList.jsx src/components/offers/OfferForm.jsx src/pages/StorageTasksPage.jsx; do echo "$f $(rg -o 'window\.confirm|window\.alert|busyRef|data-storage-done|id="(calc|credit)-[a-z-]+"' $f | wc -l)"; done
rg -n "react-icons|size=\"sm\"|style=\{\{|!important" src/pages/{CustomerChat,Credits,Offers,StorageTasks}Page.jsx src/components/{chat,credits,offers} src/styles/chat.css
rg -n "admin-areas" src
git diff --stat design-p5..HEAD -- src/lib/loadChatComposer.js tests/unit/loadChatComposer.test.mjs
test ! -e src/styles/admin-areas.css && echo "admin-areas.css weg"
```
Expected: Zählung 16, 4, 1, 3. Die Befehle zwei bis vier sind leer, am Ende steht „admin-areas.css weg“.
- [ ] **Step 4: Code-Review (sonnet)**
Geprüft wird der Diff `design-p5..HEAD` gegen die Spec-Abschnitte „Bausteine“, „Seiten ohne Prototyp-Vorlage“ und P6. Schwerpunkte:
- Jede Chat-Regel hat Präfix und strikt höhere Spezifität (Tabelle in Task 4).
- `admin-areas.css` ist gelöscht, kein Import und keine ihrer Klassen ist übrig, `.funktionen-*` stehen unverändert in `global.css`.
- `CreditDetail` verhält sich unverändert (`busyRef`, Debounce, `confirm`).
- Der Textdiff (`git diff design-p5..HEAD -w`) zeigt nur Textänderungen, die die Spec unter „Texte“ erlaubt (darunter „Speichert …“, „Sendet …“, „Wird geladen …“) oder die ein Icon durch ein Zeichen ersetzen (z. B. „← Alle Unterhaltungen“).
- Jede Aktion der alten Seiten ist erreichbar: Prüfen/Ansehen, Schließen, Kundenansicht öffnen, In Stripe öffnen, Weitere Anträge, Genehmigen, Rückfrage, Ablehnen, Jetzt in Stripe gutschreiben, Neues Angebot, E-Mail senden/Erneut senden, Zurückziehen, Coupon- und Abo-Link, Erledigt, Kundenlink, Erneut versuchen, Neue Nachricht an Kunde, Unterhaltung öffnen, Zurück.
Wichtige Befunde werden behoben. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only chat-verlauf,gutschriften,angebote,speicher`.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p6 && git log --oneline origin/test..design-p6 && git rev-parse design-p6`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p6/1440x900` und `shots/p6/390x844`. Auswahl: `chat`, `chat-verlauf`, `chat-menue`, `chat-anhang-fehler`, `chat-tablet`, `gutschriften`, `gutschriften-antrag`, `gutschriften-tablet`, `angebote`, `angebote-dialog`, `angebote-vorschlag`, `speicher`, `kunde-chat`, `kunde-abo`.
- die bewusst offenen Punkte:
- Lightbox und Kamera bleiben dunkel, „Foto verwenden“ bleibt im Portal-Lila (nicht aus WOMS änderbar).
- Die Anhang-Menü-Icons des Portal-Bausteins bleiben.
- Die Blasenfarben `--bg-hover` und `--grey-bg` sind auf Weiß sehr zart. Die Stellschrauben sind `--chat-bubble-in` und `--chat-bubble-out` in `chat.css`.
- den Hinweis zu tp2: `admin-areas.css` ist gelöscht. Ändert `feature/terminplaner-tp2` die Datei (Anleitungs-Panel der Funktionen), gibt es beim Zusammenführen einen Konflikt, und die Panel-Regeln gehören dann in den Block „Kunden (P4)“ von `global.css`.
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p6^{commit}:refs/heads/test'`
- die Rücknahme `git revert --no-commit design-p5..design-p6` als Commit auf `test`, durch den Eigentümer. Sie gilt nur, solange P6 das jüngste Paket ist.
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` stimmt mit `dist/index.html` des Tags überein.
- Lesender Sichttest im Chrome des Eigentümers: `/customers/chat` mit geöffnetem Verlauf (Anhang-Menü öffnen), `/customers/gutschriften` und `/customers/angebote`.