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

55 KiB
Raw Blame History

WOMS „Weißes Design“ P4: Kunden – 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: Nach P4 sieht der Kundenbereich wie im Prototyp aus:

  • Kundenliste als Tabelle mit den Reitern Feste Kunden · Leads · Archiviert · Alle samt Zählern, mit Suche und Abzeichen
  • „Neuer Kunde“ und „Neuer Lead“ als Dialoge, auch im Zuordnen-Assistenten
  • Kundenseite mit Brotkrumen, Eyebrow und allen neun Reitern. Übersicht als zwei Spalten mit Schlüsselwerten, Stammdaten weiter direkt bearbeitbar, Tickets-Reiter mit Links
  • Funktionen, Protokoll und Abo-Status ohne Karten und ohne Icons, neuer Grundstil in admin-areas.css

Architecture:

  • Filter, Zähler und Abzeichen sind reine Funktionen in der neuen src/lib/customerList.js mit Tests. lib/leads.js importiert Appwrite und ist unter Node nicht ladbar. Deshalb ziehen customerStage und isArchived dorthin um, und leads.js re-exportiert sie. Alle bestehenden Importe bleiben gültig.
  • Tabellenzeilen haben als erste Zelle einen echten Link, der Zeilenklick läuft über isRowActivation aus src/lib/rowClick.js (entsteht in P2, P4 baut den Helfer nicht neu).
  • Die beiden Anlege-Formulare rendern <Dialog> aus P1.
  • CSS: ein kleiner Block „Kunden (P4)“ in global.css, dazu der neue Grundstil in src/styles/admin-areas.css.

Tech Stack: React 18, react-router-dom 6, reines CSS. Tests mit node --test, Screenshot-Werkzeug aus P0.

Spec: docs/superpowers/specs/2026-10-04-weisses-design-design.md, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Tabelle, Reiter, Badge, Formular, Dialog, Meldungen, Laden), „Ticket als eigene Seite / Route und Links“, „Texte“, „Risiken“ (Zeile zu feature/terminplaner-tp2) und Paket P4. Detail im Anhang 2026-10-04-weisses-design-umbaukarte.md, Abschnitte 5 (P4), 3, 4 und 7.5. Globale Regeln und gemeinsame Schnittstellen: 2026-10-05-weisses-design-00-uebersicht.md.

Global Constraints

Jeder Run-Block beginnt mit source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&. Danach gelten $WT (Worktree, aktuelles Verzeichnis), $W (Werkzeug) und $TMPDIR. Tests und Build laufen nur über "$W/check.sh", Browser-Läufe nur über "$W/shots.sh" (Regeln der Übersicht).

