662 lines
50 KiB
Markdown
662 lines
50 KiB
Markdown
# 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) und `src/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.css` und `AdminPage.css` (416 → rund 45 Zeilen) werden neu geschrieben. `.credential*`, `.sr-only` und `.spin` ziehen nach `global.css`.
|
||
- Hooks (`useLegal`, `useEmployees`) und `lib/legal.js` bleiben 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 gegen `design-p7`.
|
||
- Unverändert bleiben `src/hooks/useLegal.js`, `useEmployees.js`, `useDialogFocus.js`, `useScrollIntoView.js` und `src/lib/legal.js`. Beide `window.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ägt `type="button"` (`Button` setzt 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.jsx` importiert `App` vor `global.css`, also lädt `AdminPage.css` vor `global.css`, und bei gleicher Spezifität gewinnt `global.css`. Regeln in `AdminPage.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-row` und `.check` schon 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-x` ist seit dem Gate `p1` für alle Ansichten scharf, also auch im Gate `p8`.
|
||
- **Punkt 3, Teilausfall:** Ansicht `admin-ausfall` (Szenario `fehler`). 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>` mit `aria-expanded` und eigenem `onClick` (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.
|
||
|
||
---
|
||
|
||
### 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: `formatDate` und `deadlineTone` aus `src/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 aus `ProviderContractsTab.jsx` Z. 8–36.
|
||
- `deadlineView(result, today) → { text, lead, rest, badge, muted }` mit `badge: 'danger' | 'warn' | null`.
|
||
|
||
- [ ] **Step 1: Failing Test schreiben**
|
||
|
||
```js
|
||
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 von `ProviderContractsTab.jsx`) wandern unverändert nach `src/lib/legalView.js`.
|
||
- `deadlineView` ergänzt sie. `text` ist `deadlineLabel(result)`. Bei `deadlineTone(result, today)` gleich `red` ist `badge` `'danger'`, bei `orange` `'warn'`, sonst `null`.
|
||
- Bei gesetztem `badge` ist `lead` `kündbar bis ${formatDate(noticeBy)}` und `rest` `(Periode endet ${formatDate(periodEnd)})`. Sonst ist `lead` gleich `text` und `rest` leer.
|
||
- `muted` ist wahr bei Ton `grey` oder `missing`.
|
||
|
||
- [ ] **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**
|
||
|
||
```bash
|
||
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, `null` heißt gültig. Prüfungen und Texte sind die aus `AdminPage.jsx` Z. 70–82.
|
||
|
||
- [ ] **Step 1: Failing Test schreiben**
|
||
|
||
```js
|
||
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**
|
||
|
||
```bash
|
||
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`, `GATES` und die Testdaten aus P0.
|
||
- Produces: die Ansichten aus Step 4, `GATES.p8`, das Szenario `'mitarbeiter'` und die erweiterte Konsolen-Ausnahme des Szenarios `fehler`.
|
||
|
||
- [ ] **Step 1: Testdaten `lang-recht-admin` anlegen**
|
||
|
||
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 `userId` des angemeldeten Kontos, sonst ergänzen (Ansicht `admin-bearbeiten` braucht 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 Szenario `fehler`.
|
||
- Deckt die Ausnahme ihre Meldungen nicht ab, wird sie in `checks.mjs` um die URL-Muster `**/collections/*/documents` (nur POST) und `**/api/admin/employees` erweitert. P7 tut dasselbe für die Finanzen, ein schon vorhandener gleichwertiger Eintrag wird mitbenutzt.
|
||
|
||
- [ ] **Step 4: Ansichten in `views.mjs` anlegen 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:
|
||
|
||
```js
|
||
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ür `button.link` aus Task 1)
|
||
|
||
**Interfaces:**
|
||
- Consumes: `Card`, `Badge`, `Tabs`, `EmptyState`, `PageHeader` (P1), `.link`, `.msg.err`, `.loading`, `currentVersion` und `formatDate` aus `lib/legal.js`.
|
||
- Produces: `LegalTextsTab` und `FileLinks` behalten ihre Props. Dateilinks tragen `class="link"`, der Download-Link zusätzlich `legal-dl`.
|
||
|
||
- [ ] **Step 1: `LegalPage.jsx` anpassen**
|
||
|
||
- `<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.jsx` umbauen**
|
||
|
||
- 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}>` statt `div 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 ein `li` mit der Klasse `legal-version`, bei Status `superseded` zusätzlich `is-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`, `VersionForm` wird wie bisher gerendert.
|
||
|
||
- [ ] **Step 3: `legalUi.jsx` anpassen**
|
||
|
||
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**
|
||
|
||
```bash
|
||
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)` aus `src/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: `ProviderContractsTab` behä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 `.toolbar` mit „Neuer Vertrag“ (`Button variant="primary"`) bleibt.
|
||
- `<table className="table legal-contracts">`. Die Spalten „Wofür“, „Kosten“, „Laufzeit“ und „Kündigungsfrist“ bekommen `hide-sm`.
|
||
- Zeile ohne `legal-tone-*`: `<tr className="row" onClick={(e) => { if (isRowActivation(e)) setOpen(open === c.$id ? null : c.$id) }}>`. Der Helfer ignoriert Klicks auf `a, 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`, weil `isRowActivation` Klicks auf `button` ausfiltert. Ohne ihn klappt weder Klick noch Enter auf dem Button etwas auf, und die Ansicht `rechtliches-anbieter` scheitert.
|
||
- Zelle „Nächster Termin“ mit `view = deadlineView(result, today)`: Bei `view.badge` steht `<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-facts` mit „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**
|
||
|
||
```bash
|
||
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)` aus `src/hooks/useScrollIntoView.js`.
|
||
- Produces: `ContractForm({ contract, onSave, onClose })` und `VersionForm({ title, onSave, onClose })` behalten ihre Signaturen.
|
||
|
||
- [ ] **Step 1: `ContractForm.jsx` umbauen**
|
||
|
||
`FIELDS`, `NUMBERS`, `toForm`, `toDoc`, `set` und `submit` bleiben (Fehler landet weiter in `error`, Erfolg ruft `onClose()`).
|
||
- Der `FaTimes`-Import entfällt. Neu sind `errorRef = useRef(null)` und `useScrollIntoView(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 htmlFor` mit `id="contract-<key>"`. Eingaben tragen `className="input"`, Auswahlen `select`, die Notiz `textarea`. `name`, `required`, `type`, `min`, `inputMode` und `accept` bleiben.
|
||
- 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=…>` in `Field 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.jsx` umbauen**
|
||
|
||
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.err` im offenen Dialog, die Eingabe ist erhalten.
|
||
- Bei 390 px ist der Dialog ein Bottom-Sheet, und der Speichern-Knopf ist erreichbar.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
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`, `.spin` samt `@keyframes admin-spin` löschen)
|
||
|
||
**Interfaces:**
|
||
- Consumes: `src/components/CopyableCredential.jsx` in der Fassung von P5 (Textknöpfe „Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“).
|
||
- Produces: die Klassen für `CopyableCredential` und `.sr-only` fü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.css` verschieben 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**
|
||
|
||
```bash
|
||
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; `showNotice` und `removeEmployee` Z. 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`, `useEmployees` und `useAuth` unverändert.
|
||
- Produces: nichts für spätere Pakete.
|
||
|
||
- [ ] **Step 1: Seitengerüst**
|
||
|
||
- Alle Icon-Importe entfallen. `Notice` rendert ``<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>` und `Button variant="primary"` „Mitarbeiter anlegen“, der einzige schwarze Knopf im Kopf.
|
||
- Der Reiter ist `Tabs` mit einem Eintrag. Das Label bleibt `Mitarbeiter (${employees.length})` ohne `count`, weil `count` den 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 „–“ | – |
|
||
| E-Mail | `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`, `errorRef` und `useScrollIntoView(errorRef, formError)`. `editingSelf` ist die einmal berechnete Bedingung „Admin-Haken gehört zum eigenen Konto“ (heute zweimal inline).
|
||
- „Mitarbeiter anlegen“ und „Bearbeiten“ setzen Formular und `editingEmployee`, leeren `formError` und öffnen mit `setFormOpen(true)`.
|
||
- `submitEmployee` prüft zuerst `validateEmployeeForm(employeeForm, Boolean(editingEmployee))`. Bei einem Text setzt es `formError` (kein Banner) und kehrt zurück.
|
||
- Sonst läuft der Ablauf wie bisher: `employeeBusy` an, `updateEmployee` bzw. `createEmployee`, `employeeBusy` aus.
|
||
- Bei Misserfolg steht `result.error || 'Mitarbeiter konnte nicht gespeichert werden.'` in `formError`, der Dialog bleibt offen.
|
||
- Bei Erfolg folgen `setCreatedCredential` (nur beim Anlegen), `showNotice('success', …)` mit den bisherigen Texten, dann Formular schließen und zurücksetzen.
|
||
- 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 `Field` mit `id="employee-<key>"` und `className="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>`.
|
||
- 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.css` neu 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.jsx` angleichen**
|
||
|
||
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, weil `tests/unit/adminRoute.test.mjs` genau diese Zeichenfolge prüft. Der Seitenabstand kommt von `.loading`, Inline-Style, `spinner` und 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.err` oben 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**
|
||
|
||
```bash
|
||
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 `busy` gesperrt.
|
||
- 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` (Unterordner `1440x900` und `390x844`; 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: `.spin` hat 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 auf `test` ist)
|
||
|
||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||
|
||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||
- Ein lesender Sichttest im Chrome des Eigentümers: Rechtliches mit beiden Reitern und Admin mit der Tabelle. Dabei wird nichts gespeichert oder gelöscht.
|