55 KiB
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.jsmit Tests.lib/leads.jsimportiert Appwrite und ist unter Node nicht ladbar. Deshalb ziehencustomerStageundisArchiveddorthin um, undleads.jsre-exportiert sie. Alle bestehenden Importe bleiben gültig. - Tabellenzeilen haben als erste Zelle einen echten Link, der Zeilenklick läuft über
isRowActivationaussrc/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 insrc/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-p3und aus P2ticketPathundticketRef(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 inglobal.css(dazu fällt dort die KOMPAT-Regel.side-logo), inlib/leads.jszwei Funktionen (Umzug) und eine Stelle inAssignProjectPage.jsx(Aufruf vonCreateCustomerForm). Neu sindlib/customerList.jsund sein Test. - Unverändert bleiben
lib/customerTabs.js(der Test pinnt[id, label], Zähler kommen beim Rendern),lib/funktionenView.js,useCustomers,customerAdminApi,teamApi,CopyableCredentialundAssignedProjectCard(P5), Chat, Gutschriften, Angebote (P6) undCustomerInvoices(P7). - tp2 zuerst klären:
feature/terminplaner-tp2hat 5 Commits (6f03712,c001554,857e001,45e5dc3,225e2f1) vortest. Sie ändernCustomerFunktionen.jsx,lib/funktionenView.js,admin-areas.cssund Rechtliches. P4 bautCustomerFunktionen.jsxundadmin-areas.cssneu. 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 keinedata-testidin 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 Archiviertplain, 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):
CreateLeadFormim Standarddialog mit einer.field-row,CreateCustomerFormwidemit zwei.field-row-3. Fuß wie im Prototyp:<span className="grow" />, Abbrechen (Standardknopf mittype="button", sonst löst er in<Dialog as="form">das Absenden aus), Absenden (schwarz,type="submit"). Abbrechen ist wie × und Esc währendbusygesperrt. Beide Formulare bleiben inCustomersPage.jsx, der Export vonCreateCustomerFormbleibt. - 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.
- 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 nieundefined,nulloderNaN. - 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.
- Teilausfall (
fehler-kunde*): Antworten die Portal-Wegeportal,abo,protokollundfunktionenmit 500, zeigen Protokoll, Abo und Funktionen.msg.err, die Übersicht „nicht abrufbar“. Die Reiter bleiben bedienbar. - 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. - 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-p3und die P2-BausteineticketPath,ticketRef,ticketNumber,isRowActivationund.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ägtmerge-basefehl, istdesign-p3nicht im Branch: Stopp und melden. -
Die Zahl
# passist 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
testzu bringen. - Er holt keinen Commit einzeln per Cherry-Pick.
6f03712allein ließeCustomerFunktionen.jsxundlib/funktionenView.jsin 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.jsundadmin-areas.cssspä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 inkunde-funktionen.
Task 2: customerList.js (Filter, Zähler, Abzeichen)
Files:
- Create:
src/lib/customerList.js - Modify:
src/lib/leads.js(Zeilen 8–18:customerStageundisArchivedsamt 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 aussrc/lib/leads.js:customerStage(c) → 'lead' | 'lost' | 'customer'undisArchived(c) → boolean, unverändert verschobenfilterCustomers(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.jsimplementieren -
customerStageundisArchivedwerden Zeile für Zeile ausleads.jsübernommen. -
filterCustomers:- Ohne Liste
[], die Reihenfolge der Eingabe bleibt. - Der Reiter
archivedliefert nur Archivierte, alle anderen Reiter schließen sie aus. - Stufe
losterscheint nur im Reiterall.customerundleadliefern 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.
- Ohne Liste
-
stageCounts: je Eintrag vonCUSTOMER_TABSdie Länge vonfilterCustomers(customers, { stage, search }). -
customerBadges: Wichtig (importantCustomer, Tonwarn), dann die Stufe (leadTonmuted,lost„Abgesagt“ Tonplain,customernichts, nur mitstage: true), dann Archiviert (Tonplain). -
customerEyebrow=[String(c.code || '').trim(), stageLabel(customerStage(c))].filter(Boolean).join(' · ').customerTitle=c.name || c.email || '–'. -
Step 4:
leads.jsumstellen
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,GATESaus P0. -
Produces: die Ansichten aus Step 3 und das Gate
p4. -
Step 1: Kunden-Testdaten prüfen und ergänzen
-
standard: Der „Abgesagte“ trägtcustomerStatus: 'lost'(nichtcrmStatus). Soll-Zähler: Feste Kunden 6 · Leads 2 · Archiviert 1 · Alle 9. -
altdaten: zusätzlich zu den Standardkunden fünf Kunden:Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.(60 Zeichen), fest,importantCustomer: true, ohne Kundennummer, Standort, E-Mail, Telefon, Ansprechpartner- Lead
Frisch Gelead UGmitimportantCustomer: true - archivierter Lead
Archivierter Wichtiger LeadmitimportantCustomer: true - fester Kunde ohne Namen mit E-Mail
ohne-name@example.test - fester Kunde mit leerem Namen und ohne E-Mail
-
viel: zusätzlich 300 KundenTestkunde 001bisTestkunde 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:
Bäckerei Sonnenschein WebsitemitliveUrl. NewsactivemitticketWoid: '100131', TerminplanersetupmitticketWoid: '100140'undadminNotevon 80 Zeichen.- Projekt mit 70 Zeichen langem Namen und
liveUrlvon 80 Zeichen ohne Leerzeichen. Newsoff, TerminplaneractivemitsiteKey: 'q74ehq7hvnpiymqy'undticketMissing: true. Altes ProjektohneliveUrl. NewsdisabledmitdisabledReason: 'abo_ende'undloeschungAm: '2026-11-04', TerminplanerdisabledmitdisabledReason: 'abo_ende_vor_einrichtung'undloeschungAm: '2026-11-04'.
Im Szenario fehler antworten /customers/:id/portal, /abo, /protokoll und /funktionen/:id mit 500.
- Step 3: Ansichten in
views.mjsergä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
p4ingates.mjsanlegen
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 vonCreateCustomerForm) - Modify:
src/styles/global.css(nur die KOMPAT-Regel.side-logolöschen)
Interfaces:
-
Consumes:
PageHeader,Tabs,Badge,Button,EmptyState,Field,Dialog(P1).CUSTOMER_TABS,filterCustomers,stageCounts,customerBadges,customerTitle(Task 2).isRowActivationaussrc/lib/rowClick.js(P2).createLead,useCustomers,useAuthunverändert. -
Produces:
export function CreateCustomerForm({ onCancel, onCreate })rendert einenwideDialog. 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 „–“ |
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-rowmit „Unternehmen“ (required) und „E-Mail“ (type="email"). Ein Fehler steht als.msg.errmitrole="alert"als erstes Kind des Körpers. Fuß:<Button type="button">„Abbrechen“ undButton variant="primary" type="submit"„Lead anlegen + Akquise-Ticket“ (beibusy„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,typeundautoComplete="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“ undButton variant="primary" type="submit"„Kunde anlegen“ (beibusy„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-logolö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,OverviewTab126–212,PortalAccessCard214–298,CustomerTickets300–344,CustomerProjects346–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).isRowActivationaussrc/lib/rowClick.js(P2). -
Produces:
KundenansichtButton({ customer, variant = 'default', checkAccess = true }). Die Voreinstellung wechselt vonprimaryaufdefault. Die anderen Aufrufer (CreditDetail, P6) übergebenghostund 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, leerEmptyState 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 || '-'), danebenUrgentPill. Darunter.cell-submitt.type. Die Kurznummer ist nie Linkziel. - Zweite Zelle
StatusPill.<tr className="row">mitisRowActivationwie in Task 4.
- Erste Zelle
-
CustomerProjects: Laden.loading, leer wie heute, sonstCard title={Projekte (${n})}mitAssignedProjectCard. -
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,siteKeyOfund, nur in Fall A (Task 1 Step 4),abgeschaltetTextauslib/funktionenView(Stand vontest, nicht geändert).copyTextausCopyableCredential.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 vonadmin-areas.cssmehr. -
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.errmit der Meldung und danebenButton„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-submite.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
p4Exit 0. -
Keine Verstöße gegen
dark-surface,low-contrast,overflow-x,console-error,external-requestundstep-failedauf allen Ansichten, auch aufgutschriften,angeboteundspeicher. -
Die Kundenansichten aus dem
icon-svg-Umfang haben keinicon-svgauß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) ticketPathals einziges Linkziel auf Ticketsadmin-areas.cssohne!importantlib/leads.jsexportiert 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/1440x900undshots/p4/390x844(Auswahl:kunden,kunden-altdaten-archiv,dlg-kunde-neu,kunde-overview,kunde-tickets,kunde-funktionen,kunde-protokoll) und den Vorher-Vergleich ausshots/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 inCustomerFunktionen.jsx,lib/funktionenView.jsundadmin-areas.csslöst die vom Eigentümer genannte Stelle beim Rebase von tp2 auf, die Panels müssen dann in die Zusatzzeilefunktionen-extrader Tabelle) -
Notizen für spätere Pakete:
- P5:
CopyableCredentialundAssignedProjectCarderscheinen im Portal-Zugang und im Projekte-Reiter noch in alter Form, die Icons vonAssignProjectPage.jsxsind unberührt. - P6:
KundenansichtButtonist jetzt ein Standardknopf, inCreditDetailbleibt „Genehmigen“ der einzige schwarze Knopf. Die Raster.storage-row,.credit-row,.offer-rowund.area-kvstehen noch inadmin-areas.css.
- P5:
-
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-buildsteht „Deployment completed successfully“. -
curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'entsprichtdist/index.htmldes Tags. -
Ein lesender Sichttest im Chrome des Eigentümers: Kundenliste mit Reitern, eine Kundenseite mit Tickets-Reiter und Funktionen.