Es gelten die Regeln der Übersicht. Für P4 kommt hinzu:

  • Zeilenangaben gelten für den Stand 603d08c. Nach P1 bis P3 können sie leicht verschoben sein, maßgeblich sind die genannten Namen.
  • Voraussetzung sind das Tag design-p3 und aus P2 ticketPath und ticketRef (lib/ticketRoutes.js), ticketNumber (lib/ticketView.js), isRowActivation (lib/rowClick.js) und die Klasse .row-link.
  • Geändert werden nur CustomersPage.jsx, CustomerDetailPage.jsx, components/customer/*, admin-areas.css, ein Block in global.css (dazu fällt dort die KOMPAT-Regel .side-logo), in lib/leads.js zwei Funktionen (Umzug) und eine Stelle in AssignProjectPage.jsx (Aufruf von CreateCustomerForm). Neu sind lib/customerList.js und sein Test.
  • Unverändert bleiben lib/customerTabs.js (der Test pinnt [id, label], Zähler kommen beim Rendern), lib/funktionenView.js, useCustomers, customerAdminApi, teamApi, CopyableCredential und AssignedProjectCard (P5), Chat, Gutschriften, Angebote (P6) und CustomerInvoices (P7).
  • tp2 zuerst klären: feature/terminplaner-tp2 hat 5 Commits (6f03712, c001554, 857e001, 45e5dc3, 225e2f1) vor test. Sie ändern CustomerFunktionen.jsx, lib/funktionenView.js, admin-areas.css und Rechtliches. P4 baut CustomerFunktionen.jsx und admin-areas.css neu. Task 1 stoppt deshalb und fragt den Eigentümer. Einzelne Commits werden nicht per Cherry-Pick geholt.
  • Texte bleiben wörtlich, auch die ae/ue-Schreibweisen („Strasse + Nr.“, „Fuer einen Lead …“). Neu sind nur Spaltenköpfe der neuen Tabellen, das Abzeichen „Wichtig“, „Wird geladen …“, „Speichert …“, „Kopieren“ und „Kopiert“.
  • Erreichbar bleiben alle heutigen Bedienelemente: Reiter und Suche der Liste, Zeile öffnet die Kundenseite, „Neuer Kunde“ (Admin) und „Neuer Lead“ mit beiden Formularen, auch im Zuordnen-Assistenten. Auf der Kundenseite „Kunde löschen“ (Admin, mit window.confirm), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, der Weg zurück über die Brotkrume, die neun Reiter, „Speichern“, „Zum festen Kunden machen“, „Passwort ändern“/„Zugang anlegen“, Aktivieren und Abschalten mit Notiz, Site-Key kopieren und „Erneut versuchen“. Es gibt keine data-testid in diesen Dateien.
  • Entscheidung (Reiter der Liste): Reihenfolge Feste Kunden · Leads · Archiviert · Alle, Start bei Feste Kunden, nicht in der URL (wie heute).
  • Entscheidung (Zähler und Suche): Die Zähler nutzen dieselbe Funktion wie die Liste und beachten deshalb den Suchtext. Die Zahl im aktiven Reiter ist immer die Zeilenzahl. Die Suche durchsucht Name, Code, Standort, E-Mail wie heute und zusätzlich den Ansprechpartner, weil die Tabelle ihn zeigt (der Prototyp tut es auch).
  • Entscheidung (Spalten der Liste): Unternehmen mit Ansprechpartner als .cell-sub, Kundennummer (hide-sm), Standort, E-Mail und Telefon (hide-md). Keine Spalte „Offene Tickets“ (Spec), keine Fußzeile.
  • Entscheidung (Abzeichen): Lead grau (muted), Abgesagt und Archiviert plain, Wichtig orange (warn, wie der frühere Stern). „Fester Kunde“ entfällt als Abzeichen, der Reiter sagt es, die Eyebrow der Kundenseite nennt es.
  • Entscheidung (erste Zelle): <div className="cell-title"> mit <Link className="row-link"> um den Namen und den Abzeichen dahinter, wie in der Ticketliste aus P2.
  • Entscheidung (Kundenseite): Eyebrow „Kundennummer · Stufe“ (Stufe als Klartext), h1 nur der Name (Rückfall E-Mail, dann „–“). „Wichtig“ und „Archiviert“ stehen als Abzeichen in der .sub-Zeile vor Standort · E-Mail · Telefon. Kopfknöpfe: „Kunde löschen“ (Admin, roter Textlink), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, keiner schwarz. „Alle Kunden“ wird die Brotkrume „Kunden“. Die Reiter bekommen keine Zähler (zusätzliche Abfragen, die Spec nennt Zähler nur für die Liste).
  • Entscheidung (Übersicht): Schwarz ist nur „Speichern“. „Zum festen Kunden machen“ und „Passwort ändern“/„Zugang anlegen“ sind Standardknöpfe. Rückmeldungen laufen über .msg (ok, err, warn).
  • Entscheidung (Tickets-Reiter): Spalten „Ticket“ (Nummer, Betreff, „Dringend“, darunter die Art roh wie heute) und „Status“, keine zusätzlichen Abfragen.
  • Entscheidung (Dialoge): CreateLeadForm im Standarddialog mit einer .field-row, CreateCustomerForm wide mit zwei .field-row-3. Fuß wie im Prototyp: <span className="grow" />, Abbrechen (Standardknopf mit type="button", sonst löst er in <Dialog as="form"> das Absenden aus), Absenden (schwarz, type="submit"). Abbrechen ist wie × und Esc während busy gesperrt. Beide Formulare bleiben in CustomersPage.jsx, der Export von CreateCustomerForm bleibt.
  • Entscheidung (Funktionen und Protokoll): Funktionen: eine Tabelle mit einer Projektzeile über den Funktionszeilen, die Bestätigung als eigene Zeile darunter. Dieselbe Zusatzzeile kann später das Panel von tp2 aufnehmen. Protokoll: Tabelle „Zeit“, „Mitarbeiter“, „Aktion“ mit dem Zusatztext als .cell-sub.
  • Bekannt und unverändert: CustomerDetailPage.load() ersetzt die Seite beim Nachladen durch den Ladezustand, die Meldung „Gespeichert“ bleibt deshalb nicht stehen. Das ist heute schon so und kein P4-Thema.

Review Focus

Alle fünf Punkte der Übersicht treffen P4. Die Ansichten stehen in Task 3.

  1. Altdaten (kunden-altdaten*, kunde-altdaten): Kunden ohne Kundennummer, Standort, E-Mail, Telefon und Ansprechpartner zeigen „–“. Ein Kunde ohne Namen zeigt die E-Mail, ohne beides „–“. Im Text steht nie undefined, null oder NaN.
  2. Lange Inhalte: Firmenname mit 60 Zeichen, ein Kunde mit den Abzeichen Wichtig, Lead und Archiviert, Funktionen mit langem Projektnamen, langer URL, Site-Key und Notiz. Kein waagerechtes Scrollen bei 390 px.
  3. Teilausfall (fehler-kunde*): Antworten die Portal-Wege portal, abo, protokoll und funktionen mit 500, zeigen Protokoll, Abo und Funktionen .msg.err, die Übersicht „nicht abrufbar“. Die Reiter bleiben bedienbar.
  4. Nur Tastatur (kunden-tastatur, dlg-*): Ein Zeilenlink ist per Tab erreichbar, Enter öffnet die Kundenseite. Esc schließt die Dialoge, der erste Tab im offenen Dialog bleibt im Dialog. Ein Klick in die Zeile neben dem Link öffnet genau einmal.
  5. Viele Einträge (kunden-viel): 300 zusätzliche Kunden, wie heute ohne Seitenwechsel. Die Zähler stimmen in jedem Reiter mit den Zeilen überein.

Task 1: Paketstart und tp2-Prüfung

P4 baut CustomerFunktionen.jsx und admin-areas.css neu, beide Dateien ändert auch feature/terminplaner-tp2. Vor dem ersten Umbau wird deshalb geklärt, in welcher Reihenfolge beide Arbeiten live gehen.

Files: keine Änderung (nur Lesen und Prüfen).

Interfaces:

  • Consumes: Tag design-p3 und die P2-Bausteine ticketPath, ticketRef, ticketNumber, isRowActivation und .row-link.

  • Produces: die Ausgangs-Testzahl N3 und die Entscheidung des Eigentümers zu tp2.

  • Step 1: Paketstart

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p3 HEAD && git rebase origin/test && "$W/check.sh" Expected:

  • Alle Befehle enden mit Exit 0, # fail 0. Schlägt merge-base fehl, ist design-p3 nicht im Branch: Stopp und melden.

  • Die Zahl # pass ist N3. Soll nach P4: N3 + 6.

  • Step 2: Parallele Arbeit an feature/terminplaner-tp2

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer src/styles/admin-areas.css src/styles/global.css src/lib/leads.js src/lib/funktionenView.js src/pages/AssignProjectPage.jsx Expected: keine Ausgabe. Gibt der Befehl Zeilen aus (Stand 2026-10-05: 6f03712, c001554, 857e001, 45e5dc3), stoppt der Agent und fragt den Eigentümer:

  • Er empfiehlt, tp2 zuerst nach test zu bringen.
  • Er holt keinen Commit einzeln per Cherry-Pick. 6f03712 allein ließe CustomerFunktionen.jsx und lib/funktionenView.js in einem Zwischenstand, den die übrigen tp2-Commits wieder umbauen.

Weiter geht es erst nach der Antwort:

  • Fall A, tp2 ist in test: Der Rebase aus Step 1 wird wiederholt, dieser Step ist dann leer.

  • Fall B, der Eigentümer legt P4 vor tp2: Er nennt, wer die Konflikte in CustomerFunktionen.jsx, lib/funktionenView.js und admin-areas.css später auflöst. Die Übergabe (Task 8) hält das fest.

  • Step 3: P2-Bausteine sind da

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js && rg -n "export function ticketNumber" src/lib/ticketView.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -m 2 -n "\.row-link" src/styles/global.css Expected: Treffer in allen vier Dateien. Sonst Stopp: P4 darf erst nach P2 laufen.

  • Step 4: Stand von CustomerFunktionen.jsx

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "abgeschaltetText" src/lib/funktionenView.js; rg -c "AnleitungPanel|TextformPanel" src/components/customer/CustomerFunktionen.jsx Expected:

  • Fall A (tp2 ist in test): Treffer in beiden Dateien. Task 6 enthält dann die Zeile „tp2-Zusatz“.
  • Fall B (der Eigentümer hat P4 vor tp2 freigegeben): keine Treffer. In Task 6 entfallen die Zeile „tp2-Zusatz“ und abgeschaltetText, und in Task 3 entfällt die Zusicherung zum Abschaltgrund in kunde-funktionen.

Task 2: customerList.js (Filter, Zähler, Abzeichen)

Files:

  • Create: src/lib/customerList.js
  • Modify: src/lib/leads.js (Zeilen 8–18: customerStage und isArchived samt Kommentar ersetzen)
  • Test: tests/unit/customerList.test.mjs (neu)

Interfaces:

  • Consumes: nichts (reine Datei ohne Importe).

  • Produces, alle aus src/lib/customerList.js, die ersten vier auch aus src/lib/leads.js:

    • customerStage(c) → 'lead' | 'lost' | 'customer' und isArchived(c) → boolean, unverändert verschoben
    • filterCustomers(customers, { stage = 'customer', search = '' } = {}) → Kunde[]
    • stageCounts(customers, search = '') → { customer, lead, archived, all }
    • CUSTOMER_TABS: [{ id, label }]
    • customerBadges(c, { stage = true } = {}) → [{ key, label, tone }]
    • stageLabel(stage), customerEyebrow(c), customerTitle(c) → string
  • Step 1: Failing Tests schreiben

const { CUSTOMER_TABS, customerStage, isArchived, filterCustomers, stageCounts, customerBadges, stageLabel, customerEyebrow, customerTitle } =
  await loadModule('src/lib/customerList.js')

const A = { $id: 'a', name: 'Alpha GmbH', code: 'K-1', location: 'Kaiserslautern', email: 'info@alpha.de', contactName: 'Anna Alt', customerStatus: 'customer' }
const B = { $id: 'b', name: 'Beta', customerStatus: 'lead' }
const C = { $id: 'c', name: 'Gamma', customerStatus: 'lost' }
const D = { $id: 'd', name: 'Delta' } // Bestandsdaten ohne Status gelten als fester Kunde
const E = { $id: 'e', name: 'Epsilon', customerStatus: 'lead', archived: true, importantCustomer: true }
const F = { $id: 'f', name: 'Zeta', customerStatus: 'customer', archived: true }
const ALL = [A, B, C, D, E, F]
const ids = (list) => list.map((c) => c.$id)

test('CUSTOMER_TABS, customerStage, isArchived', () => {
  assert.deepEqual(CUSTOMER_TABS, [
    { id: 'customer', label: 'Feste Kunden' }, { id: 'lead', label: 'Leads' },
    { id: 'archived', label: 'Archiviert' }, { id: 'all', label: 'Alle' },
  ])
  assert.equal(customerStage({ customerStatus: 'LEAD' }), 'lead')
  assert.equal(customerStage({ customerStatus: 'Lost' }), 'lost')
  for (const c of [{}, { customerStatus: '' }, { customerStatus: 'sonstwas' }, null, undefined]) assert.equal(customerStage(c), 'customer')
  assert.equal(isArchived({ archived: true }), true)
  for (const c of [{}, { archived: false }, null, undefined]) assert.equal(isArchived(c), false)
})

test('filterCustomers: jeder Reiter', () => {
  assert.deepEqual(ids(filterCustomers(ALL, { stage: 'customer' })), ['a', 'd'])
  assert.deepEqual(ids(filterCustomers(ALL, { stage: 'lead' })), ['b'])
  assert.deepEqual(ids(filterCustomers(ALL, { stage: 'archived' })), ['e', 'f'])
  assert.deepEqual(ids(filterCustomers(ALL, { stage: 'all' })), ['a', 'b', 'c', 'd'])
  assert.deepEqual(ids(filterCustomers(ALL)), ['a', 'd'])
  assert.deepEqual(ids(filterCustomers(ALL, { stage: 'x' })), [])
  assert.deepEqual(filterCustomers(undefined, { stage: 'all' }), [])
})

test('filterCustomers: Suche in Name, Code, Standort, E-Mail und Ansprechpartner', () => {
  const find = (search, stage = 'all') => ids(filterCustomers(ALL, { stage, search }))
  for (const s of ['ALPHA', 'k-1', 'kaisers', 'info@alpha', 'anna']) assert.deepEqual(find(s), ['a'], s)
  assert.deepEqual(find('xyz'), [])
  assert.deepEqual(find('eta', 'lead'), ['b'])
  assert.deepEqual(find(''), ['a', 'b', 'c', 'd'])
  assert.deepEqual(ids(filterCustomers([{ $id: 'n' }], { stage: 'all', search: 'undefined' })), [])
})

test('stageCounts: dieselbe Filterfunktion wie die Liste', () => {
  assert.deepEqual(stageCounts(ALL), { customer: 2, lead: 1, archived: 2, all: 4 })
  assert.deepEqual(stageCounts(ALL, 'eta'), { customer: 0, lead: 1, archived: 1, all: 1 })
  assert.deepEqual(stageCounts([]), { customer: 0, lead: 0, archived: 0, all: 0 })
})

test('customerBadges: Reihenfolge, Töne, Option stage', () => {
  assert.deepEqual(customerBadges(E), [
    { key: 'important', label: 'Wichtig', tone: 'warn' },
    { key: 'lead', label: 'Lead', tone: 'muted' },
    { key: 'archived', label: 'Archiviert', tone: 'plain' },
  ])
  assert.deepEqual(customerBadges(C), [{ key: 'lost', label: 'Abgesagt', tone: 'plain' }])
  for (const c of [A, D, null]) assert.deepEqual(customerBadges(c), [])
  assert.deepEqual(customerBadges(E, { stage: false }).map((b) => b.key), ['important', 'archived'])
})

test('stageLabel, customerEyebrow, customerTitle', () => {
  assert.deepEqual(['customer', 'lead', 'lost'].map(stageLabel), ['Fester Kunde', 'Lead', 'Abgesagt'])
  assert.equal(customerEyebrow(A), 'K-1 · Fester Kunde')
  assert.equal(customerEyebrow(B), 'Lead')
  assert.equal(customerEyebrow({ code: '  ', customerStatus: 'lost' }), 'Abgesagt')
  assert.equal(customerTitle(A), 'Alpha GmbH')
  assert.equal(customerTitle({ email: 'x@y.de' }), 'x@y.de')
  assert.equal(customerTitle({}), '–')
})
  • Step 2: Test laufen lassen

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs Expected: FAIL, src/lib/customerList.js fehlt.

  • Step 3: customerList.js implementieren

  • customerStage und isArchived werden Zeile für Zeile aus leads.js übernommen.

  • filterCustomers:

    • Ohne Liste [], die Reihenfolge der Eingabe bleibt.
    • Der Reiter archived liefert nur Archivierte, alle anderen Reiter schließen sie aus.
    • Stufe lost erscheint nur im Reiter all. customer und lead liefern ihre Stufe, ein unbekannter Reiter [].
    • Die Suche ist ein Teilstring-Vergleich in Kleinbuchstaben ohne Trim über ${name} ${code} ${location} ${email} ${contactName}, jedes Feld mit || ''. Eine leere Suche filtert nicht.
  • stageCounts: je Eintrag von CUSTOMER_TABS die Länge von filterCustomers(customers, { stage, search }).

  • customerBadges: Wichtig (importantCustomer, Ton warn), dann die Stufe (lead Ton muted, lost „Abgesagt“ Ton plain, customer nichts, nur mit stage: true), dann Archiviert (Ton plain).

  • customerEyebrow = [String(c.code || '').trim(), stageLabel(customerStage(c))].filter(Boolean).join(' · '). customerTitle = c.name || c.email || '–'.

  • Step 4: leads.js umstellen

In src/lib/leads.js werden die Definitionen von customerStage und isArchived durch export { customerStage, isArchived, filterCustomers, stageCounts } from './customerList' ersetzt. Alles andere bleibt, auch isInvoiceReady.

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "from '../lib/leads'" src Expected: drei Importeure (CustomersPage, CustomerDetailPage, AssignProjectPage), alle ohne Änderung gültig.

  • Step 5: Test grün und Build

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs && "$W/check.sh" --nur-build Expected: PASS mit 6 Tests, Build Exit 0.

  • Step 6: Commit
git add src/lib/customerList.js src/lib/leads.js tests/unit/customerList.test.mjs
git commit -m "Kunden: Filter, Zaehler und Abzeichen als reine Funktionen (lib/customerList.js, Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 3: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau)

Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit.

Files (unter $W/):

  • Modify: fixtures/standard.mjs, fixtures/altdaten.mjs, fixtures/viel.mjs, backend.mjs, views.mjs, gates.mjs

Interfaces:

  • Consumes: createBackend, VIEWS, GATES aus P0.

  • Produces: die Ansichten aus Step 3 und das Gate p4.

  • Step 1: Kunden-Testdaten prüfen und ergänzen

  • standard: Der „Abgesagte“ trägt customerStatus: 'lost' (nicht crmStatus). Soll-Zähler: Feste Kunden 6 · Leads 2 · Archiviert 1 · Alle 9.

  • altdaten: zusätzlich zu den Standardkunden fünf Kunden:

    1. Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co. (60 Zeichen), fest, importantCustomer: true, ohne Kundennummer, Standort, E-Mail, Telefon, Ansprechpartner
    2. Lead Frisch Gelead UG mit importantCustomer: true
    3. archivierter Lead Archivierter Wichtiger Lead mit importantCustomer: true
    4. fester Kunde ohne Namen mit E-Mail ohne-name@example.test
    5. fester Kunde mit leerem Namen und ohne E-Mail
  • viel: zusätzlich 300 Kunden Testkunde 001 bis Testkunde 300, 200 fest und 100 Leads. Soll-Zähler: 206 · 102 · 1 · 309.

  • Step 2: Portal-Antworten ergänzen

Falls P0 sie nicht liefert (unter https://portal.test/api/team):

Pfad Antwort
/customers/:id/portal { hasAccess: true, lastLoginAt: '2026-10-04T08:15:00.000Z' }
/customers/:id/abo ein aktives Abo mit Zusatzspeicher, für einen zweiten Kunden { status: '' } („kein Abo“)
/customers/:id/protokoll { entries } mit 5 Einträgen: geoeffnet, news_angelegt mit Zusatztext, termin_aktion mit 120 Zeichen Zusatztext, einer ohne staffName, einer mit unbekannter Aktion xyz
/funktionen/:id { projects } mit den drei Projekten unten

Projekte der Funktionen:

  1. Bäckerei Sonnenschein Website mit liveUrl. News active mit ticketWoid: '100131', Terminplaner setup mit ticketWoid: '100140' und adminNote von 80 Zeichen.
  2. Projekt mit 70 Zeichen langem Namen und liveUrl von 80 Zeichen ohne Leerzeichen. News off, Terminplaner active mit siteKey: 'q74ehq7hvnpiymqy' und ticketMissing: true.
  3. Altes Projekt ohne liveUrl. News disabled mit disabledReason: 'abo_ende' und loeschungAm: '2026-11-04', Terminplaner disabled mit disabledReason: 'abo_ende_vor_einrichtung' und loeschungAm: '2026-11-04'.

Im Szenario fehler antworten /customers/:id/portal, /abo, /protokoll und /funktionen/:id mit 500.

  • Step 3: Ansichten in views.mjs ergänzen

Die Schritte haben die Signatur steps(page, ctx) aus der Übersicht. Eine nicht erfüllte Zusicherung wirft einen Fehler (Verstoß step-failed, in jedem Gate aktiv). zaehlerStimmen klickt jeden der vier Reiter und vergleicht die Zahl im Reiter mit der Zahl der Zeilen tbody tr.row (leer: 0). keinNull wirft, wenn der Seitentext undefined, null oder NaN enthält.

ID Daten Schritte Zusicherung
kunden standard wie P0 Reiter 6 · 2 · 1 · 9, zaehlerStimmen
kunden-leads, kunden-archiv, kunden-alle standard je der Reiter 2 Zeilen mit „Lead“ · 1 Zeile mit „Archiviert“ · 9 Zeilen, davon eine mit „Abgesagt“
kunden-suche standard Suche „Weber“ 1 Zeile, Reiter 1 · 0 · 0 · 1
kunden-altdaten, kunden-altdaten-leads, kunden-altdaten-archiv altdaten Reiter Alle, Leads, Archiviert keinNull, der 60-Zeichen-Name steht vollständig im Link, eine Zeile mit „Wichtig“ und „Lead“, eine mit „Wichtig“, „Lead“ und „Archiviert“
kunden-viel viel wie P0 Reiter 206 · 102 · 1 · 309, zaehlerStimmen
kunden-tastatur standard Tab, bis ein tbody tr a.row-link Fokus hat, dann Enter Fokusring 2 px solid. Pfad beginnt mit /customers/. Nach „Zurück“ erhöht ein Klick in die Zeile neben dem Link history.length um genau 1
dlg-lead-neu, dlg-kunde-neu standard Klick „Neuer Lead“ bzw. „Neuer Kunde“ role="dialog", Titel „Neuer Lead (potenzieller Kunde)“ bzw. „Neuer Kunde mit Portal-Zugang“. Ein Tab landet im Dialog (document.activeElement.closest('.dialog')). Esc schließt, der Fokus kehrt zum Knopf zurück. Danach erneut öffnen
dlg-kunde-neu-fehler standard Name und E-Mail füllen, Passwort „kurz“, absenden .msg.err mit „Das Portal-Passwort muss mindestens 8 Zeichen haben.“, Dialog bleibt offen
zuordnen-kunde-neu standard /projects/zuordnen/<id>, erste Art, „Ja, für einen Kunden“, „Neuer Kunde“ Dialog „Neuer Kunde mit Portal-Zugang“ über der Seite
kunde-overview, kunde-tickets, kunde-invoices, kunde-projects, kunde-chat, kunde-abo, kunde-gutschriften, kunde-funktionen, kunde-protokoll standard die neun Ansichten aus P0 (Reiter-IDs aus src/lib/customerTabs.js, siehe Übersicht). Fehlende werden als /customers/<id>?tab=<reiter> ergänzt. Für dieselbe URL entsteht keine zweite Ansicht keine eigene Zusicherung, außer bei kunde-funktionen (nächste Zeile)
kunde-funktionen (zusätzliche Schritte) standard Tab „Funktionen“, „Abschalten“ in der News-Zeile des ersten Projekts, „Abbrechen“, „Abschalten“ erneut Nach dem ersten Klick steht die Zeile „News abschalten? Die Website zeigt dann keine Beiträge mehr.“ mit Feld „Notiz (optional)“, „Ja“ und „Abbrechen“. „Abbrechen“ schließt sie, am Ende ist sie offen, damit das Bild sie zeigt. Der Text enthält „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ (nur Fall A, siehe Task 1 Step 4)
kunde-lead, kunde-archiv, kunde-altdaten standard, standard, altdaten Kunde „Gärtnerei Grünblick“, „Blumen Lehmann“, Kunde Nr. 1 aus Step 1 Eyebrow „Lead“ mit „Zum festen Kunden machen“ · „Archiviert“ mit „Wiederherstellen“ · keinNull, langer Name in h1 und Brotkrume
kunde-nicht-gefunden standard /customers/gibt-es-nicht „Kunde nicht gefunden“ und Knopf „Zurück“
fehler-kunde, fehler-kunde-funktionen, fehler-kunde-protokoll, fehler-kunde-abo standard, fehler Übersicht bzw. ?tab=funktionen, ?tab=protokoll, ?tab=abo „nicht abrufbar“ beim letzten Login bzw. je ein .msg.err. Ein Klick auf „Übersicht“ zeigt die Stammdaten
  • Step 4: Gate p4 in gates.mjs anlegen

p4 ist p3 plus die P4-Ansichten im bereichsbezogenen Gate icon-svg (Schema der Übersicht). step-failed, external-request und console-error sind ohnehin aktiv. Das Gate deckt auch gutschriften, angebote und speicher ab, die der neue Grundstil in admin-areas.css trifft.

const I3 = GATES.p3.scoped['icon-svg']
GATES.p4 = { ...GATES.p3, scoped: { ...GATES.p3.scoped, 'icon-svg': {
  views: [...I3.views, 'kunden', 'kunden-*', 'dlg-lead-neu', 'dlg-kunde-neu', 'dlg-kunde-neu-fehler', 'kunde-overview', 'kunde-lead', 'kunde-archiv', 'kunde-altdaten', 'kunde-tickets', 'kunde-funktionen', 'kunde-protokoll', 'kunde-nicht-gefunden', 'fehler-kunde', 'fehler-kunde-funktionen', 'fehler-kunde-protokoll'],
  ignore: [...(I3.ignore || []), '.credential'],
} } }

.credential (CopyableCredential) ist bis P5 ausgenommen. Nicht im icon-svg-Umfang sind kunde-abo und fehler-kunde-abo (Angebote, P6), kunde-projects (P5), kunde-chat, kunde-invoices, kunde-gutschriften und zuordnen-kunde-neu (fremde Bausteine). Deshalb stehen die fehler-kunde-*-Ansichten einzeln in der Liste. Ihre P4-Anteile prüft Task 8 über rg auf Icon-Importe.

  • Step 5: Vorher-Lauf (muss scheitern)

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4-vorher && "$W/shots.sh" p4-vorher "$W/shots/p4-vorher" --only kunden,kunde-overview,kunde-funktionen,kunde-protokoll --gate p4 Expected: Exit 1 wegen icon-svg. Die Bilder in shots/p4-vorher/1440 und 390 sind die Vorher-Bilder mit den neuen Testdaten.

Task 4: Kundenliste und Dialoge

Files:

  • Modify: src/pages/CustomersPage.jsx (Zeilen 1–208 ersetzen)
  • Modify: src/pages/AssignProjectPage.jsx (Zeilen 275–289: Aufruf von CreateCustomerForm)
  • Modify: src/styles/global.css (nur die KOMPAT-Regel .side-logo löschen)

Interfaces:

  • Consumes: PageHeader, Tabs, Badge, Button, EmptyState, Field, Dialog (P1). CUSTOMER_TABS, filterCustomers, stageCounts, customerBadges, customerTitle (Task 2). isRowActivation aus src/lib/rowClick.js (P2). createLead, useCustomers, useAuth unverändert.

  • Produces:

    • export function CreateCustomerForm({ onCancel, onCreate }) rendert einen wide Dialog. Die Signatur bleibt, P5 braucht sie.
    • CreateLeadForm({ onCancel, onDone }) (nicht exportiert) rendert einen Dialog.
  • Step 1: Seite neu aufbauen

Element Umsetzung
Kopf PageHeader title="Kunden" subtitle="Potenzielle Kunden (Leads) und feste Kunden – eine Pflege". Aktionen: für Admins Button variant="primary" „Neuer Kunde“, für alle Button „Neuer Lead“. Beide setzen showCreate direkt, kein Umschalten mehr
Reiter Tabs mit CUSTOMER_TABS und count = stageCounts(customers, search)[id], bis zum Laden ohne Zähler
Suche <div className="toolbar"> mit <input className="input search" type="search" placeholder="Kunde suchen …" aria-label="Kunde suchen">, filtert sofort wie heute
Laden, leer <div className="loading">Wird geladen …</div>. Leer: EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht."
Tabelle .table, <tr className="row"> mit onClick, der bei isRowActivation(e) zu /customers/<$id> navigiert
Dialoge showCreate === 'lead' rendert CreateLeadForm, 'customer' und isAdmin rendert CreateCustomerForm, mit denselben Handlern wie heute. Bei Erfolg schließt die Seite, setzt bei Kunden den Reiter auf Feste Kunden und lädt neu

Spalten (Klasse gilt für Kopf und Zelle):

Kopf Klasse Inhalt
Unternehmen – <div className="cell-title"> mit <Link className="row-link" to=…>{customerTitle(c)}</Link> und dahinter den Badges aus customerBadges(c). Darunter .cell-sub mit contactName, nur wenn vorhanden
Kundennummer hide-sm num code, sonst <span className="faint">–</span>
Standort – location, sonst „–“
E-Mail hide-md muted email, sonst „–“
Telefon hide-md muted nowrap num phone, sonst „–“

Formulare (jedes Feld ist ein Field mit htmlFor und id, Dialog bekommt onClose={onCancel} und busy, Fehler wie in P2 als .msg.err oben im Körper):

  • CreateLeadForm: as="form", Titel „Neuer Lead (potenzieller Kunde)“. Im Körper der Hinweis „Fuer einen Lead reichen Unternehmen und E-Mail. Es wird automatisch ein Akquise-Ticket erstellt.“ (.muted .small), dann .field-row mit „Unternehmen“ (required) und „E-Mail“ (type="email"). Ein Fehler steht als .msg.err mit role="alert" als erstes Kind des Körpers. Fuß: <Button type="button"> „Abbrechen“ und Button variant="primary" type="submit" „Lead anlegen + Akquise-Ticket“ (bei busy „Speichert …“). „Unternehmen erforderlich“ bleibt.

  • CreateCustomerForm: wide, Titel „Neuer Kunde mit Portal-Zugang“, zwei .field-row-3: „Unternehmen / Name“, „E-Mail (Portal-Login)“, „Kundennummer“ und „Telefon“, „Standort“, „Portal-Passwort (mind. 8 Zeichen)“. name, required, type und autoComplete="new-password" bleiben, ebenso die Texte „Das Portal-Passwort muss mindestens 8 Zeichen haben.“ und „Kunde konnte nicht angelegt werden.“. Fuß: <Button type="button"> „Abbrechen“ und Button variant="primary" type="submit" „Kunde anlegen“ (bei busy „Speichert …“).

  • Step 2: Aufruf im Zuordnen-Assistenten

In AssignProjectPage.jsx entfällt nur der Umschlag <div style={{ marginBottom: 12 }}> um CreateCustomerForm, der Dialog liegt über der Seite. Props und Handler bleiben. Der Rest der Datei gehört zu P5.

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat src/pages/AssignProjectPage.jsx Expected: nur wenige Zeilen geändert (der Umschlag).

  • Step 3: Hygiene prüfen, .side-logo löschen

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge-|\bdot\b" src/pages/CustomersPage.jsx; rg -w side-logo src Expected: Aus CustomersPage.jsx keine Treffer. side-logo steht nur noch als KOMPAT-Regel in src/styles/global.css, keine Datei benutzt es. Die Spec lässt die Regel bis P4 stehen, deshalb wird sie jetzt gelöscht (samt Hover- und Kindregeln).

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -w side-logo src Expected: keine Treffer.

  • Step 4: Bauen und prüfen

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t4" --only kunden,kunden-leads,kunden-archiv,kunden-alle,kunden-suche,kunden-altdaten,kunden-altdaten-leads,kunden-altdaten-archiv,kunden-viel,kunden-tastatur,dlg-lead-neu,dlg-kunde-neu,dlg-kunde-neu-fehler,zuordnen-kunde-neu --gate p4 Expected: Exit 0 und alle Zusicherungen aus Task 3. Die Bilder zeigen Haarlinien, schwarzes „Neuer Kunde“ und Standard-„Neuer Lead“. Bei 390 px bleiben Unternehmen und Standort.

  • Step 5: Commit
git add src/pages/CustomersPage.jsx src/pages/AssignProjectPage.jsx src/styles/global.css
git commit -m "Kundenliste als Tabelle mit Reitern und Zaehlern, Neuer Kunde und Neuer Lead als Dialoge" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 5: Kundenseite und Kundenansicht-Knopf

Files:

  • Modify: src/pages/CustomerDetailPage.jsx (Kopf Zeilen 56–124, OverviewTab 126–212, PortalAccessCard 214–298, CustomerTickets 300–344, CustomerProjects 346–370)
  • Modify: src/components/customer/KundenansichtButton.jsx (Zeilen 1–36)
  • Modify: src/styles/global.css (neuer Block „Kunden (P4)“ vor dem KOMPAT-Block)

Interfaces:

  • Consumes: PageHeader (crumb, eyebrow, subtitle), Tabs, Card, Badge, Button, EmptyState, StatusPill, UrgentPill (P1). customerEyebrow, customerBadges, customerTitle, customerStage, isArchived (Task 2). ticketPath(ref), ticketRef(ticket) (P2), ticketNumber(ticket) (P2). isRowActivation aus src/lib/rowClick.js (P2).

  • Produces: KundenansichtButton({ customer, variant = 'default', checkAccess = true }). Die Voreinstellung wechselt von primary auf default. Die anderen Aufrufer (CreditDetail, P6) übergeben ghost und sehen gleich aus. Sonst nichts Neues, die Aufrufe der Reiter-Inhalte bleiben wie heute.

  • Step 1: Kopf prüfen und umbauen

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "subtitle" src/components/ui/PageHeader.jsx Expected: subtitle steht in .sub und nimmt einen Knoten an. Die Abzeichen sind span und damit auch in einem <p> gültig.

Element Umsetzung
Laden <div className="page"><div className="loading">Wird geladen …</div></div>
Nicht gefunden EmptyState title="Kunde nicht gefunden" hint={error} mit Button as={Link} to="/customers" „Zurück“ (ohne Pfeil)
Kopf crumb={[{ label: 'Kunden', to: '/customers' }, { label: customerTitle(customer) }]}, eyebrow={customerEyebrow(customer)}, title={customerTitle(customer)}. .sub: Abzeichen aus customerBadges(customer, { stage: false }), dann [location, email, phone].filter(Boolean).join(' · '), beides leer: kein .sub
Aktionen Button variant="danger" „Kunde löschen“ (nur Admin), Button „Wiederherstellen“ bzw. „Archivieren“, KundenansichtButton. removeCustomer (mit window.confirm/window.alert) und toggleArchived bleiben

KundenansichtButton: Icon und Spinner entfallen, der Text „Kundenansicht öffnen“ bleibt, bei busy steht „Wird geladen …“. title, die disabled-Regel, window.alert und openInNewTab bleiben. PortalAccessCard übergibt keine variant mehr.

Reiter: Tabs tabs={customerTabs(isAdmin)}, activeTab, ?tab= und setSearchParams(…, { replace: true }) bleiben. Die neun Reiter:

id Label Inhalt
overview Übersicht OverviewTab (P4)
tickets Tickets CustomerTickets (P4)
invoices Rechnungen (nur Admins) CustomerInvoices unverändert (P7)
projects Projekte CustomerProjects mit AssignedProjectCard (P5)
chat Chat <Card><ChatThread …/></Card> unverändert (P6)
abo Abo & Angebote AboStatusCard (Task 7) und OffersView (P6), ohne Umschlag mit Inline-Style
gutschriften Gutschriften CreditsView unverändert (P6)
funktionen Funktionen CustomerFunktionen (Task 6)
protokoll Protokoll CustomerViewLog (Task 7)
  • Step 2: Übersicht

Wurzel <div className="two-col"> mit zwei <div>-Spalten. Links Card title="Stammdaten" und Card title="Rechnungsdaten", rechts PortalAccessCard. State und Handler (save, upgrade, savePassword) bleiben, ebenso alle Texte und dass der Name companyName mitschreibt. Aus den Field-Zeilen werden .kv-Zeilen (<dt><label htmlFor>, <dd> mit className="input"), jedes Feld bekommt eine id.

Abschnitt Inhalt in dieser Reihenfolge
Stammdaten Unternehmen / Name, Code, E-Mail (type="email"), Telefon, Standort, Ansprechpartner, Notizen (<textarea className="textarea" rows={3}>). Darunter <label className="check"> „Wichtiger Kunde“, dann .toolbar mit Button variant="primary" „Speichern“ (bei busy „Speichert …“), bei Leads Button „Zum festen Kunden machen“ und der Meldung
Rechnungsdaten oben <Badge tone="warn">Adresse unvollständig</Badge> bei !ready, dann Strasse + Nr., PLZ, Stadt, Land, USt-IdNr.
Portal-Zugang Kundenportal (Badge ok „freigeschaltet“ bzw. muted „kein Zugang“), Letzter Login („…“, „nicht abrufbar“ oder formatLastLogin), Login (E-Mail) und Passwort als CopyableCredential, das Feld „Neues Passwort setzen“ bzw. „Passwort festlegen (min. 8 Zeichen)“, .toolbar mit Button „Passwort ändern“ bzw. „Zugang anlegen“ (bei busy „Speichert …“), KundenansichtButton, Meldung, zuletzt der Portal-Hinweis als <p className="muted small">

Meldungen: msg wird { tone, text } mit den bisherigen Texten. Fehler und Pflichthinweise sind err bzw. warn (mit role="alert"), Erfolge ok.

  • Step 3: Tickets- und Projekte-Reiter

  • CustomerTickets: Abfrage und Zustände bleiben. Laden .loading, leer EmptyState title="Keine Tickets" hint="Fuer diesen Kunden gibt es noch keine Tickets.".

  • Tabelle „Ticket“ und „Status“:

    • Erste Zelle <div className="cell-title"> mit <Link className="row-link" to={ticketPath(ticketRef(t))}>, darin <span className="id">{ticketNumber(t)}</span> und der Betreff (topic || title || '-'), daneben UrgentPill. Darunter .cell-sub mit t.type. Die Kurznummer ist nie Linkziel.
    • Zweite Zelle StatusPill. <tr className="row"> mit isRowActivation wie in Task 4.
  • CustomerProjects: Laden .loading, leer wie heute, sonst Card title={Projekte (${n})} mit AssignedProjectCard.

  • Step 4: CSS-Block „Kunden (P4)“

Der Block steht hinter den Bausteinen und vor dem KOMPAT-Block und beginnt mit /* ===== Kunden (P4) ===== */:

  • .kv .textarea { width: 100%; max-width: 360px } (die Prototypregel gilt nur für .input)

