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

57 KiB
Raw Blame History

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.

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.

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.

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

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
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.

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.

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.

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
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.

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.

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:

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.