51 KiB
WOMS „Weißes Design“ P8: Rechtliches und Admin – 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 P8 sind „Rechtliches“ und „Admin“ kastenlos und ohne Icons. Rechtstexte stehen als Abschnitte mit Haarlinien-Zeilen, die Anbieter-Verträge als Tabelle. Vertrags-, Versions- und Mitarbeiter-Formular sind Dialoge, die nur bei Erfolg schließen und Fehler im Dialog zeigen. Jede heutige Funktion bleibt erreichbar.
Architecture:
- Zwei neue reine Lib-Dateien mit Tests:
src/lib/legalView.js(Anzeige-Texte der Vertragsliste, Badge-Anteil der Frist) undsrc/lib/employeeForm.js(Prüfung des Mitarbeiter-Formulars). - Bausteine aus P1:
Dialog,Field,Card,Badge,Tabs,PageHeader,EmptyState,.table,.msg,.loading,.link. - CSS: Der Rechtliches-Block in
global.cssundAdminPage.css(416 → rund 45 Zeilen) werden neu geschrieben..credential*,.sr-onlyund.spinziehen nachglobal.css. - Hooks (
useLegal,useEmployees) undlib/legal.jsbleiben unverändert.
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“ (Abschnitt, Knöpfe, Links, Badge, Tabelle, Formular, Dialog, Meldungen, Laden), „Texte“ und Paket P8. Anhang 2026-10-04-weisses-design-umbaukarte.md: Abschnitt 3 (Dialog-Verbraucher), 4 (Icon-Regel), 5 (P8), 7.4, 7.8. Globale Regeln und Schnittstellen: 2026-10-05-weisses-design-00-uebersicht.md.
Global Constraints
Es gelten die Regeln der Übersicht. Für P8 kommt hinzu:
- Jeder Run-Block beginnt mit
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh. Danach gelten$WT(Worktree, Arbeitsverzeichnis),$W(Werkzeug) und$TMPDIR. Tests und Build werden nur über"$W/check.sh"ausgewertet, Browser-Skripte laufen nur über"$W/shots.sh". - Ausgangspunkt ist das Tag
design-p7, alle Diff-Prüfungen vergleichen gegendesign-p7. - Unverändert bleiben
src/hooks/useLegal.js,useEmployees.js,useDialogFocus.js,useScrollIntoView.jsundsrc/lib/legal.js. Beidewindow.confirm-Rückfragen („Vertrag „…“ wirklich löschen?“, „… und dessen Login wirklich löschen?“) bleiben wörtlich. - Sichtbare Textänderungen sind nur: „Bearbeiten“ und „Löschen“ in der Admin-Tabelle (waren Icons), „Speichert …“ im Speichern-Knopf des Admin-Formulars (war Spinner) und der Leer-Hinweis der Admin-Tabelle (Task 9).
- Dialoge schließen nur über „ד, Esc und bei Erfolg. Bei Fehlern bleiben Dialog und Eingaben offen. Solange gespeichert wird, sind „ד, Esc und „Abbrechen“ gesperrt.
- Jeder Knopf in einem
<Dialog as="form">außer dem Absenden-Knopf trägttype="button"(Buttonsetzt keinen eigenen Standard-Typ). - Abstände zwischen aufeinanderfolgenden Abschnitten kommen von
.page section + section { margin-top: 36px }aus P1. P8 baut dafür keine eigenen Regeln (kein.legal-doc + .legal-doc, kein.admin-page section + section). - CSS-Reihenfolge:
main.jsximportiertAppvorglobal.css, also lädtAdminPage.cssvorglobal.css, und bei gleicher Spezifität gewinntglobal.css. Regeln inAdminPage.css, die eine Global-Klasse überstimmen, haben höhere Spezifität (0,2,0, z. B..page.admin-page …,.check.admin-check) und nie!important. Deshalb wirkt die heutige Breite 1480 px nie. - Die CSS-Tabellen dieses Plans nennen nur die Abweichungen vom Prototyp und von den Tokens. Was Basis (
*-Reset,button-Reset),.table,.kv,.field-rowund.checkschon liefern, wird nicht wiederholt. - In den umgebauten Dateien: keine Icon-Importe, keine neuen
style={{…}}, keine Farbliterale.
Review Focus
Aus der Übersicht gehören drei Punkte zu P8:
- Punkt 2, lange Inhalte am Handy: Testdaten
lang-recht-admin(Anbietername 60 Zeichen, Zweck 90, Notiz 300, Dateiname 70, E-Mail 70).overflow-xist seit dem Gatep1für alle Ansichten scharf, also auch im Gatep8. - Punkt 3, Teilausfall: Ansicht
admin-ausfall(Szenariofehler). Die Mitarbeiterliste antwortet 500, die Seite zeigt.msg.err, „Mitarbeiter anlegen“ öffnet weiter den Dialog. - Punkt 4, nur Tastatur: Esc schließt Vertrags- und Mitarbeiter-Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos (
dlg-vertrag-fehler,admin-fehler). Aufklappzeilen sind echte<button>mitaria-expandedund eigenemonClick(Enter und Klick auf den Button klappen auf).
Zusätzlich gilt: Das Seiten-Banner der Admin-Seite bleibt für Erfolg und Löschen, Validierungs- und Speicherfehler des Formulars stehen im Dialog. Checkliste der Bedienelemente, nichts darf fehlen:
- Rechtliches: 2 Reiter, „Neue Version“, Verlauf auf-/zuklappen, „Live-Seite öffnen“, „Als gültig markieren“, Dateilinks mit „(herunterladen)“, „Neuer Vertrag“, Zeile aufklappen, „Bearbeiten“, „Als gekündigt markieren“ samt „Endet am“, „Speichern“, „Abbrechen“, „Löschen“ (mit Rückfrage), alle Felder und der Upload beider Dialoge.
- Admin: Reiter, „Mitarbeiter anlegen“, „Bearbeiten“, „Löschen“ (nicht in der eigenen Zeile, mit Rückfrage), Kopieren von E-Mail und User-ID, Zugangsdaten-Abschnitt mit „Anzeigen“, „Kopieren“, „ד, Admin-Checkbox (für das eigene Konto gesperrt), Kürzel in Großbuchstaben.
Nachtrag aus P0 (Gate): Gate
p1und alle folgenden Gates schließenadminundadmin-neuvonoverflow-xaus (['*', '!admin', '!admin-neu']). P8 macht die Admin-Seite bei 390 px ohne waagerechtes Scrollen und entfernt die beiden Ausschlüsse aus allen Gates abp8(ein Schritt im Abschluss-Task, geprüft mit"$W/shots.sh" … --only admin,admin-neu --gate p8).
Task 1: Paketstart und Ausgangswerte
Files: keine Änderung.
Interfaces: keine.
- Step 1: Paketstart
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p7 HEAD && git rebase origin/test && "$W/check.sh" && git status --short
Expected: Exit 0, Rebase ohne Konflikt, # fail 0 und eine leere Statusliste. Endet merge-base mit Exit 1, liegt design-p7 nicht im Branch: stoppen und melden. Die Testzahl heißt im Folgenden N0, die Bundle-Größen aus der Ausgabe von check.sh werden notiert.
- Step 2: Abgleich mit TP2
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/LegalPage.jsx src/components/legal src/lib/legal.js src/hooks/useLegal.js src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx src/components/CopyableCredential.jsx src/styles/global.css
Expected: keine Ausgabe. Stand 2026-10-05 berührt 225e2f1 die Rechtliches-Dateien. Gibt es Treffer, stoppt der Agent vor Task 2, nennt dem Eigentümer die Commits und fragt nach. Er empfiehlt dabei, TP2 zuerst nach test zu bringen.
- Step 3: Voraussetzungen aus P1 bis P7 prüfen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/components/ui/Dialog.jsx src/components/ui/Field.jsx src/components/ui/tones.js src/lib/rowClick.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -n "button\.link" src/styles/global.css && rg -n -m 6 "\.field-row|\.dialog-foot|\.msg\.err|\.loading|\.kv\b" src/styles/global.css
Expected: Die vier Dateien sind da. src/lib/rowClick.js (P2 Task 2) enthält isRowActivation. Fehlt die Datei, stoppt der Agent und meldet es, P8 baut keinen eigenen Zeilenklick-Helfer. button.link hat ein Zurücksetzen mit background: none und border: 0 (P2 oder P5), und die Klassen sind definiert.
Entscheidung: Fehlt das Zurücksetzen, ergänzt P8 es im Commit von Task 5: button.link { background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer }.
Task 2: legalView (Lib, TDD)
Files:
- Create:
src/lib/legalView.js - Test:
tests/unit/legalView.test.mjs(neu)
Interfaces:
-
Consumes:
formatDateunddeadlineToneaussrc/lib/legal.js, unverändert. -
Produces, alles in
src/lib/legalView.js:costLabel(contract),termLabel(contract),noticeLabel(contract),deadlineLabel(result)→ String. Es sind die bisherigen lokalen Funktionen ausProviderContractsTab.jsxZ. 8–36.deadlineView(result, today) → { text, lead, rest, badge, muted }mitbadge: 'danger' | 'warn' | null.
-
Step 1: Failing Test schreiben
const { costLabel, termLabel, noticeLabel, deadlineView } = await loadModule('src/lib/legalView.js')
const NB = '\u00a0'
const today = '2026-09-30'
const active = (noticeBy, periodEnd = '2027-03-12') => ({ state: 'active', noticeBy, periodEnd })
const v = (r) => deadlineView(r, today)
test('Vertragsliste: Kosten, Laufzeit, Frist wie bisher', () => {
assert.equal(costLabel({ cost: null }), '–')
assert.equal(costLabel({ cost: 12.5, costInterval: 'month' }), `12,50${NB}€/Monat`)
assert.equal(costLabel({ cost: 120, costInterval: 'year' }), `120,00${NB}€/Jahr`)
assert.equal(termLabel({}), '–')
assert.equal(termLabel({ termMonths: 0 }), 'jederzeit kündbar')
assert.equal(termLabel({ termMonths: 12, renewalMonths: 12 }), '12 Monate, dann je 12 Monate')
assert.equal(termLabel({ termMonths: 24, renewalMonths: 0 }), '24 Monate, endet danach')
assert.equal(noticeLabel({}), '–')
assert.equal(noticeLabel({ noticeValue: 1, noticeUnit: 'months' }), '1 Monate')
assert.equal(noticeLabel({ noticeValue: 14, noticeUnit: 'days' }), '14 Tage')
})
test('deadlineView: Badge nur bei rot und orange, Text wörtlich wie bisher', () => {
assert.deepEqual(v(active('2026-10-15')), {
text: 'kündbar bis 15.10.2026 (Periode endet 12.03.2027)',
lead: 'kündbar bis 15.10.2026', rest: '(Periode endet 12.03.2027)', badge: 'danger', muted: false,
})
assert.equal(v(active('2026-10-30')).badge, 'danger') // 30 Tage
assert.equal(v(active('2026-10-31')).badge, 'warn') // 31 Tage
assert.equal(v(active('2026-12-29')).badge, 'warn') // 90 Tage
const normal = v(active('2026-12-30')) // 91 Tage
assert.deepEqual([normal.badge, normal.lead === normal.text, normal.rest], [null, true, ''])
})
test('deadlineView: Text und Dämpfung der übrigen Zustände', () => {
assert.deepEqual([v({ state: 'anytime' }).text, v({ state: 'anytime' }).muted], ['jederzeit kündbar', false])
assert.equal(v({ state: 'ends', periodEnd: '2027-01-31' }).text, 'endet am 31.01.2027')
assert.deepEqual([v({ state: 'ended', periodEnd: '2026-01-31' }).text, v({ state: 'ended', periodEnd: '2026-01-31' }).muted], ['beendet seit 31.01.2026', true])
assert.equal(v({ state: 'cancelled', periodEnd: '2027-01-31' }).text, 'gekündigt zum 31.01.2027')
assert.equal(v({ state: 'cancelled', periodEnd: null }).text, 'gekündigt')
assert.deepEqual([v({ state: 'incomplete' }).text, v({ state: 'incomplete' }).muted], ['Fristen fehlen', true])
})
- Step 2: Test laufen lassen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs
Expected: FAIL, Modul nicht gefunden.
-
Step 3: Implementieren
-
Die vier Funktionen samt Formatierer
euro(Z. 8–36 vonProviderContractsTab.jsx) wandern unverändert nachsrc/lib/legalView.js. -
deadlineViewergänzt sie.textistdeadlineLabel(result). BeideadlineTone(result, today)gleichredistbadge'danger', beiorange'warn', sonstnull. -
Bei gesetztem
badgeistleadkündbar bis ${formatDate(noticeBy)}undrest(Periode endet ${formatDate(periodEnd)}). Sonst istleadgleichtextundrestleer. -
mutedist wahr bei Tongreyodermissing. -
Step 4: Test grün
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs && "$W/check.sh" --nur-tests
Expected: PASS, Exit 0, Testzahl N0 + 3, # fail 0.
- Step 5: Commit
git add src/lib/legalView.js tests/unit/legalView.test.mjs
git commit -m "Rechtliches: Anzeige-Texte der Vertragsliste als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 3: validateEmployeeForm (Lib, TDD)
Files:
- Create:
src/lib/employeeForm.js - Test:
tests/unit/employeeForm.test.mjs(neu)
Interfaces:
-
Produces:
validateEmployeeForm(form, isEdit) → string | null. Der String ist der deutsche Fehlertext,nullheißt gültig. Prüfungen und Texte sind die ausAdminPage.jsxZ. 70–82. -
Step 1: Failing Test schreiben
const { validateEmployeeForm } = await loadModule('src/lib/employeeForm.js')
const base = { displayName: 'Max Muster', email: 'max@webklar.com', password: '', shortcode: '', isAdmin: false }
const NEED = 'Name und E-Mail sind erforderlich.'
test('validateEmployeeForm: neuer Mitarbeiter', () => {
assert.equal(validateEmployeeForm({ ...base, displayName: ' ', password: '12345678' }, false), NEED)
assert.equal(validateEmployeeForm({ ...base, email: '', password: '12345678' }, false), NEED)
assert.equal(validateEmployeeForm({ ...base, password: '1234567' }, false), 'Das Passwort muss mindestens 8 Zeichen haben.')
assert.equal(validateEmployeeForm({ ...base, password: '12345678' }, false), null)
})
test('validateEmployeeForm: Mitarbeiter bearbeiten', () => {
assert.equal(validateEmployeeForm(base, true), null) // leeres Passwort = unverändert
assert.equal(validateEmployeeForm({ ...base, password: 'abc' }, true), 'Ein neues Passwort muss mindestens 8 Zeichen haben.')
assert.equal(validateEmployeeForm({ ...base, password: 'abcdefgh' }, true), null)
assert.equal(validateEmployeeForm({ ...base, email: ' ' }, true), NEED)
})
- Step 2: Test laufen lassen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs
Expected: FAIL, Modul nicht gefunden.
- Step 3: Implementieren
Reihenfolge der Prüfungen: erst Name und E-Mail (getrimmt), dann bei !isEdit die Mindestlänge 8, dann bei isEdit mit nicht leerem Passwort die Mindestlänge 8.
- Step 4: Test grün
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs && "$W/check.sh" --nur-tests
Expected: PASS, Exit 0, Testzahl N0 + 5, # fail 0.
- Step 5: Commit
git add src/lib/employeeForm.js tests/unit/employeeForm.test.mjs
git commit -m "Admin: Pruefung des Mitarbeiter-Formulars als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 4: Werkzeug – Testdaten, Szenario, Ansichten, Gate p8
Files (alle unter $W/, kein Commit im Repo):
- Create:
fixtures/lang-recht-admin.mjs - Modify:
backend.mjs,selftest.mjs,checks.mjs,views.mjs,gates.mjs
Interfaces:
-
Consumes:
createBackend,VIEWS,GATESund die Testdaten aus P0. -
Produces: die Ansichten aus Step 4,
GATES.p8, das Szenario'mitarbeiter'und die erweiterte Konsolen-Ausnahme des Szenariosfehler. -
Step 1: Testdaten
lang-recht-adminanlegen
Die Datei erweitert standard im Stil von altdaten.mjs. Alle Daten sind relativ zum heutigen Tag berechnet, damit die Töne stabil bleiben.
-
Rechtstexte: Dokument A (Titel 50 Zeichen) mit gültiger Version, Entwurf und zwei abgelösten Versionen, eine mit 300-Zeichen-Notiz und eine Datei mit 70-Zeichen-Namen. Dokument B hat nur einen Entwurf, Dokument C keine Version.
-
Verträge: Je ein Vertrag mit Kündigungstermin in 10 Tagen (rot), 60 (orange) und 200 (normal), jeweils Frist 30 Tage, Laufzeit und Verlängerung 12 Monate. Dazu ein gekündigter, beendeter, unvollständiger, jederzeit kündbarer und auslaufender Vertrag. Einer trägt Anbieter (60 Zeichen), Zweck (90), Notiz (300) und eine Datei (70).
-
Mitarbeiter: die 4 Standard-Mitarbeiter plus einer mit Namen (40) und E-Mail (70 Zeichen). Der Mitarbeiter KNSO trägt die
userIddes angemeldeten Kontos, sonst ergänzen (Ansichtadmin-bearbeitenbraucht die eigene Zeile). -
Step 2: Szenario
mitarbeiter(erst Selbsttest, dann Backend)
selftest.mjs bekommt die Prüfung: GET https://ticket.test/v1/account liefert im Szenario mitarbeiter 200 mit labels: ['mitarbeiter'].
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs
Expected: FAIL. Nach der Ergänzung in backend.mjs: selftest ok.
- Step 3: Konsolen-Ausnahme für absichtliche 500er
Die absichtlich per page.route erzeugten 500er schreibt der Browser als console.error („Failed to load resource“). Der Check console-error nimmt Meldungen aus, die das Szenario fehler auslöst (P0).
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/checks.mjs" "$W/shots.mjs"
-
Die Ansichten mit absichtlichen 500ern (
dlg-vertrag-fehler,dlg-version-fehler,admin-fehler,admin-ausfall) laufen im Szenariofehler. -
Deckt die Ausnahme ihre Meldungen nicht ab, wird sie in
checks.mjsum die URL-Muster**/collections/*/documents(nur POST) und**/api/admin/employeeserweitert. P7 tut dasselbe für die Finanzen, ein schon vorhandener gleichwertiger Eintrag wird mitbenutzt. -
Step 4: Ansichten in
views.mjsanlegen oder anpassen
Alle nutzen fixtures: 'lang-recht-admin', die vier Fehleransichten zusätzlich scenario: 'fehler'. Der path ist /rechtliches bzw. /admin. Die Schritte haben die Signatur steps(page, ctx), ctx.backend.calls listet die vom Backend beantworteten Requests. Fehlerantworten setzen die Schritte mit page.route(…). Seitenrouten haben Vorrang vor den Kontextrouten des Backends, route.fallback() reicht alles andere durch. Geprüft wird mit throw new Error(…), das den Verstoß step-failed erzeugt. Die vier dlg-*-Ansichten behalten die Viewports [[1440, 900], [390, 844], [1440, 700], [390, 700]] und rufen am Ende submitReachable(page) auf (P0).
| ID | Schritte |
|---|---|
rechtliches |
/rechtliches. Fokus auf .legal-doc-summary des ersten Dokuments, Enter. Es muss aria-expanded="true" stehen und .legal-history sichtbar sein. |
rechtliches-anbieter |
Reiter „Verträge mit Anbietern“. Fokus auf button.link der ersten Zeile, Enter. .legal-detail muss sichtbar sein. Klick „Als gekündigt markieren“, .legal-cancel muss sichtbar sein. |
dlg-vertrag |
Reiter Anbieter, Klick „Neuer Vertrag“. [role=dialog] muss offen sein, Esc muss ihn schließen. Dann wieder öffnen. |
dlg-vertrag-fehler |
Dialog öffnen, Anbieter „Testanbieter“ eintragen. POST **/collections/*/documents mit 800 ms Verzögerung und 500 { message: 'Test: Speichern fehlgeschlagen' } beantworten, Speichern klicken. Sofort muss .dialog-close disabled sein, Esc darf nicht schließen. Nach der Antwort steht .msg.err[role=alert] mit dem Text im offenen Dialog, und „Anbieter“ trägt noch „Testanbieter“. |
dlg-version |
Reiter Texte, Klick „Neue Version“ am ersten Dokument. Bei 390 px liegt der Speichern-Knopf nach scrollIntoView im Fenster. |
dlg-version-fehler |
Wie dlg-vertrag-fehler mit Version „v9“. |
admin |
/admin. |
admin-neu |
Klick „Mitarbeiter anlegen“. Dialog muss offen sein, Esc schließt ihn. Dann wieder öffnen. |
admin-fehler |
Dialog öffnen, Name, E-Mail, Passwort „abc“, absenden. Im Dialog muss „Das Passwort muss mindestens 8 Zeichen haben.“ als .msg.err stehen. Dann Passwort „12345678“, POST **/api/admin/employees mit 500 { error: 'Test: Login konnte nicht angelegt werden' } beantworten, absenden. Die Meldung wechselt, der Dialog bleibt offen. |
admin-zugang |
Gültiges Formular absenden. Der Dialog muss zu sein, .msg.ok und der Abschnitt „Login wurde angelegt“ müssen sichtbar sein. |
admin-bearbeiten |
Klick „Bearbeiten“ in der Zeile des angemeldeten Kontos. Titel „Mitarbeiter bearbeiten“, Admin-Checkbox disabled. |
admin-ausfall |
Zusätzlich zum Szenario GET **/api/admin/employees mit 500 beantworten (die Seite fragt erst das Portal, dann die Collection). Ein .msg.err steht auf der Seite, „Mitarbeiter anlegen“ öffnet den Dialog. |
admin-403 |
Szenario mitarbeiter. Die Überschrift „Zugriff verweigert“ steht da. |
- Step 5: Gate festlegen
In gates.mjs kommt nach der Deklaration von GATES dazu. Das Gate p8 erbt Prüfungen und Ansichten von p7 (die p1-Prüfungen für alle Ansichten, step-failed, external-request und console-error immer) und schaltet icon-svg zusätzlich für die Ansichten von P8 und aller früheren Pakete ein:
const iconSvg = GATES.p7.scoped['icon-svg']
GATES.p8 = {
...GATES.p7,
scoped: {
...GATES.p7.scoped,
'icon-svg': {
...iconSvg,
views: [...iconSvg.views, 'rechtliches', 'rechtliches-anbieter', 'dlg-vertrag', 'dlg-vertrag-fehler', 'dlg-version', 'dlg-version-fehler',
'admin', 'admin-neu', 'admin-fehler', 'admin-zugang', 'admin-bearbeiten', 'admin-ausfall', 'admin-403'],
},
},
}
- Step 6: Ausgangslauf
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8-vorher && "$W/shots.sh" p8-vorher "$W/shots/p8-vorher" --only admin --gate p8
Expected: Exit 1 mit icon-svg in admin (Icons der Zeilenaktionen und des Knopfes). Das ist der Ausgangszustand.
Task 5: Rechtstexte-Reiter
Files:
- Modify:
src/pages/LegalPage.jsx(Z. 19 Ladetext, Z. 34 Fehlerzeile) - Modify:
src/components/legal/LegalTextsTab.jsx(Markup Z. 23–83, Logik Z. 9–21 bleibt) - Modify:
src/components/legal/legalUi.jsx(FileLinks, Z. 8–21) - Modify:
src/styles/global.css(Block „Rechtliches“,rg -n "Rechtliches ---" src/styles/global.css; bei Bedarf das Zurücksetzen fürbutton.linkaus Task 1)
Interfaces:
-
Consumes:
Card,Badge,Tabs,EmptyState,PageHeader(P1),.link,.msg.err,.loading,currentVersionundformatDateauslib/legal.js. -
Produces:
LegalTextsTabundFileLinksbehalten ihre Props. Dateilinks tragenclass="link", der Download-Link zusätzlichlegal-dl. -
Step 1:
LegalPage.jsxanpassen -
<div className="faint">Lädt …</div>wird<div className="loading">Lädt …</div>.<div className="legal-error">wird<div className="msg err" role="alert">.
Entscheidung: Die Ladetexte „Lädt …“ und (Task 9) „Mitarbeiter werden geladen …“ bleiben wörtlich. Die Spec ändert nur „Loading...“, hier ändert sich nur die Klasse.
-
Step 2:
LegalTextsTab.jsxumbauen -
Wurzel
<div className="legal-docs">. Der Aktionsfehler wird<div className="msg err legal-msg" role="alert">. -
Pro Dokument
Card className="legal-doc" title={doc.title}(ein eigener Abschnitt, den Abstand zum nächsten liefert P1). Die Aktion ist<button type="button" className="link" onClick={() => setFormFor(doc)}>Neue Version</button>. -
Die Zusammenfassung ist ein echter
<button type="button" className="legal-doc-summary" aria-expanded={expanded}>stattdiv role="button". Der Inhalt bleibt: Badge „{version} · gilt seit …“ bzw. „noch keine gültige Version“, Badges „Entwurf {version}“, dazu<span className="legal-toggle">mit „Verlauf ausblenden“ bzw. „Verlauf (n)“. -
„Live-Seite öffnen“ ist
<a className="link legal-live" …>. -
Verlauf
ol.legal-history, je Version einlimit der Klasselegal-version, bei Statussupersededzusätzlichis-superseded(kein Klassenname mehr aus dem Status). Kopf: Version fett, Zeitraum in<span className="muted">, Badges „gültig“ (ok) und „Entwurf“ (muted), „Als gültig markieren“ als<button type="button" className="link">. -
„Noch keine Versionen eingetragen.“ bleibt
.faint,VersionFormwird wie bisher gerendert. -
Step 3:
legalUi.jsxanpassen
Dateilink <a className="link" …>, Download <a className="link legal-dl" …>(herunterladen)</a>. „keine Datei“ bleibt .faint (leerer Zustand).
- Step 4: CSS im Block „Rechtliches“ ersetzen
Gelöscht werden .legal-grid samt > .legal-error, .legal-version-current, -draft, -superseded und .legal-files a. Neu oder geändert:
| Regel | Wert |
|---|---|
.legal-doc-summary |
display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; padding: 12px 0; text-align: left; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line) (Knopf-Reset kommt aus der Basis) |
.legal-toggle |
margin-left: auto; font-size: 13px; color: var(--text-2) |
.legal-live |
display: inline-block; margin-top: 8px; font-size: 13px |
.legal-history |
list-style: none; margin: 0; padding: 0 |
.legal-version |
padding: 10px 0; border-bottom: 1px solid var(--line) |
.legal-version.is-superseded |
color: var(--text-2) |
.legal-version-head |
display: flex; flex-wrap: wrap; align-items: center; gap: 8px |
.legal-version-head .link |
margin-left: auto |
.legal-note |
white-space: pre-wrap; margin: 4px 0; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere |
.legal-files |
display: inline-flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; overflow-wrap: anywhere |
.legal-files .link |
font-weight: 400 |
.legal-files .legal-dl |
color: var(--text-2) |
.legal-msg |
margin: 0 0 12px |
- Step 5: Bauen und prüfen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t5" --only rechtliches --gate p8
Expected: PASS. In den Bildern gilt:
-
Kein Kasten um Dokumente, nur Haarlinien über und unter der Zusammenfassung und zwischen den Versionszeilen.
-
„Neue Version“ steht als Textlink rechts neben der Überschrift.
-
Die Zeile der gültigen Version hat keinen grünen Hintergrund (nur das Badge ist grün), die des Entwurfs keinen gestrichelten Rand.
-
Dateilinks sind unterstrichen. Bei 390 px läuft nichts über.
-
Step 6: Commit
git add src/pages/LegalPage.jsx src/components/legal/LegalTextsTab.jsx src/components/legal/legalUi.jsx src/styles/global.css
git commit -m "Design: Rechtstexte als Abschnitte mit Haarlinien, Aufklappzeile als Button" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 6: Anbieter-Reiter
Files:
- Modify:
src/components/legal/ProviderContractsTab.jsx(lokale Label-Funktionen Z. 8–36 löschen, Markup Z. 75–140) - Modify:
src/styles/global.css(Block „Rechtliches“)
Interfaces:
-
Consumes:
costLabel,termLabel,noticeLabel,deadlineView(Task 2),isRowActivation(event)aussrc/lib/rowClick.js(P2 Task 2, Prüfung in Task 1),deadlineTone,nextCancellationDate(lib/legal.js),FileLinks,ContractForm(Props unverändert),Badge,Button,EmptyState. -
Produces:
ProviderContractsTabbehält seine Props. -
Step 1: Markup umbauen
TONE_ORDER, Sortierung, run, confirmCancel und remove (Z. 38–73) bleiben.
- Aktionsfehler
<div className="msg err legal-msg" role="alert">. Die.toolbarmit „Neuer Vertrag“ (Button variant="primary") bleibt. <table className="table legal-contracts">. Die Spalten „Wofür“, „Kosten“, „Laufzeit“ und „Kündigungsfrist“ bekommenhide-sm.- Zeile ohne
legal-tone-*:<tr className="row" onClick={(e) => { if (isRowActivation(e)) setOpen(open === c.$id ? null : c.$id) }}>. Der Helfer ignoriert Klicks aufa, button, select, input, label. - Erste Zelle:
<div className="cell-title"><button type="button" className="link" aria-expanded={open === c.$id} onClick={() => setOpen(open === c.$id ? null : c.$id)}>{c.provider}</button></div>.- Button und Zeilenhandler benutzen denselben Umschalter
setOpen(open === c.$id ? null : c.$id). - Der Button braucht seinen eigenen
onClick, weilisRowActivationKlicks aufbuttonausfiltert. Ohne ihn klappt weder Klick noch Enter auf dem Button etwas auf, und die Ansichtrechtliches-anbieterscheitert.
- Button und Zeilenhandler benutzen denselben Umschalter
- Zelle „Nächster Termin“ mit
view = deadlineView(result, today): Beiview.badgesteht<Badge tone={view.badge}>{view.lead}</Badge>und dahinter<span className="muted"> {view.rest}</span>. Sonst steht<span className={view.muted ? 'muted' : undefined}>{view.text}</span>. - Detail-Zeile
tr.legal-detail > td colSpan={6}, von oben nach unten:dl.kv.legal-factsmit „Wofür“, „Kosten“, „Laufzeit“, „Kündigungsfrist“, nur unter 860 px sichtbar.- „Beginn: …“, Notiz
.legal-note,FileLinks. div.legal-actions:button.link„Bearbeiten“,button.link„Als gekündigt markieren“ (Bedingung unverändert),Button variant="danger"„Löschen“.- Beim Kündigen steht statt des Links
span.legal-cancel:<label htmlFor={`cancel-${c.$id}`}>Endet am</label>,<input id=… type="date" className="input">,Button variant="primary"„Speichern“,Button„Abbrechen“.
Entscheidung: Die Aufklappzeile folgt der Tabellenregel der Spec. Der echte Button in der ersten Zelle trägt die Bedienung, der Zeilenklick ist nur eine Zugabe für die Maus.
Entscheidung: Unter 860 px zeigt die Tabelle nur „Anbieter“ und „Nächster Termin“ (Kernspalten-Regel). Die übrigen Angaben stehen dann im Detail (.legal-facts) mit den vorhandenen Spaltenköpfen als Beschriftung. Es kommt kein neuer Text dazu, und keine Information geht verloren.
- Step 2: CSS ergänzen und Altregeln löschen
Gelöscht werden .legal-tone-*, das alte .legal-detail td und der @media (max-width: 700px)-Block. Neu oder geändert:
| Regel | Wert |
|---|---|
.legal-detail td |
padding: 0 0 16px (ohne Hintergrund) |
.legal-facts |
display: none; margin-bottom: 8px, unter 860 px display: grid |
.legal-actions |
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 8px |
.legal-cancel |
display: flex; flex-wrap: wrap; align-items: center; gap: 8px |
.legal-cancel .input |
width: 160px |
- Step 3: Bauen und prüfen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t6" --only rechtliches-anbieter --gate p8
Expected: PASS. In den Bildern gilt:
-
Keine getönte Zeile. Bei rot und orange steht im Terminfeld ein Badge „kündbar bis …“ mit dem Rest in Grau, beendete und gekündigte Verträge haben grauen Text im Terminfeld.
-
Das Detail zeigt Textlinks, das Kündigungs-Formular steht in einer Zeile.
-
Bei 390 px gibt es zwei Spalten, das Detail zeigt die vier Angaben als
.kv. -
Step 4: Commit
git add src/components/legal/ProviderContractsTab.jsx src/styles/global.css
git commit -m "Design: Anbieter-Vertraege als Tabelle, Dringlichkeit nur im Terminfeld" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 7: Vertrags- und Versions-Dialog
Files:
- Modify:
src/components/legal/ContractForm.jsx(Markup Z. 56–105, Import Z. 2) - Modify:
src/components/legal/VersionForm.jsx(Markup Z. 26–63, Import Z. 2) - Modify:
src/styles/global.css(Block „Rechtliches“)
Interfaces:
-
Consumes:
Dialog,Field,Button(P1),useScrollIntoView(ref, trigger)aussrc/hooks/useScrollIntoView.js. -
Produces:
ContractForm({ contract, onSave, onClose })undVersionForm({ title, onSave, onClose })behalten ihre Signaturen. -
Step 1:
ContractForm.jsxumbauen
FIELDS, NUMBERS, toForm, toDoc, set und submit bleiben (Fehler landet weiter in error, Erfolg ruft onClose()).
- Der
FaTimes-Import entfällt. Neu sinderrorRef = useRef(null)unduseScrollIntoView(errorRef, error). - Gerüst:
<Dialog as="form" wide title={contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'} busy={saving} onClose={onClose} onSubmit={submit} footer={…}>. - Felder als
Field label htmlFormitid="contract-<key>". Eingaben tragenclassName="input", Auswahlenselect, die Notiztextarea.name,required,type,min,inputModeundacceptbleiben. - Paare in
<div className="field-row">: Kosten und „pro“, Laufzeit und „Verlängert sich um“, Kündigungsfrist und „Einheit“. - Upload:
<input type="file" className="legal-file" multiple accept=…>inField label="Dateien hinzufügen". - Der Fehler ist das erste Kind:
{error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}. - Fuß:
<span className="grow" />,<Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>,<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>.
Entscheidung: „Abbrechen“ ist während des Speicherns gesperrt, wie „ד und Esc. Heute schließt es mitten im Speichern und verwirft die Rückmeldung.
- Step 2:
VersionForm.jsxumbauen
Gleiches Muster, auch im Fuß (Abbrechen mit type="button", Absenden mit type="submit"). Der Titel ist Neue Version: ${title}, der Dialog nicht wide, die IDs sind version-<key>. „Version“ behält placeholder="z. B. v6" und required, „Gültig ab“ bleibt required, die Notiz rows={4}, der Upload bekommt legal-file.
- Step 3: CSS
Gelöscht werden .legal-error und .legal-form-row. Neu:
| Regel | Wert |
|---|---|
.legal-file |
font-size: 13px; color: var(--text-2) |
.legal-file::file-selector-button |
height: 28px; padding: 0 10px; margin-right: 10px; border: 0; border-radius: var(--radius); background: var(--bg); box-shadow: var(--ring); font: inherit; font-weight: 500; color: var(--text); cursor: pointer |
- Step 4: Hygiene und Bilder
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "overlay|react-icons|form-control|form-group|form-label|legal-error|legal-form-row|ui-btn" src/components/legal src/pages/LegalPage.jsx
Expected: keine Treffer.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t7" --only dlg-vertrag,dlg-vertrag-fehler,dlg-version,dlg-version-fehler --gate p8
Expected: PASS, das heißt:
-
Esc schließt den Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos.
-
Nach dem Fehler steht
.msg.errim offenen Dialog, die Eingabe ist erhalten. -
Bei 390 px ist der Dialog ein Bottom-Sheet, und der Speichern-Knopf ist erreichbar.
-
Step 5: Commit
git add src/components/legal/ContractForm.jsx src/components/legal/VersionForm.jsx src/styles/global.css
git commit -m "Design: Vertrags- und Versions-Formular als Dialoge, Fehler im Dialog" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 8: Zugangsdaten-, sr-only- und spin-Klassen nach global.css
Files:
- Modify:
src/styles/global.css(Ende von Block 3 „Bausteine“, vor/* ===== KOMPAT (bis P10) ===== */) - Modify:
src/pages/AdminPage.css(Blöcke.credential*,.sr-only,.spinsamt@keyframes admin-spinlöschen)
Interfaces:
-
Consumes:
src/components/CopyableCredential.jsxin der Fassung von P5 (Textknöpfe „Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“). -
Produces: die Klassen für
CopyableCredentialund.sr-onlyfür alle Seiten. -
Step 1: Klassen der Komponente lesen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o 'className=\{?[`"][^`"]*credential[^`"]*' src/components/CopyableCredential.jsx | sort -u
Expected: credential, credential-value, is-masked, credential-empty. P5 hat credential-action und credential-check gelöscht, die Knöpfe sind button.link in .credential. Es werden nur die Regeln verschoben, die design-p7:src/pages/AdminPage.css noch enthält.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git show design-p7:src/pages/AdminPage.css
Expected: Zu sehen sind die verbliebenen Blöcke .credential*, .sr-only, .spin und @keyframes admin-spin. Nur diese werden verschoben.
- Step 2: Regeln nach
global.cssverschieben und neu einfärben
| Regel | Änderung beim Verschieben |
|---|---|
.credential, .credential .link, .credential-value.is-masked |
unverändert (Stand P5: gap: 8px, .link mit flex: 0 0 auto; font-size: 12px; white-space: nowrap) |
.credential-value |
overflow: hidden; text-overflow: ellipsis wird overflow-wrap: anywhere, sonst unverändert. Lange Werte brechen um, statt abgeschnitten zu werden und die Admin-Tabelle bei 390 px zu sprengen. |
.credential-empty |
color: var(--text-3) (steht für „–“) |
.sr-only |
unverändert (position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0) |
.spin |
animation: spin .8s linear infinite (nutzt die vorhandenen @keyframes spin, der Block @keyframes admin-spin entfällt) |
Entscheidung: .spin zieht wie in der Spec nach global.css, obwohl es nach P8 keinen Aufrufer mehr hat. Der einzige war der Spinner der Admin-Seite. P10 löscht es, wenn rg -w spin src --glob '!*.css' dann weiter nichts findet. Das steht in der Übergabe.
- Step 3: Prüfen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.filter(n=>/credential|sr-only/.test(n)).join('\n'))"
Expected: leer.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t8" --only admin,kunde-overview --gate p7
Expected: PASS. Das Gate p7 ist das des Vorgängerpakets, denn icon-svg für admin wird erst mit Task 9 sauber. Zugangsdaten in der Admin-Tabelle und auf der Kundenseite (Ansicht kunde-overview, Abschnitt „Portal-Zugang“) sind lesbar, die Textknöpfe „Kopieren“ und „Anzeigen“ unterstrichen.
- Step 4: Commit
git add src/styles/global.css src/pages/AdminPage.css
git commit -m "Design: Zugangsdaten-, sr-only- und spin-Klassen nach global.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 9: AdminPage
Files:
- Modify:
src/pages/AdminPage.jsx(Importe Z. 1–13, Zustand Z. 40–45, 403-Block Z. 52–62, Formular-Logik Z. 64–112, Markup Z. 123–226;showNoticeundremoveEmployeeZ. 114–121 bleiben) - Rewrite:
src/pages/AdminPage.css - Modify:
src/components/AdminRoute.jsx
Interfaces:
-
Consumes:
validateEmployeeForm(form, isEdit)(Task 3),Dialog,Field,Button,Badge,Card,PageHeader,Tabs,EmptyState(P1),CopyableCredential(P5),useScrollIntoView,useEmployeesunduseAuthunverändert. -
Produces: nichts für spätere Pakete.
-
Step 1: Seitengerüst
-
Alle Icon-Importe entfallen.
Noticerendert<div className={`msg ${notice.type === 'error' ? 'err' : 'ok'} admin-notice`} role={notice.type === 'error' ? 'alert' : 'status'}>. -
Kopf:
PageHeader eyebrow="Systemverwaltung" title="Admin" subtitle="Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“.". Die Aktionen sind<span className="muted small">Angemeldet als <strong>{user?.name || user?.email}</strong></span>undButton variant="primary"„Mitarbeiter anlegen“, der einzige schwarze Knopf im Kopf. -
Der Reiter ist
Tabsmit einem Eintrag. Das Label bleibtMitarbeiter (${employees.length})ohnecount, weilcountden Text ändern würde. -
Die Wurzel bleibt
<div className="page admin-page">. Der 403-Block behält sein Markup (span„403“,h1„Zugriff verweigert“,p). -
Step 2: Zugangsdaten-Abschnitt
createdCredential rendert Card title="Login wurde angelegt" actions={<button type="button" className="btn btn-quiet" aria-label="Hinweis schließen">×</button>} mit <p className="muted small">Diese Zugangsdaten jetzt sicher weitergeben.</p> und <div className="credential-list"> um die zwei CopyableCredential (E-Mail, Passwort mit secret). Er steht über der Tabelle und ist kein Dialog.
- Step 3: Tabelle
Card title="Mitarbeiterkonten" mit <p className="muted small admin-lead">Aktive Logins für das Ticketsystem</p>:
- Ladefehler
employeesError:div.msg.err[role=alert]. - Laden:
<div className="loading">Mitarbeiter werden geladen …</div>. - Leer:
EmptyState title="Noch keine Mitarbeiter" hint="Lege mit „Mitarbeiter anlegen“ den ersten Login an.".
Entscheidung: Der Hinweis „Lege links den ersten Login an.“ passt nicht mehr, weil das Formular nicht mehr links steht. Das ist die einzige Textänderung außerhalb der Spec-Liste, die Übergabe nennt sie.
Die Tabelle ist <table className="table"> ohne tr.row, denn die Zeilen sind heute auch nicht klickbar. Spalten:
| Spalte | Inhalt | Klassen |
|---|---|---|
| Mitarbeiter | strong mit Name oder „–“ |
– |
CopyableCredential (Label „E-Mail“) |
– | |
| Kürzel | <span className="admin-shortcode"> |
hide-sm |
| Rolle | Badge tone="ok" „Admin“, sonst <span className="muted">Mitarbeiter</span> |
hide-sm |
| User-ID | CopyableCredential (Label „User-ID“) |
hide-sm |
Aktionen (sr-only) |
div.admin-actions mit button.link „Bearbeiten“ und Button variant="danger" „Löschen“, Letzteres nur wenn employee.userId !== user?.$id |
right |
Die aria-label der Aktionen bleiben ${employee.displayName} bearbeiten und … löschen. Auf dem Handy bleiben Mitarbeiter, E-Mail und Aktionen.
-
Step 4: Formular als Dialog
-
Neu sind
formOpen,formError,errorRefunduseScrollIntoView(errorRef, formError).editingSelfist die einmal berechnete Bedingung „Admin-Haken gehört zum eigenen Konto“ (heute zweimal inline). -
„Mitarbeiter anlegen“ und „Bearbeiten“ setzen Formular und
editingEmployee, leerenformErrorund öffnen mitsetFormOpen(true). -
submitEmployeeprüft zuerstvalidateEmployeeForm(employeeForm, Boolean(editingEmployee)). Bei einem Text setzt esformError(kein Banner) und kehrt zurück. -
Sonst läuft der Ablauf wie bisher:
employeeBusyan,updateEmployeebzw.createEmployee,employeeBusyaus.- Bei Misserfolg steht
result.error || 'Mitarbeiter konnte nicht gespeichert werden.'informError, der Dialog bleibt offen. - Bei Erfolg folgen
setCreatedCredential(nur beim Anlegen),showNotice('success', …)mit den bisherigen Texten, dann Formular schließen und zurücksetzen.
- Bei Misserfolg steht
-
Gerüst:
<Dialog as="form" title={editingEmployee ? 'Mitarbeiter bearbeiten' : 'Mitarbeiter anlegen'} busy={employeeBusy} onClose={closeForm} onSubmit={submitEmployee} footer={…}>. -
Inhalt von oben nach unten:
- Fehler
div.msg.err[role=alert][ref=errorRef], dann<p className="muted small">Die User-ID wird automatisch vergeben.</p>. - Vier
Fieldmitid="employee-<key>"undclassName="input": Name (required), E-Mail (type="email",required), Passwort (Label „Neues Passwort (optional)“ bzw. „Passwort“,autoComplete="new-password",required={!editingEmployee}, Platzhalter „Mindestens 8 Zeichen“), Kürzel (maxLength={10}, Platzhalter „z. B. KNSO“, Großschreibung beim Tippen). - Admin-Checkbox:
<label className={`check admin-check${editingSelf ? ' is-disabled' : ''}`}>mit<input type="checkbox" disabled={editingSelf}>,<strong>Admin-Account</strong>und<small>Darf Systemkonfiguration und Benutzer verwalten.</small>.
- Fehler
-
Fuß:
<span className="grow" />,Button type="button" disabled={employeeBusy}„Abbrechen“,Button variant="primary" type="submit" disabled={employeeBusy}mit „Speichert …“, „Änderungen speichern“ oder „Mitarbeiter anlegen“. -
Step 5:
AdminPage.cssneu schreiben
Die Datei wird vollständig ersetzt:
| Regel | Wert |
|---|---|
.page.admin-page |
max-width: 1480px |
.admin-notice |
margin-bottom: 16px |
.admin-lead |
margin: 0 0 8px |
.admin-shortcode |
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; letter-spacing: .04em |
.admin-actions |
display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px |
.credential-list |
display: flex; flex-wrap: wrap; gap: 8px 32px; margin-top: 8px |
.check.admin-check |
align-items: flex-start (Spezifität 0,2,0, sonst gewinnt .check aus global.css) |
.admin-check small |
display: block; margin-top: 2px; font-size: 13px; line-height: 18px; color: var(--text-2) |
.admin-check.is-disabled |
cursor: not-allowed; opacity: .55 |
.admin-access-denied |
max-width: 540px; margin: 12vh auto 0 |
.admin-access-denied > span |
font-size: 12px; font-weight: 500; color: var(--red-fg) |
.admin-access-denied h1 |
margin: 4px 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em |
.admin-access-denied p |
color: var(--text-2) |
Weg sind alle !important, Kästen, Rahmen, Schatten, Farbliterale, der Split-Layout-Block und die Media-Queries. Eine Regel für den Abstand zwischen Zugangsdaten und Mitarbeiterkonten gibt es nicht, ihn liefert .page section + section aus P1.
- Step 6:
AdminRoute.jsxangleichen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n 'className="loading"' src/components/AdminRoute.jsx
- Trifft es, hat ein früheres Paket die Datei schon umgestellt, und dieser Schritt entfällt.
- Sonst wird der Ladezweig
<div className="loading"><p>Wird geladen …</p></div>. Das<p>bleibt ohne Klasse, weiltests/unit/adminRoute.test.mjsgenau diese Zeichenfolge prüft. Der Seitenabstand kommt von.loading, Inline-Style,spinnerund die Hilfsklassen entfallen.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/adminRoute.test.mjs
Expected: PASS.
- Step 7: Bauen und prüfen
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|#[0-9a-fA-F]{3,8}\b|!important|admin-icon-button|admin-primary-button|form-control|spinner" src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx
Expected: keine Treffer.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t9" --only admin,admin-neu,admin-fehler,admin-zugang,admin-bearbeiten,admin-ausfall,admin-403 --gate p8
Expected: PASS. In den Bildern gilt:
-
Keine Karten, die Tabelle hat nur Haarlinien, Aktionen sind Textlinks.
-
Der Dialog zeigt Validierungs- und Speicherfehler als
.msg.erroben und schließt nur bei Erfolg. -
Nach Erfolg steht das Banner auf der Seite, darunter der Zugangsdaten-Abschnitt.
-
Bei 390 px sind Mitarbeiter, E-Mail und Aktionen sichtbar, ohne waagerechtes Scrollen.
-
Step 8: Commit
git add src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx
git commit -m "Design: Admin mit Tabelle, Mitarbeiter-Formular als Dialog, AdminPage.css neu" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
Task 10: Gate P8, 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, Rebase ohne Konflikt, # fail 0. Die Testzahl ist N0 + 5. Liegen neue Commits auf test, kann sie höher sein, die Zahl wird notiert. So zeigen die Bilder der folgenden Schritte den Stand, der getaggt wird.
- Step 2: Vollständiger Gate-Lauf
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8" --gate p8
Expected: Exit 0. Das Gate p8 prüft alle Ansichten mit den p1-Prüfungen (overflow-x, dark-surface, low-contrast) und icon-svg für die Ansichten von P2 bis P8. Die 13 P8-Ansichten haben Bilder bei 1440 und 390 px ohne Verstöße.
- Step 3: Hygiene und unveränderte Logik
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{|#[0-9a-fA-F]{3,8}\b|!important|overlay-close|overlay-content|form-control|ui-btn|ui-table" src/pages/AdminPage.jsx src/pages/AdminPage.css src/pages/LegalPage.jsx src/components/legal
Expected: keine Treffer.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat design-p7 -- src/hooks src/lib/legal.js; git diff design-p7 -- src | rg '^[-+].*window\.confirm'; rg -n "legal-tone|legal-version-(current|draft)|legal-grid|legal-form-row|legal-error|legal-doc \+|admin-page section" src
Expected: alle drei Ausgaben leer.
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.filter(n=>/^(legal|admin|credential)-/.test(n)).join('\n'))"
Expected: leer.
- Step 4: Bundle-Größe
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build
Expected: Exit 0, CSS kleiner als bei Task 1. Die Werte werden notiert.
- Step 5: Code-Review (sonnet)
Geprüft wird der Diff design-p7..HEAD gegen die Spec-Abschnitte „Bausteine“ (Dialog, Tabelle, Badge, Meldungen), „Texte“ und Paket P8. Wichtige Befunde werden behoben. Danach kommen Step 1 (Rebase) noch einmal und Step 2 als Kurzlauf mit --only rechtliches,rechtliches-anbieter,admin. Schwerpunkte:
-
Der Dialog schließt nie bei einem Fehler, und „ד, Esc und „Abbrechen“ sind bei
busygesperrt. -
Die Checkliste der Bedienelemente aus „Review Focus“ ist vollständig.
-
Die Bedingung „eigene Zeile“ (kein „Löschen“, Admin-Haken gesperrt) gilt weiter.
-
Die Dringlichkeit steht nur im Terminfeld.
-
Die Aufklappzeile der Verträge öffnet per Klick auf den Button, per Enter und per Klick auf die Zeile.
-
Step 6: Tag und Übergabe
Run: source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p8 && git log --oneline origin/test..design-p8 && git rev-parse design-p8
Der Eigentümer bekommt:
-
die Screenshots aus
$W/shots/p8(Unterordner1440x900und390x844; Auswahl:rechtliches,rechtliches-anbieter,dlg-vertrag-fehler,admin,admin-neu,admin-fehler,admin-zugang) -
den Hinweis auf die Textänderungen: „Bearbeiten“/„Löschen“, „Speichert …“ und der Leer-Hinweis der Admin-Tabelle
-
die Notiz für P10:
.spinhat keinen Aufrufer mehr -
die Push-Zeile
git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p8^{commit}:refs/heads/test' -
die Rücknahme
git revert --no-commit design-p7..design-p8, Commit und Push durch den Eigentümer (gilt nur, solange P8 das jüngste Paket auftestist) -
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: Rechtliches mit beiden Reitern und Admin mit der Tabelle. Dabei wird nichts gespeichert oder gelöscht.