Den Abstand zwischen aufeinanderfolgenden Abschnitten in den Spalten (h2.section) liefert P1 mit .page section + section { margin-top: 36px }. P4 baut dafür keine eigene Regel.

  • Step 5: Hygiene und Prüfung

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge badge-|\bdot\b" src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx Expected: keine Treffer.

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t5" --only kunde-overview,kunde-tickets,kunde-projects,kunde-lead,kunde-archiv,kunde-altdaten,kunde-nicht-gefunden,kunde-chat,kunde-invoices,kunde-gutschriften --gate p4 Expected: Exit 0. kunde-overview zeigt zwei Spalten mit .kv, der Abstand zwischen Stammdaten und Rechnungsdaten beträgt mindestens 24 px (36 px aus P1), der Kopf hat keinen schwarzen Knopf. Bei 390 px stehen die Spalten untereinander, und die Reiter scrollen waagerecht.

  • Step 6: Commit
git add src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx src/styles/global.css
git commit -m "Kundenseite: Brotkrumen, Eyebrow, Uebersicht in zwei Spalten, Tickets-Reiter mit Links" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 6: Funktionen als Tabelle

Files:

  • Modify: src/components/customer/CustomerFunktionen.jsx (Zeilen 1–130, die Logik bleibt)
  • Modify: src/styles/global.css (Block „Kunden (P4)“ aus Task 5: Funktionen-Regeln)
  • Modify: src/styles/admin-areas.css (Funktionen-Regeln löschen: Zeilen 24–32, 36 und 60)

Interfaces:

  • Consumes: FUNKTIONEN, ZUSTAND, funktionOf, aktionen, mergeFunktion, siteKeyOf und, nur in Fall A (Task 1 Step 4), abgeschaltetText aus lib/funktionenView (Stand von test, nicht geändert). copyText aus CopyableCredential. getFunktionen, setFeature. ticketPath (P2).

  • Produces: CustomerFunktionen({ customerId }), gleiche Signatur.

  • Step 1: Markup

State, load, save, ask und short bleiben. Jede Bedienung von heute bleibt erreichbar, alle Texte bleiben. Der Import von admin-areas.css entfällt, die Funktionen-Regeln wandern nach global.css, damit P6 die Datei ohne P4-Nutzer löschen kann.

Element Umsetzung
Zustände Fehler <div className="msg err" role="alert">. Laden .loading „Wird geladen …“. Leer EmptyState title="Keine Projekte für diesen Kunden."
Rahmen Card title="Funktionen" mit <table className="table funktionen-table">, Kopf „Funktion“, „Status“, „Ticket“, „Aktionen“
Projektzeile je Projekt ein <tbody>, dessen erste Zeile <tr className="funktionen-group"><th colSpan={4} scope="rowgroup"> den Projektnamen trägt. Dahinter liveUrl ohne Protokoll als <a className="link" target="_blank" rel="noreferrer">
Funktionszeile je Funktion: f.label, Status, Ticket, Aktionen
Status Badge tone={tone} mit label. Darunter (Fall A) .cell-sub mit abgeschaltetText(feature) (wenn nicht leer), .cell-sub mit short(feature.adminNote) und title={feature.adminNote} und bei siteKeyOf(...) der SiteKey
SiteKey .funktionen-sitekey mit „Site-Key“ (.muted), <code> und <button type="button" className="link"> „Kopieren“ bzw. „Kopiert“ (1,8 s). title ist „Site-Key kopieren“ bzw. „Kopiert“, aria-label „Site-Key kopieren“ bzw. „Site-Key kopiert“, beides wie heute. Kein Icon
Ticket ticketMissing → <Badge tone="danger">Ticket fehlt</Badge>. Mit ticketWoid → <Link className="link" to={ticketPath(feature.ticketWoid)}>#{feature.ticketWoid}</Link>. Sonst <span className="faint">–</span>
Aktionen je aktionen(feature.state) ein <button type="button" className="link"> „Aktivieren (ohne Zahlung)“ bzw. „Abschalten“ in .funktionen-actions. Solange die Bestätigung dieser Zeile offen ist, bleibt die Zelle leer
Bestätigung Direkt unter der Funktionszeile <tr className="funktionen-confirm"><td colSpan={4}> mit einem .flex.items-center.gap-2.wrap: Text f.bestaetigen[confirm.action], <input className="input funktionen-note" placeholder="Notiz (optional)" maxLength={120} aria-label="Notiz (optional)">, Button variant="primary" „Ja“ (bei busy „Speichert …“, disabled) und Button „Abbrechen“ (disabled bei busy)
Rückmeldung unter der Tabelle <div className={msg ${status.ok ? 'ok' : 'err'}}>, bei Fehler mit role="alert"
tp2-Zusatz (nur Fall A) Die Datei enthält zusätzlich KopierKnopf, AnleitungPanel, TextformPanel, den Knopf „Anleitung“ (aria-expanded) und hindernisText beim Status. Logik, Zustände und Texte bleiben. Umbau wie oben: Knöpfe ohne size und Icons (Button type="button" bzw. button.link), der Spinner wird „Wird geladen …“, Kopierknöpfe zeigen „Kopiert“ bzw. ihren Namen, ein Icon-Knopf zeigt seinen aria-label-Text, form-control wird input, Inline-Styles werden Klassen im Block „Kunden (P4)“, .area-* wird .msg err, .msg warn bzw. .muted. Die Panels stehen als <tr className="funktionen-extra"><td colSpan={4}> direkt unter der Funktionszeile, <details> bleibt
  • Step 2: CSS verschieben

Aus admin-areas.css entfallen alle .funktionen-*-Regeln samt .funktionen-leer in den beiden Responsive-Blöcken. In Fall A ziehen auch die .funktionen-panel*-Regeln von tp2 um, mit Tokens statt --surface-* und --border (--bg-subtle, --line). Im Block „Kunden (P4)“ von global.css stehen:

Selektor Wert
.funktionen-group th text-align: left; font-size: 14px; font-weight: 600; color: var(--text); padding-top: 20px; overflow-wrap: anywhere
.funktionen-group .link font-size: 13px; font-weight: 400; margin-left: 8px
.funktionen-sitekey display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 4px
.funktionen-sitekey code font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; user-select: all
.funktionen-actions display: flex; flex-wrap: wrap; gap: 4px 14px
.funktionen-confirm td padding-top: 0
.funktionen-note width: 100%; max-width: 260px
  • Step 3: Hygiene und Prüfung

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|area-|admin-areas|ui-btn|size=" src/components/customer/CustomerFunktionen.jsx; rg -n "funktionen" src/styles/admin-areas.css Expected: keine Treffer.

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t6" --only kunde-funktionen,fehler-kunde-funktionen --gate p4 Expected: Exit 0. Das Bild von kunde-funktionen zeigt die geöffnete Bestätigungszeile. Bei 390 px läuft nichts über (Site-Key und lange URL brechen um), in Fall A ist „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ lesbar, und jeder Textlink zeigt den Fokusring.

  • Step 4: Commit
git add src/components/customer/CustomerFunktionen.jsx src/styles/global.css src/styles/admin-areas.css
git commit -m "Kunden-Reiter Funktionen als Tabelle mit Textaktionen, Bestaetigung als eigene Zeile" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 7: Abo-Status, Protokoll und Grundstil von admin-areas.css

Files:

  • Modify: src/components/customer/AboStatusCard.jsx (Zeilen 1–44)
  • Modify: src/components/customer/CustomerViewLog.jsx (Zeilen 1–56)
  • Modify: src/styles/admin-areas.css (Zeilen 1–61)

Interfaces:

  • Consumes: Card, Badge, Button, EmptyState. getCustomerAboStatus, getCustomerViewLog. formatViewLogTime, viewLogLabel.

  • Produces: den Grundstil der .area-*-Klassen für die P6-Seiten. Die P4-Dateien benutzen keine .area-*-Klasse und keinen Import von admin-areas.css mehr.

  • Step 1: AboStatusCard

Card title="Abo". Der Import von admin-areas.css entfällt. Laden .loading, Fehler <div className="msg err" role="alert">, ohne Abo <p className="muted">kein Abo</p>. Die Zeilen werden <dl className="kv">: „Abo“ (Produktname plus Badge, im <dd className="flex items-center gap-2 wrap">), „Leistungsbeginn“, „Nächste Abbuchung“ (nur bei abo.active), „Gekündigt zum“, „Zusatzspeicher“. Texte und Bedingungen wie heute.

  • Step 2: CustomerViewLog

  • Fehler: .msg.err mit der Meldung und daneben Button „Erneut versuchen“ (ohne Icon, onClick={load}), beides in einem .flex.items-center.gap-2.wrap. Laden .loading. Leer wie heute (EmptyState title="Noch keine Einträge").

  • Tabelle „Zeit“ (formatViewLogTime(e.at), nowrap num), „Mitarbeiter“ (e.staffName || '–'), „Aktion“ (viewLogLabel(e.action), darunter .cell-sub mit e.detail). Keine Zeilenaktion, keine Links.

  • Step 3: Grundstil von admin-areas.css

Regel Neu
.area-row padding: 12px 0 statt 12px 16px, Haarlinie var(--line)
.area-row.head color: var(--text-2); font-size: 12px; font-weight: 500, ohne text-transform und letter-spacing
.area-row.active background: var(--bg-subtle)
.area-muted, .area-danger, .area-warn 13 px in var(--text-2), var(--red-fg), var(--orange-fg)
.area-summary ohne Hintergrund, Radius und Innenabstand, margin-bottom: 12px, .warn färbt die Schrift var(--orange-fg)
.area-message, .area-error wie .msg.ok und .msg.err: 13 px, var(--green-fg) bzw. var(--red-fg), ohne Kasten, Abstand 8px 0
.area-kv dt, .credit-calc label, .credit-field color: var(--text-2)
.area-rows a, .area-rows a:hover, .credit-cols a entfallen. Links ohne Klasse bekommen Farbe und feinen Unterstrich von a:not([class]) (P1). admin-areas.css lädt vor global.css, eine eigene Regel mit gleicher Spezifität würde dort verlieren
Responsive Beide Blöcke (@media (max-width: 720px) wird 860px, und @container (max-width: 960px)) stehen am Dateiende hinter allen Spaltenregeln, !important entfällt in beiden. Bei gleicher Spezifität gewinnt die spätere Regel. Hat ein Spaltenselektor mehr als eine Klasse, bekommt der Responsive-Selektor dieselben Klassen
Bleiben bis P6 .storage-row, .credit-row, .credit-cols, .credit-*, .offer-row, .area-filter, .area-inline, .area-kv

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "!important|#[0-9a-fA-F]{3,6}\b|rgba\(|--text-muted|--text-faint|--surface|--border|--r-sm|--ok\b|--accent" src/styles/admin-areas.css Expected: keine Treffer.

  • Step 4: Hygiene und Prüfung

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|area-|admin-areas" src/components/customer Expected: keine Treffer.

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t7" --only kunde-abo,kunde-protokoll,fehler-kunde-protokoll,fehler-kunde-abo,kunde-gutschriften,gutschriften,angebote,speicher --gate p4 Expected: Exit 0. Auf gutschriften, angebote und speicher stehen die Zeilen mit Haarlinien ohne Kästen und die Kopfzeilen ohne Großbuchstaben. Sie bleiben lesbar, P6 ersetzt sie.

  • Step 5: Commit
git add src/components/customer/AboStatusCard.jsx src/components/customer/CustomerViewLog.jsx src/styles/admin-areas.css
git commit -m "Abo-Status und Protokoll ohne Karten und Icons, neuer Grundstil fuer admin-areas.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"

Task 8: Gate P4, Review, Tag

  • Step 1: Rebase

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 = N3 + 6. Liegen neue Commits auf test, kann die Zahl höher sein; sie wird notiert.

  • Step 2: Vollständiger Gate-Lauf

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4" --gate p4 Expected:

  • Gate p4 Exit 0.

  • Keine Verstöße gegen dark-surface, low-contrast, overflow-x, console-error, external-request und step-failed auf allen Ansichten, auch auf gutschriften, angebote und speicher.

  • Die Kundenansichten aus dem icon-svg-Umfang haben kein icon-svg außerhalb .credential.

  • Step 3: Code-Hygiene im umgebauten Bereich

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{\{|/tickets\?woid" src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer; rg -w side-logo src Expected: keine Treffer. AssignProjectPage.jsx behält seine Icons bis P5.

  • Step 4: Bundle-Größe

Die Größen stehen in der Ausgabe von check.sh (Step 1). Sie werden mit design-p3 verglichen. Das JS wird kleiner (Icon-Importe in sieben Dateien entfallen).

  • Step 5: Code-Review (sonnet)

Geprüft wird der Diff design-p3..HEAD gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P4. Schwerpunkte:

  • Texte unverändert (Diff-Stichprobe der Formulare und Meldungen)
  • Zeilenklick-Logik und erste Zelle als echter Link
  • Dialog-Verdrahtung (busy, Esc, onClose)
  • ticketPath als einziges Linkziel auf Tickets
  • admin-areas.css ohne !important
  • lib/leads.js exportiert weiter alle bisherigen Namen (git diff design-p3 -- src/lib/leads.js)
  • jede Bedienung aus Task 4 bis 7 ist erreichbar (Kopfknöpfe, neun Reiter, beide Formulare, Aktivieren, Abschalten, Bestätigung mit Notiz, Site-Key kopieren, „Erneut versuchen“)
  • „Abbrechen“ in beiden Anlege-Dialogen hat type="button"

Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit --only kunden,kunde-overview,dlg-kunde-neu.

  • Step 6: Tag und Übergabe

Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p4 && git log --oneline origin/test..design-p4 && git rev-parse design-p4

Der Eigentümer bekommt:

  • die Screenshots aus shots/p4/1440x900 und shots/p4/390x844 (Auswahl: kunden, kunden-altdaten-archiv, dlg-kunde-neu, kunde-overview, kunde-tickets, kunde-funktionen, kunde-protokoll) und den Vorher-Vergleich aus shots/p4-vorher

  • den Hinweis: „Fester Kunde“ gibt es in der Liste nicht mehr als Abzeichen, „Wichtig“ ersetzt den Stern, die Suche findet jetzt auch den Ansprechpartner

  • den tp2-Stand aus Task 1: Fall A (tp2 war vor P4 in test, das Anleitungs- und Zustimmungs-Panel ist in die Zusatzzeilen der Tabelle übernommen) oder Fall B (P4 ging vor tp2, die Konflikte in CustomerFunktionen.jsx, lib/funktionenView.js und admin-areas.css löst die vom Eigentümer genannte Stelle beim Rebase von tp2 auf, die Panels müssen dann in die Zusatzzeile funktionen-extra der Tabelle)

  • Notizen für spätere Pakete:

    • P5: CopyableCredential und AssignedProjectCard erscheinen im Portal-Zugang und im Projekte-Reiter noch in alter Form, die Icons von AssignProjectPage.jsx sind unberührt.
    • P6: KundenansichtButton ist jetzt ein Standardknopf, in CreditDetail bleibt „Genehmigen“ der einzige schwarze Knopf. Die Raster .storage-row, .credit-row, .offer-row und .area-kv stehen noch in admin-areas.css.
  • die Push-Zeile git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p4^{commit}:refs/heads/test'

  • die Rücknahme: git revert --no-commit design-p3..design-p4, Commit und Push durch den Eigentümer (gilt nur, solange P4 das jüngste Paket ist)

  • Step 7: 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' entspricht dist/index.html des Tags.

  • Ein lesender Sichttest im Chrome des Eigentümers: Kundenliste mit Reitern, eine Kundenseite mit Tickets-Reiter und Funktionen.