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

538 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WOMS „Weißes Design“ P7: Finanzen – 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 P7 sind alle Finanzseiten ohne Karten, Kästen und Icons gestaltet: Übersicht, Einstellungen, Bankrückkehr und der Reiter „Rechnungen“ am Kunden. Die Dialoge „Buchung zuordnen“ und „Rechnung schreiben“ laufen auf `Dialog`. `Kpi` ist gelöscht. Funktion, Rückfragen und Texte bleiben.
**Architecture:**
- Der Finanzblock in `src/styles/global.css` (heute ab `/* --- Finanzen --- */`, ca. 320 Zeilen) wird Region für Region neu geschrieben, jeweils zusammen mit den Komponenten der Region. Er enthält nur Tokens, keine Farbliterale und kein `!important`. Task 3 rahmt ihn mit zwei Marken, weil P5 und P6 hinter ihm eigene Blöcke anhängen.
- Die Container-Queries (560 Vorschau, 640 Buchungen, 860 Rechnungen) bleiben. `container-type` sitzt auf einem neuen Wrapper `.fin-body` unter der Abschnittsüberschrift, Dialoge stehen nie darin.
- Neue Datei `src/lib/financeUi.js` mit zwei reinen Helfern: Vorzeichen-Rot und Meldungsklassen.
**Tech Stack:** React 18, react-router-dom 6, Vite 5, 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, Badge, Tabelle, Schlüsselwerte, Formular, Dialog, Meldungen, Laden), „Bewusste Änderungen“ (Esc), „Risiken“ (Scroll-Anker, Vorschlagslisten in Dialogen) und Paket P7. Dazu Anhang `…-umbaukarte.md`, Abschnitte 3, 4, 5/P7, 7.8. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
## Global Constraints
Es gelten die Regeln der Übersicht. Für P7 kommt hinzu:
- **Befehle:** Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT`, `$W` und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"`. `FIN` ist die kommagetrennte Liste aller Ansichten-IDs der Tabelle aus Task 2 Step 2 (Shell-Variable, gesetzt in Task 2 Step 4).
- **Gate-Läufe:** `--gate p7` schaltet `icon-svg` für die `FIN`-Ansichten scharf. Die Finanzseite ist erst nach Task 5 ohne Icons, deshalb laufen Task 3 und 4 mit `--gate p6` (dieselben übrigen Prüfungen). Ab Task 5 gilt `--gate p7`.
- **Dateien:** nur `src/pages/FinancePage.jsx`, `FinanceSettingsPage.jsx`, `BankCallbackPage.jsx`, alle 17 Dateien in `src/components/finance/`, `src/components/ui/Kpi.jsx` (löschen), `src/components/ui/index.js` (Export entfernen), `src/lib/financeUi.js` (neu), `src/styles/global.css` (nur der Finanzblock). Alle `src/lib/finance*.js`, `src/hooks/*`, `CustomerDetailPage.jsx` und alle bestehenden Tests bleiben unberührt.
- **Texte wörtlich.** Erlaubt sind nur `▾`/`▴` statt Chevron-Icons, „+ Position“ (Plus-Icon wird Zeichen), „Entfernen“ in der Positionszeile (heute nur Icon) und „← Zurück zu den Finanzen“. Ladetexte („Speichere …“, „Sende …“, „Rechnungen werden geladen …“) bleiben, nur Spinner und Icons entfallen.
- **Tonnamen** `muted|info|ok|warn|danger` aus `financeFormat.js`/`financeView.js` bleiben, gemappt wird nur in `Badge`.
- **Beträge** behalten das Vorzeichen (U+2212). Rot (`--red-fg`) gilt genau dann, wenn der angezeigte Text mit „−“ beginnt: Klasse `.fin-neg`, bei Kennzahlen `tone="neg"`. Wo die Lib schon markiert (`saldoSicherNegative`, `saldoErwartetNegative`, `low.negative`, `shortfall`), gilt diese Markierung weiter.
- **Container-Falle:** `container-type: inline-size` macht das Element zum Containing Block für `position: fixed`. Es steht nur auf `.fin-body`, `AssignDialog` und `InvoiceCreateModal` sind nie Nachfahren davon.
- **Hygiene in allen P7-Dateien:** keine Icon-Importe, kein `style={{…}}`, kein `size="sm"`, keine Farbliterale, keine Klassen `ui-btn`, `form-control`, `form-group`, `form-label`, `overlay-content`, `overlay-close`, `spinner`, `kpi`. `lockRef`/`busyRef` und alle `window.confirm`-Rückfragen bleiben.
**Gemeinsame Ersetzungen** (Komponenten und CSS):
| Alt | Neu |
|---|---|
| Spinner plus Text im `.empty` | `<div className="loading" role="status">Text wörtlich</div>` |
| `fin-hint` / `fin-hint-warn` / `fin-dialog-*` | `msg muted` / `msg warn` (`role="alert"` bleibt) / Fehler im Dialog `msg err` |
| tonabhängige Meldung | `<p {...noticeProps(tone, 'fin-notice')}>` |
| `Button variant="ghost"`, `size="sm"` | `Button` (Standard), `size` entfällt |
| Aktion in Zeile, Liste oder Meldung | `<button type="button" className="link">`, Links `<a className="link">`, destruktiv `Button variant="danger"` |
| `form-control` / `form-group` + `form-label` | `input`/`select`/`textarea` / `Field` bzw. `FormField` |
| CSS-Farben und -Maße | `--text-muted`, `--text-faint` → `--text-2`; `--warn` → `--orange-fg`; Text-`--danger` → `--red-fg` (Formularfehler bleibt `--danger`); `--border` → `--line`; Flächen und Kartenrahmen entfallen; Gewicht 700/800 → 600 (Tabellentitel 500); Großbuchstaben entfallen; Media-Queries 640/700/720 → 860 |
Die CSS-Schritte nennen nur Abweichungen, Spezifitäts- und Reihenfolgeregeln sowie Maße, die ein Gate-Schritt prüft. Werte, die dort fehlen, bleiben wie im heutigen Finanzblock und folgen der Ersetzungstabelle.
**Steuerelemente, die erreichbar bleiben** (Namen unverändert):
- Übersicht: „Einstellungen“, „Aktualisieren“, „Aufschlüsselung“, der eine Knopf je Aufgabe (bank-verbinden, bank-aktualisieren, rechnung-oeffnen, zuordnen, kunde-oeffnen, fixkosten-uebernehmen, einstellungen), „Alle N anzeigen“/„Weniger anzeigen“, „Schließen“ (Zuordnen-Hinweis), jedes „Erneut versuchen“, das heute steht. Rechnungen: „Rechnung schreiben“, Filter Offen/Überfällig/Bezahlt/Entwürfe/Alle, Aktionen Zahlungsseite, PDF, Senden, Erinnerung senden, Stornieren, Löschen. Buchungen: „Zuordnen“/„Ändern“. Pager: „Zurück“/„Weiter“.
- Dialoge: „ד, „Abbrechen“, „Schließen“, „Zuordnen“, „Zuordnung lösen“, „Zurück“, „Ja, als bezahlt markieren“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“, „Zur Übersicht“.
- Einstellungen und Rest: „Speichern“, „Jetzt abrufen“, „Neu verbinden“/„Bank verbinden“, „Neue Position“, „Bearbeiten“, „Aktivieren“/„Auf inaktiv setzen“, „Löschen“, „Abbrechen“, „Zu den Finanzen“, „Daten erfassen“, „← Zurück zu den Finanzen“.
**Entscheidungen** (die Spec schweigt; nächste Lösung zu heute):
- Entscheidung: `.fin-stack` ist ein normaler Block ohne `gap` und ohne Flex. Der Abstand der Seitenteile ist `margin-top: 36px` auf jedem Geschwister nach dem ersten (`.fin-stack > * + *`). Das ist derselbe Wert wie `.page section + section` aus P1. Beide Regeln setzen dieselbe Eigenschaft am selben Element, also addiert sich nichts. Ein `gap: 36px` ergäbe zwischen zwei Abschnitten 72 px. Als Block lässt `.fin-stack` den oberen Rand von `h2.section:first-child` (8 px, weil `Card` die Überschrift als erstes Kind in ein `<section>` setzt) mit dem Abschnittsabstand verschmelzen. Eine Sonderregel für die Überschrift entfällt.
- Entscheidung: Finanztabellen sind keine `tr.row` (Zeilen nicht klickbar, Aktionen sind echte Knöpfe und Links). Es gibt kein `.hide-sm`/`.hide-md`, schmal stapeln die Container-Queries die Zeilen wie heute.
- Entscheidung: Die Aufgaben-Aktion je Zeile und „Alle N anzeigen“ sind `.link` (bis zu sieben Ringknöpfe untereinander wären zu unruhig). „Aktualisieren“, allein stehendes „Erneut versuchen“, „Jetzt abrufen“ und „Neue Position“ sind Standardknöpfe.
- Entscheidung: Pro Abschnitt höchstens ein schwarzer Knopf (Rechnung schreiben, Speichern, Jetzt abrufen bzw. Bank verbinden, Zuordnen/Senden im Dialog). Die Seitenköpfe haben keinen.
- Entscheidung: Die Aufschlüsselungszeilen („− Steuer-Rücklage …“) bleiben neutral, weil sie Abzüge sind. Rot sind Summen, Salden sowie Posten- und Buchungsbeträge.
- Entscheidung: Der Altersstreifen ist `<ul class="stats fin-ages">` mit `Stat` je Gruppe. Er überschreibt das Flex-Layout durch 4 Spalten, ab Container ≤ 860 px 2 Spalten (die Handy-Regel von `.stats` ergäbe 3 + 1).
- Entscheidung: Der Dialog-Fuß ist `<div className="fin-foot">` (Flex, umbrechend, rechtsbündig, auf dem Handy gleich breite Knöpfe) statt `<span className="grow" />`. Mit `.dialog-foot { flex-wrap: wrap }` aus P6 allein stünde der dritte Knopf bei 390 px linksbündig in einer zweiten Zeile.
## Review Focus
Aus der Übersicht gehören diese Punkte zu P7:
- **Punkt 2, lange Inhalte am Handy:** Testdaten `finanz-minus` mit Kundenname (60 Zeichen), Verwendungszweck (140 Zeichen ohne Leerzeichen) und Gegenpartei (80 Zeichen). `overflow-x` ist im Gate `p7` für alle Ansichten scharf.
- **Punkt 3, Teilausfall:** `finanzen-teilausfall` (Stripe aus, Abos nicht ladbar, Rechnungen und Buchungen antworten 500). Jeder Block zeigt seine Meldung mit „Erneut versuchen“, die Seite bleibt bedienbar.
- **Punkt 4, nur Tastatur:** Esc, Tab-Falle, gesperrtes „ד bei `busy`, „Enter sendet nie“ und „Esc schließt zuerst die Vorschlagsliste“ sind Schritte in `dlg-zuordnen-*` und `dlg-rechnung-*`.
- **Punkt 5, viele Einträge:** 120 Buchungen, 60 Rechnungen, 50 Zeilen je Seite (`finanzen-pager`).
Zusätzlich für P7 (Risiken der Spec): Scroll-Anker `finanzen-rechnungen` und `finanzen-buchungen` liegen nicht unter der Mobil-Leiste (`finanzen-anker`). Die Vorschlagsliste im Kundenfeld wird im scrollenden `.dialog-body` nicht abgeschnitten (`dlg-rechnung-kunde`). Negative Beträge sind rot und tragen ihr Minus (`finanzen-minus`, `finanzen-tabellen`).
---
### Task 1: Hilfsfunktionen `financeUi`
**Files:**
- Create: `src/lib/financeUi.js`
- Test: `tests/unit/financeUi.test.mjs` (neu)
**Interfaces:**
- Consumes: `euro`, `euroWhole`, `signedEuro` aus `src/lib/financeFormat.js` (nur im Test).
- Produces:
- `isNegText(text) → boolean`: wahr, wenn `text` ein String ist und mit „−“ (U+2212) beginnt.
- `noticeProps(tone, extra = '') → { className, role }`: `'ok'` → `msg ok`/`status`, `'warn'` → `msg warn`/`alert`, sonst `msg muted`/`status`. `extra` hängt mit Leerzeichen an `className`.
- [ ] **Step 1: Paketstart und Ausgangslage**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p6 HEAD && git rebase origin/test && git status --short && "$W/check.sh"`
Expected: Der Rebase läuft ohne Konflikt, `git status` ist leer, `# fail 0`. Ist `design-p6` kein Vorfahr von `HEAD`, stoppt der Agent und meldet das. Testzahl N sowie JS- und CSS-Größe aus der `check.sh`-Ausgabe werden notiert (Vergleich in Task 9).
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance src/components/ui/Kpi.jsx src/components/ui/index.js src/lib/financeUi.js src/styles/global.css`
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
- [ ] **Step 2: Failing Test schreiben**
```js
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
const { isNegText, noticeProps } = await loadModule('src/lib/financeUi.js')
const { euro, euroWhole, signedEuro } = await loadModule('src/lib/financeFormat.js')
test('isNegText: nur das angezeigte Minus zählt', () => {
assert.equal(isNegText(euro(-449)), true)
assert.equal(isNegText(signedEuro(-449)), true)
assert.equal(isNegText(euroWhole(-123456)), true)
assert.equal(isNegText(euroWhole(-40)), false) // „0 €“: Rundung auf 0 trägt kein Vorzeichen
assert.equal(isNegText(signedEuro(16879)), false)
assert.equal(isNegText(euro(null)), false) // „–“ (U+2013)
for (const v of ['-4,49 €', '', undefined, null, 5]) assert.equal(isNegText(v), false)
})
test('noticeProps: Klasse und Rolle je Ton', () => {
assert.deepEqual(noticeProps('ok'), { className: 'msg ok', role: 'status' })
assert.deepEqual(noticeProps('warn'), { className: 'msg warn', role: 'alert' })
assert.deepEqual(noticeProps('info'), { className: 'msg muted', role: 'status' })
assert.deepEqual(noticeProps(undefined), { className: 'msg muted', role: 'status' })
assert.deepEqual(noticeProps('warn', 'fin-notice'), { className: 'msg warn fin-notice', role: 'alert' })
})
```
- [ ] **Step 3: Test laufen lassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs`
Expected: FAIL (Modul `financeUi.js` fehlt).
- [ ] **Step 4: `financeUi.js` implementieren**
Zwei benannte Exporte nach der Signatur oben, deutscher Kommentar im Stil der Nachbardateien, keine Abhängigkeit.
- [ ] **Step 5: Test grün**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs && "$W/check.sh" --nur-tests`
Expected: PASS, `# pass` = N + 2, `# fail 0`, Exit 0.
- [ ] **Step 6: Commit**
```bash
git add src/lib/financeUi.js tests/unit/financeUi.test.mjs
git commit -m "Finanzen: Helfer fuer Vorzeichen-Rot und Meldungsklassen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 2: Werkzeug – Finanzdaten, Ansichten, Gate `p7`
**Files** (unter `$W/`):
- Create: `fixtures/finanz-minus.mjs`, `fixtures/finanz-teilausfall.mjs`
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs` (`POST …/finanzen/buchungen/:id/zuordnen` → `{ buchung }`, `POST …/finanzen/bank/rueckkehr` → `{ ok: true, validUntil: '2027-03-29' }`)
**Interfaces:**
- Consumes: `VIEWS`- und `GATES`-Schema aus der Übersicht (`id`, `path`, `fixtures`, `scenario`, `steps(page, ctx)`). Schritte werfen bei Abweichung (`node:assert/strict`), `shots.mjs` meldet das als `step-failed`. `ctx.backend.calls` listet alle beantworteten Requests. Portal-Antworten unter `https://portal.test/api/admin/finanzen/…`.
- Produces: die Ansichten der Tabelle unten und das Gate `p7` (Aufbau nach der Übersicht, Step 3).
- [ ] **Step 1: Testdaten anlegen**
Beide Dateien leiten von `standard.mjs` ab und ersetzen nur die Finanz-Antworten (gleiche Form).
- `finanz-minus`:
- Kopf: Kontostand −1.234,56 €, `verfuegbarCents` < 0, `tiefsterSicher` negativ, `tiefsterErwartet` positiv (beweist Rot je Betrag).
- Vorschau mit 8 Posten und negativen Salden. 9 Aufgaben, darunter je eine mit `rechnung-oeffnen` ohne `hostedUrl`, `zuordnen`, `bank-aktualisieren`, `fixkosten-uebernehmen`.
- Ein Abo `past_due` (gefährdet), alle 4 Altersgruppen mit Werten.
- 60 Rechnungen aller Status (eine Abo-Rechnung, ein Kundenname mit 60 Zeichen). 120 Buchungen: Ein- und Ausgänge, `PDNG`, Verwendungszweck mit 140 Zeichen ohne Leerzeichen, Gegenpartei mit 80 Zeichen, offene Eingänge mit `vorschlaege` (Rechnung und Auszahlung).
- Ein fester Kunde mit E-Mail, aber ohne Straße, PLZ und Ort.
- `finanz-teilausfall`: `stand.stripe = { ok: false, fehler: 'Stripe ist gerade nicht erreichbar.' }`, `abos = { fehler: 'Die Abos konnten nicht geladen werden.' }`, Kopf ohne Kontostand.
- [ ] **Step 2: Ansichten anlegen oder anpassen**
| ID | Aufbau | Prüfung (Schritt wirft bei Abweichung) |
|---|---|---|
| `finanzen` (vorhanden) | `/finance` | Alle Namen aus „Steuerelemente, die erreichbar bleiben“ (Global Constraints) per `getByRole` auffindbar. Mindestens ein `button.link`, alle mit transparentem `background-color` und `border-top-width: 0px` (so auch jedes `button.link-danger` in `finanzen-tabellen`). |
| `finanzen-minus` | `finanz-minus`; Klick „Aufschlüsselung“ | Text in `.stat .v` bzw. `.fin-low .fin-amount`, der mit „−“ beginnt, trägt `.v.neg` bzw. `.fin-neg`. Kein `.v.neg` ohne „−“ (außer in `.fin-abo-stats`). Kein `.fin-breakdown li` mit `.fin-neg`. |
| `finanzen-tabellen` | `finanz-minus` | `.fin-forecast-amount`, `.fin-tx-amount` mit „−“ tragen `.fin-neg`, die anderen nicht. `thead` von `.fin-forecast`, `.fin-invoices`, `.fin-tx` ist bei 1440 nicht `position: absolute`, bei 390 `absolute`. Dreimal `.fin-body` mit `container-type: inline-size`, kein `.overlay` darin. Zwischen `#finanzen-rechnungen` und `#finanzen-buchungen` liegen 36 px (± 1 px), nicht 72. Bei 1440 liegen die Oberkanten der beiden Abschnitte in `.fin-cols` gleich hoch (± 1 px). |
| `finanzen-pager` | `finanz-minus`, zu `#finanzen-buchungen`, „Weiter“ | Fuß „Seite 2 von 3 · 120 Buchungen“, 50 `.fin-tx tbody tr`. |
| `finanzen-anker` | `finanz-minus`, Klick „Zu den Rechnungen“, 800 ms | `#finanzen-rechnungen` hat `scroll-margin-top` 16px (1440) bzw. 68px (390), seine Oberkante liegt dort (± 1 px). |
| `finanzen-teilausfall` | `finanz-teilausfall`, Szenario `fehler`, `/rechnungen` und `/buchungen` → 500 (`page.route`), Reload | Mindestens 2 `[role=alert]` und 2 „Erneut versuchen“, h1 „Finanzen“ sichtbar, „Aktualisieren“ nicht `disabled`. |
| `finanzen-fehler` | Szenario `fehler`, `/uebersicht` → 500, Reload | `[role=alert]` und klickbares „Erneut versuchen“. |
| `dlg-rechnung` (vorhanden) | Klick „Rechnung schreiben“ | `role=dialog` mit Name „Rechnung schreiben“, Klasse `dialog-wide`. Overlay-Rechteck so groß wie das Fenster, kein Vorfahr `.fin-body`. Fokus im Dialog. |
| `dlg-rechnung-kunde` | „Mü“ in `#fin-invoice-customer` | Feldbreite ≥ 90 % von `.dialog-body`. `[role=listbox]` bei 1440 ganz im `.dialog`, bei 390 ≥ 120 px sichtbar. Esc schließt nur die Liste, das zweite Esc den Dialog. |
| `dlg-rechnung-enter` | Fokus in Beschreibung, Enter; `ctx.backend.calls` zählt `POST …/rechnungen` | Dialog bleibt, 0 Requests. „Als Entwurf speichern“ ohne Kunde zeigt `.msg.err[role=alert]` im sichtbaren Teil von `.dialog-body`. |
| `dlg-zuordnen-zahlung` (vorhanden) | Klick „Zuordnen“ an erster offener Eingangsbuchung | Vorschläge sind `label.check` mit Radio. 12 × Tab und 12 × Umschalt+Tab bleiben im `.dialog`. |
| `dlg-zuordnen-bestaetigung` | Vorschlag mit Rechnung, „Zuordnen“ | `.fin-confirm` nennt „in Stripe als bezahlt markiert“, Fuß „Zurück“ und „Ja, als bezahlt markieren“, `ctx.backend.calls` enthält kein `POST …/zuordnen`. Esc schließt ohne Request. |
| `dlg-zuordnen-busy` | wie oben, `POST …/zuordnen` 1500 ms verzögert, „Ja, …“ | Währenddessen `.dialog-close` `disabled`, Esc lässt den Dialog offen. Danach schließt er. |
| `finanzen-einstellungen` (vorhanden) | `/finance/einstellungen` | h2 „Einstellungen“, „Bankzugang“, „Fixkosten“, Link „← Zurück zu den Finanzen“. |
| `finanzen-einstellungen-fehler` | Mindestpuffer „abc“, „Speichern“, dann „Neue Position“ | `[aria-invalid="true"]` hat `box-shadow` mit `rgba(192, 18, 60`, `.msg.err[role=alert]` am Feld. h3 „Neue Position“, Fokus in `#fin-fix-name`. |
| `finanzen-bank-callback`, `-ok` | `/finanzen/bank-callback`, bzw. mit `?code=x&state=y` | „Zu dieser Seite liegt keine Rückmeldung der Bank vor.“ bzw. `.msg.ok` mit „Bankzugang erneuert, gültig bis“. |
| `kunde-invoices` (vorhanden), `kunde-invoices-adresse` | Kunde ohne Anschrift aus `finanz-minus` (andere URL als `kunde-invoices`) | `.fin-address-warn-page` mit Rolle alert und „Zur Übersicht“. „Senden“ eines Entwurfs über 250 € ist `disabled`. |
- [ ] **Step 3: Gate-Eintrag**
Ein Eintrag nach dem Schema der Übersicht. `p7` ist `p6` plus `icon-svg` für die `FIN`-Ansichten und die aller früheren Pakete. Eine `ignore`-Liste des Vorgängers bleibt unverändert.
```js
p7: {
...GATES.p6,
scoped: {
...GATES.p6.scoped,
'icon-svg': { ...GATES.p6.scoped['icon-svg'], views: [...GATES.p6.scoped['icon-svg'].views, /* IDs aus FIN */] },
},
},
```
Die absichtlichen 500er der zwei Fehleransichten schreibt der Browser als `console.error` („Failed to load resource“). Deshalb laufen sie im Szenario `fehler`, dessen Meldungen `console-error` ausnimmt. Nimmt `checks.mjs` nur Meldungen der eigenen Fehlerantworten aus, wird die Ausnahme um Meldungen zu `…/api/admin/finanzen/` erweitert.
- [ ] **Step 4: Vorher-Lauf**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && FIN=finanzen,finanzen-minus,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,finanzen-fehler,finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok,dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy,kunde-invoices,kunde-invoices-adresse && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-vorher" --only "$FIN" --gate p7`
Expected: Exit 1. `icon-svg` meldet `finanzen`, `finanzen-einstellungen`, `dlg-rechnung`, `dlg-zuordnen-zahlung`. Die Schritte schlagen an in `finanzen`, `finanzen-minus`, `finanzen-tabellen`, `finanzen-anker` (390 px), `finanzen-einstellungen-fehler`, `dlg-rechnung*` und `dlg-zuordnen-zahlung`. `finanzen-pager`, `finanzen-teilausfall`, `finanzen-fehler`, `dlg-zuordnen-bestaetigung` und `dlg-zuordnen-busy` sind Wächter für heutiges Verhalten und laufen schon vorher durch.
### Task 3: Seite, Statuszeile, Liquiditätskopf, Aufgaben; `Kpi` löschen
**Files:**
- Modify: `src/pages/FinancePage.jsx`, `src/components/finance/StatusLine.jsx`, `LiquidityHead.jsx`, `TodoList.jsx`, `src/components/ui/index.js`
- Delete: `src/components/ui/Kpi.jsx`
- Modify: `src/styles/global.css` (Marken um den Finanzblock, Regionen „Allgemein“ bis `Block 2: Zu erledigen`, `.kpi*`)
**Interfaces:**
- Consumes: `Stat({ label, value, sub, tone })`, `Card`, `Badge`, `EmptyState`, `PageHeader`, `Button` (P1), `isNegText`, `noticeProps` (Task 1).
- Produces: die Klassen `.fin-stack`, `.fin-top`, `.fin-block`, `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.fin-cols`, `.fin-neg`, `.fin-flag`, `.fin-amount`, `.fin-sr` für Task 4 bis 8.
- [ ] **Step 1: P1-Voraussetzungen prüfen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "Stat|Dialog" src/components/ui/index.js && rg -n "\.stat \.sub|\.table-foot|\.msg\b|\.loading|\.field-row\b" src/styles/global.css | head`
Expected: `Stat` und `Dialog` sind exportiert, die Klassen definiert. Fehlt ein Export, wird er in `index.js` ergänzt.
- [ ] **Step 2: Finanzblock rahmen, CSS-Region „Allgemein“**
Zuerst die Grenzen setzen.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "^/\* (--- Finanzen|Finanzen: Einstellungen|=====|Kunden-Unterseiten|Projekte \(P5\))" src/styles/global.css`
Expected: Der Finanzblock beginnt bei `/* --- Finanzen --- */` und endet vor dem nächsten Blockkommentar (P6, KOMPAT …) bzw. am Dateiende. Davor kommt die Marke `/* ===== FINANZEN (P7) ===== */`, dahinter `/* ===== ENDE FINANZEN ===== */`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n -A2 "===== FINANZEN \(P7\)" src/styles/global.css`
Expected: die Marke, danach `.fin-amount`.
`.fin-amount` und `.fin-sr` bleiben (Werte gleich). Neu oder geändert:
| Regel | Wert |
|---|---|
| `.fin-neg`, `.fin-flag` | `color: var(--red-fg)`, `.fin-flag` zusätzlich `font-weight: 600` |
| `.fin-stack` | keine eigene Regel (Block, kein `gap`, siehe Entscheidung oben) |
| `.fin-stack > * + *` | `margin-top: 36px` |
| `.fin-top` | `display: flex; flex-direction: column; gap: 20px` |
| `.fin-block` | `scroll-margin-top: 16px`, bei ≤ 860 px `68px` (Mobil-Leiste 52 px + 16 px) |
| `.fin-body` | `container-type: inline-size` (einzige Stelle) |
| `.fin-table-wrap` | `overflow-x: auto` |
| `.fin-notice`, `.fin-page-notice` | `margin: 0 0 8px`, `margin-bottom: 16px` |
| `.fin-load-error` | `display: grid; gap: 12px; justify-items: start; padding: 16px 0` |
| `.fin-cols` | einspaltig, `gap: 0 56px` (den Zeilenabstand liefert `.page section + section`); ab 1100 px `minmax(0,1.8fr) minmax(0,1fr)`, `.fin-cols-half` 2 × `minmax(0,1fr)` und zusätzlich `.fin-stack .fin-cols > section + section { margin-top: 0 }`, damit beide Spalten oben bündig stehen (höhere Spezifität als die globale Regel, kein `!important`). |
Gelöscht: `.fin-spin`, `.fin-hint*`, `.fin-link*`, `.fin-stack .kpi-grid`. P5 legt den Reset für `button.link` an. Zeigt die Prüfung in `finanzen` oder `finanzen-tabellen` (Task 2) Browser-Rahmen an `button.link` oder `button.link-danger`, kommt `button.link-danger` in dieselbe Regel (`background: none; border: 0; padding: 0; font: inherit; cursor: pointer`).
- [ ] **Step 3: CSS-Blöcke 0, 1, 2**
- **Block 0:** Die Statuszeile hat keinen Kasten: `.fin-status` Flex, `space-between`, umbrechend, Abstand `8px 16px`, 13 px, `--text-2`. `.fin-status-items` Flex, Abstand `4px 24px`. `.fin-status-warn` `--orange-fg`, 600.
- **Block 1:**
- `.stats.fin-head` ist ein Raster `repeat(auto-fit, minmax(220px, 1fr))`, Abstand `24px 48px`, `margin: 0`. Die Handy-Regel von `.stats` (3 Spalten) würde die breite dritte Kennzahl abschneiden. Die Doppelklasse schlägt `.stats` durch Spezifität.
- `.stats.fin-head .sub` 13 px `--text-2`. `.fin-breakdown-total` mit `border-top: 1px solid var(--line)`, 600.
- `.fin-lows` Raster `auto auto minmax(0,1fr)`. `.fin-low .fin-amount` 18/26 px, 600.
- **Block 2:** `.fin-todos` und `.fin-todo` mit Haarlinien `var(--line)`. `.fin-todo` Raster `80px minmax(0,1fr) auto`, Abstand `12px 16px`, `padding: 12px 0`, bei ≤ 860 px einspaltig. Die Streifenklassen `.fin-todo-hoch/-mittel` und `.fin-todos-more` entfallen.
- [ ] **Step 4: `Kpi` und `.kpi*` löschen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/ui/Kpi.jsx`. Dann die Zeile `export { default as Kpi }` aus `src/components/ui/index.js` und alle `.kpi`-Regeln samt `.fin-head .kpi-value` aus `global.css` entfernen (falls P1 sie nicht schon gelöscht hat).
Expected: Nach Step 5 liefert `rg -n "Kpi|kpi" src` keinen Treffer.
- [ ] **Step 5: Komponenten umbauen**
- `FinancePage`:
- Wurzel `.page`. Kopf: `PageHeader`, „Einstellungen“ als `Button as={Link}` ohne Icon. Ladezustand `loading`.
- Ohne Daten: `msg err` plus `Button variant="primary"` „Erneut versuchen“, ohne `Card` und `style`.
- Mit Daten: `.fin-stack` > `[Teilfehler: msg warn mit .link „Erneut versuchen“]`, `.fin-top` (`StatusLine`, `LiquidityHead`), `TodoList`, `.fin-cols` (`ForecastTable`, `TaxCard`), `.fin-cols.fin-cols-half` (`AboCard`), `InvoiceTable`, `TransactionTable`.
- `assignNotice` als `msg warn fin-page-notice` mit `.link` „Schließen“. `AssignDialog` bleibt am Ende der `.page`.
- `StatusLine`: Zeilen als `.fin-status-item` (Warnungen `.fin-status-warn`, ohne Icon). „Aktualisieren“/„Aktualisiere …“ ist ein Standardknopf. Hinweis über `noticeProps(hint.limited ? 'info' : 'warn')`.
- `LiquidityHead`:
- `<div className="stats fin-head">` mit drei `Stat`. Kontostand: `tone` = `isNegText(euroWhole(…)) ? 'neg' : ''`. Verfügbar: `tone` = `shortfall ? 'neg' : ''`, Flagge „Rücklagen nicht gedeckt“ als `.fin-flag`.
- Tiefster Stand ohne `tone`, Beträge in `.fin-lows` bekommen `.fin-neg` bei `low.negative`.
- „Aufschlüsselung“ ist `<button className="link">` mit `<span aria-hidden="true">▾/▴</span>`, die Zeilen von `breakdownRows` bleiben neutral.
- `TodoList`: `Card` mit Badge „n offen“ als Aktion. Zeile `.fin-todo` ohne Zusatzklasse für die Dringlichkeit, Aktion als `<button className="link">` („Moment …“ während des Laufs, alle gesperrt), Fehler als `msg warn`. „Alle N anzeigen“ als `.link` in `<div className="table-foot">`.
- [ ] **Step 6: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|ui-btn|Kpi" src/pages/FinancePage.jsx src/components/finance/{StatusLine,LiquidityHead,TodoList}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t3" --only finanzen,finanzen-minus,finanzen-anker,finanzen-teilausfall,finanzen-fehler --gate p6`
Expected: `rg` ohne Treffer, Gate Exit 0. In `finanzen-minus.png`: drei Kennzahlen nebeneinander, Kontostand und Verfügbar rot, beim „Tiefsten Stand“ nur der negative Betrag, Aufgaben als Haarlinienzeilen ohne Streifen.
- [ ] **Step 7: Commit**
```bash
git add src/styles/global.css src/pages/FinancePage.jsx src/components/finance/StatusLine.jsx src/components/finance/LiquidityHead.jsx src/components/finance/TodoList.jsx src/components/ui/index.js
git commit -m "Finanzen: Seitenkopf, Statuszeile, Kennzahlen und Aufgaben ohne Karten; Kpi entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
(`git rm` hat `Kpi.jsx` schon gestaged.)
### Task 4: Vorschau, Steuer, Abos
**Files:**
- Modify: `src/components/finance/ForecastTable.jsx`, `TaxCard.jsx`, `AboCard.jsx`, `src/styles/global.css` (Blöcke 3, 4, 5)
**Interfaces:**
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-neg`, `.fin-cols` (Task 3), `Stat`, `.table`, `.stats`.
- Produces: Signaturen unverändert (`ForecastTable({ vorschau, kontostandCents, stripeDown, abosFehler })`, `TaxCard({ steuer })`, `AboCard({ abos })`).
- [ ] **Step 1: CSS Blöcke 3, 4, 5**
- **Vorschau:** `.fin-forecast-label` `overflow-wrap: anywhere`. Randzellen-Padding, `th:first-child` und Hover-Regeln entfallen. `@container (max-width: 560px)` bleibt mit denselben Flächen (`date kind amount / label / sicher erwartet`), Trenner `1px solid var(--line)`, `padding: 10px 0`.
- **Steuer:** `.fin-facts` Raster, Abstand 16 px. `.fin-fact` mit Haarlinie unten `var(--line)`, der letzte ohne. `.fin-fact-big` 22/30, 600. `.fin-fact-unknown` `--orange-fg`.
- **Abos:** `.fin-abos` und `.fin-abo` mit Haarlinien `var(--line)`. `.fin-abo` Raster `minmax(0,1fr) auto`, `padding: 12px 0`.
- Gelöscht: `.fin-stats`, `.fin-stat-*`, `.fin-todo-card`, `.fin-forecast-card`, `.fin-tax-card`, `.fin-abo-card`.
- [ ] **Step 2: Komponenten umbauen**
- `ForecastTable`: `Card title` ohne `className` und `bodyStyle`. Darunter `.fin-body` > `.fin-table-wrap` > `<table className="table fin-forecast …">` (Rollen, `caption.fin-sr`, `data-label` unverändert). Betragszelle mit `fin-neg` bei `isNegText(row.amountText)`. Hinweise unter der Tabelle als `msg warn` (`role="status"`) ohne Icon, die Annahme als `msg muted`.
- `TaxCard`: gleiche Struktur ohne Card-Klasse. `v.noteText` steht als `<p className="msg muted fin-tax-note">`, Wortlaut unverändert (rechtlich relevant).
- `AboCard`: `<div className="stats fin-abo-stats">` (Haken für `finanzen-minus`, ohne eigene Regel) mit `Stat` „Abo-Einnahmen pro Monat“, „zahlende Kunden“ und, nur wenn gesetzt, „gefährdet“ mit `tone="neg"`. Liste wie bisher, Fehlerfall `msg warn` ohne Icon.
- [ ] **Step 3: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|bodyStyle|Kpi" src/components/finance/{ForecastTable,TaxCard,AboCard}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t4" --only finanzen,finanzen-minus,finanzen-teilausfall --gate p6`
Expected: `rg` ohne Treffer, Exit 0. Bild: bei 390 px Vorschau-Zeilen dreizeilig (Datum/Art/Betrag, Posten, beide Salden), bei 1440 als Tabelle; Steuer als Liste mit Haarlinien.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/ForecastTable.jsx src/components/finance/TaxCard.jsx src/components/finance/AboCard.jsx
git commit -m "Finanzen: Vorschau, Steuer und Abos ohne Karten, Container-Query auf Wrapper" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 5: Rechnungen, Buchungen, Blätterleiste, Kundenreiter
**Files:**
- Modify: `src/components/finance/InvoiceTable.jsx`, `TransactionTable.jsx`, `Pager.jsx`, `AddressWarning.jsx`, `CustomerInvoices.jsx`, `src/styles/global.css` (Block 6 und 7 ohne Dialoge)
**Interfaces:**
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.table`, `.seg`, `.toolbar`, `.table-foot`, `.stats`, `Stat`, `noticeProps`, `isNegText`.
- Produces: Signaturen unverändert (`InvoiceTable`, `TransactionTable`, `Pager({ page, pages, total, noun, onPage })`, `AddressWarning({ customer, onShowOverview, variant })`, `CustomerInvoices({ customer, onShowOverview })`) und `export const TRANSACTIONS_ANCHOR = 'finanzen-buchungen'`.
- [ ] **Step 1: CSS Block 6 und 7**
- `.stats.fin-ages`: Liste ohne Zeichen, Raster `repeat(4, minmax(0,1fr))`, Abstand `16px 32px`, `margin: 0 0 16px`; `.fin-ages .stat .v` 18/26 px; `@container (max-width: 860px)` 2 Spalten.
- `.fin-block .seg { flex-wrap: wrap; max-width: 100% }`, bei ≤ 860 px Knopf-Padding `0 9px`. `.fin-pager` (mit `.table-foot`): `justify-content: center; gap: 16px`, `.fin-pager-info` tabular.
- Tabellen: Abstands- und Randzellenregeln entfallen (`.table` aus P1 gilt). Es bleiben `.fin-inv-customer` und `.fin-tx-counterparty` mit `overflow-wrap: anywhere`, `.fin-inv-number`, `.fin-inv-created/-due` und `.fin-tx-date` nowrap, `.fin-inv-statusbox` Raster 3 px, `.fin-actions` Flex `gap: 4px 14px`, `.fin-tx-remittance` mit 2 Zeilen Klemme, `.fin-tx-assign .badge { white-space: normal; height: auto; padding-block: 2px }` (zwei Klassen schlagen `.badge`), `.fin-tx-action` nowrap rechts.
- `@container (max-width: 860px)` (Rechnungen) und `(max-width: 640px)` (Buchungen) bleiben mit den Flächen `number status / customer / created due / total remaining / actions` bzw. `date amount / what / assign action`, Trenner `var(--line)`, `padding: 10px 0`.
- `.fin-address-warn` Flex umbrechend `4px 12px`; `-page` `margin: 0 0 16px`, `-dialog` `margin: 0`.
- [ ] **Step 2: `InvoiceTable` und `Pager`**
- Aufbau: `<section id={…ANCHOR} className="fin-block">` > `Card title="Rechnungen"` mit schwarzem „Rechnung schreiben“ als Aktion > `.fin-body` (Altersstreifen, `.toolbar` mit `.seg`, Meldung, Inhalt, `Pager`). `InvoiceCreateModal` bleibt Geschwister der `Card`.
- Filter: `.seg` mit `aria-pressed` und Klasse `on`. Meldung über `noticeProps(message.tone, 'fin-notice')`. Altersstreifen: `Stat label={cell.label} value={cell.sumText} sub={…Rechnung(en)}` je `<li>`.
- Zeilenaktionen: „Zahlungsseite“ und „PDF“ als `<a className="link" target="_blank" rel="noopener noreferrer">` mit unverändertem `aria-label`, „Senden“ und „Erinnerung senden“ als `.link`-Knopf, „Stornieren“ und „Löschen“ als `Button variant="danger"`. Sperren, Laufbeschriftungen („Sende …“, „Storniere …“, „Lösche …“) und `title` am gesperrten „Senden“ bleiben.
- Tabelle `<table className="table fin-invoices" role="table">`, Rollen, `caption.fin-sr`, `data-label` unverändert. `Pager`: `<nav className="table-foot fin-pager">` mit „Zurück“/„Weiter“ als `Button variant="quiet"`.
- [ ] **Step 3: `TransactionTable`, `AddressWarning`, `CustomerInvoices`**
- `TransactionTable`: `Card title="Buchungen"` in `<section id=TRANSACTIONS_ANCHOR className="fin-block">`, Inhalt in `.fin-body`. Laden `loading`, Fehler `fin-load-error` mit `msg warn` und Standardknopf, Stripe-Hinweis `msg warn` ohne Icon. Betrag `fin-amount fin-tx-amount` plus `fin-neg` bei `isNegText`, `fin-tx-out` bleibt. „Zuordnen“/„Ändern“ als `.link`-Knopf mit `aria-label`.
- `AddressWarning`: `<div className="msg warn fin-address-warn fin-address-warn-{variant}" role="alert">` mit `<button type="button" className="link">` bzw. `Link className="link"`. Der Knopf trägt immer `type="button"`, weil die Variante `dialog` in einem `Dialog as="form"` steht.
- `CustomerInvoices`: Sperrfall als `EmptyState` ohne `Card`, „Daten erfassen“ bleibt schwarz. Sonst `AddressWarning` und `InvoiceTable`.
- [ ] **Step 4: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|bodyStyle|FaSpinner" src/components/finance/{InvoiceTable,TransactionTable,Pager,AddressWarning,CustomerInvoices}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t5" --only finanzen,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,kunde-invoices,kunde-invoices-adresse --gate p7`
Expected: `rg` ohne Treffer, Exit 0 (kein `overflow-x` bei 390 px trotz langer Namen), alle Schritte laufen durch. Bild: Altersstreifen 4 Spalten (1440) bzw. 2 (390), Filter als graue Schiene, Tabellen mit Haarlinien und Textaktionen.
- [ ] **Step 5: Commit**
```bash
git add src/styles/global.css src/components/finance/InvoiceTable.jsx src/components/finance/TransactionTable.jsx src/components/finance/Pager.jsx src/components/finance/AddressWarning.jsx src/components/finance/CustomerInvoices.jsx
git commit -m "Finanzen: Rechnungen und Buchungen als Tabellen ohne Karten, Textaktionen, Blaetterleiste" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 6: Einstellungen, Bankzugang, Fixkosten, Bankrückkehr, `FormField`
**Files:**
- Modify: `src/pages/FinanceSettingsPage.jsx`, `src/pages/BankCallbackPage.jsx`, `src/components/finance/SettingsCard.jsx`, `BankAccessCard.jsx`, `FixedCostsCard.jsx`, `FormField.jsx`, `src/styles/global.css` (Region „Finanzen: Einstellungen“)
**Interfaces:**
- Consumes: `.field`, `.hint`, `.field-row`, `.input/.select/.textarea` mit Fehlerring bei `[aria-invalid="true"]` (P1), `.kv`, `.msg`, `noticeProps`.
- Produces: `FormField({ id, label, hint, error, children })` unverändert. Das Kind bekommt weiter `id`, `aria-invalid`, `aria-describedby`.
- [ ] **Step 1: CSS Einstellungen**
- `.fin-fieldset` Raster `gap: 16px`, ohne Rand und Innenabstand. `.fin-form-field { min-width: 0 }`. `.fin-input-amount` rechts. `.fin-counter` 12 px `--text-2` rechts, `margin-top: -8px`, `.fin-counter-over` `--danger` (Formularfehler), 600. `.fin-form-actions` Flex, `gap: 12px`, `margin-top: 16px`.
- `.fin-fixed-form` ohne Rahmen, nur `padding-bottom: 24px; margin-bottom: 8px; border-bottom: 1px solid var(--line)`. `.fin-form-title` 14/20, 600. `.fin-result` 16 px, 600.
- `.fin-fixed-list` und `.fin-fixed` mit Haarlinien `var(--line)`. `.fin-fixed` Raster `minmax(0,1fr) auto`, `padding: 12px 0`. `.fin-fixed-off` Name und Betrag `--text-2`.
- Gelöscht: `.fin-form-grid`, `.fin-bank-facts*`, `.fin-field-error`, `.fin-form-hint`, `.fin-form-notice`, `.fin-form-field .form-control[aria-invalid]`.
- [ ] **Step 2: Komponenten umbauen**
- `FormField`: `.field fin-form-field`, Hinweis `<div className="hint">`, Fehler `<div className="msg err" role="alert">`. Den Fehlerring liefert allein `aria-invalid`.
- `SettingsCard`: Feldpaare als `.field-row`, Felder `input fin-input-amount`, Textarea `textarea`, Ladefehler `fin-load-error` mit `msg warn`. „Speichern“/„Speichere …“ schwarz ohne Icon. Meldung über `noticeProps(notice.tone)`.
- `BankAccessCard`: `Card` mit Badge als Aktion. Fakten als flaches `<dl className="kv">` (`<dt>`/`<dd className="num">` ohne `<div>`-Gruppen). Schwarz ist „Jetzt abrufen“ (bei `canRefresh`), sonst „Bank verbinden“, der andere Knopf ist Standard. „Rufe ab …“ und „Leite weiter …“ bleiben.
- `FixedCostsCard`: „Neue Position“ im Kopf als Standardknopf ohne Plus. Formular mit `<h3 id="fin-fix-form-title" className="fin-form-title">`, `.field-row`, „Speichern“ schwarz, „Abbrechen“ Standard. Zeilenaktionen: „Bearbeiten“ und `row.toggleLabel` als `.link`, „Löschen“ als `Button variant="danger"`. `aria-label`, `window.confirm` und Sperren bleiben.
- [ ] **Step 3: Seiten**
- `FinanceSettingsPage`: `PageHeader` mit „← Zurück zu den Finanzen“ als `Button as={Link}` (Standard, Pfeil ist Zeichen). Inhalt `.fin-stack` mit den drei Abschnitten.
- `BankCallbackPage`: ohne `Card` und `style`. Ergebnis `<p className="msg ok fin-result">` bzw. `msg err fin-result` mit unveränderter `role`. Laden `loading` („Bankzugang wird eingerichtet …“). „Zu den Finanzen“ bleibt schwarz.
- [ ] **Step 4: Hygiene, Bau, Bild**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|form-control|form-group|FaSpinner" src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/{SettingsCard,BankAccessCard,FixedCostsCard,FormField}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t6" --only finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch. Bild `finanzen-einstellungen-fehler`: roter Ring und Fehlertext am Feld, keine Kästen.
- [ ] **Step 5: Commit**
```bash
git add src/styles/global.css src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/SettingsCard.jsx src/components/finance/BankAccessCard.jsx src/components/finance/FixedCostsCard.jsx src/components/finance/FormField.jsx
git commit -m "Finanzen: Einstellungen, Bankzugang, Fixkosten und Bankrueckkehr ohne Karten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 7: `AssignDialog` auf `Dialog`
**Files:**
- Modify: `src/components/finance/AssignDialog.jsx`, `src/styles/global.css` (Dialog-Regionen für die Zuordnung)
**Interfaces:**
- Consumes: `Dialog({ title, onClose, busy, wide, footer, children })` (P1), `Field`, `.check`, `.kv`, `.msg`, `useScrollIntoView`.
- Produces: `AssignDialog({ transaction, invoices, invoicesLoading, invoicesError, invoicesLoaded, onClose, onDone })` unverändert. `onClose({ refresh })` und `onDone(buchung)` verhalten sich wie bisher.
- [ ] **Step 1: CSS Zuordnen**
- `.fin-foot`: Flex, `flex: 1`, umbrechend, `justify-content: flex-end`, Abstand 8 px; bei ≤ 860 px `.fin-foot .btn { flex: 1 1 auto }`.
- `.fin-choices`: Raster ohne Rand. `.fin-choice` (mit `.check`): volle Breite, `padding: 8px 0`, Haarlinie unten. Mit gewähltem Radio ist `.fin-choice-label` 600 (`:has(input:checked)`). `.fin-choice-off` `opacity: .55; cursor: not-allowed`. `.fin-choice-detail` 13 px `--text-2`, mit `.warn` `--orange-fg`.
- `.fin-confirm` 600 in `--orange-fg`, kein Kasten. `.fin-assigned { margin: 0 }`. `.kv .fin-tx-facts-amount { text-align: left; font-weight: 600 }`.
- Gelöscht: `.fin-tx-facts*`, `.fin-confirm:focus`, Kastenregeln von `.fin-choice`.
- [ ] **Step 2: Komponente umbauen**
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay-Markup und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `dirtyRef`, `useScrollIntoView(errorRef, error)` und `confirmRef`.
- Gerüst `<Dialog title="Buchung zuordnen" onClose={close} busy={disabled} footer={…}>`. `close()` ist parameterlos (das „ד ruft es mit dem Klick-Event), prüft `lockRef` und ruft `onClose({ refresh: dirtyRef.current })`.
- Body: Fakten als flaches `<dl className="kv">`, Fehler `msg err` mit `ref={errorRef}`, Hinweise `msg muted`/`msg warn` mit unveränderten Rollen. Vorschläge und Kategorien über `radio(o)` als `<label className="check fin-choice…">`. „Andere offene Rechnung“, „Fixkosten“ und „Notiz (optional)“ sind `Field` mit `select`/`textarea`. Beim Bestätigen steht der Text im `.fin-confirm` (`role="group"`, `tabIndex={-1}`, Fokus per `confirmRef` wie heute).
- Fuß je Zustand: zugeordnet „Schließen“ und, bei `canUnassign`, „Zuordnung lösen“ (beide Standard). Bestätigend „Zurück“ (Standard) und „Ja, als bezahlt markieren“ (schwarz, „Markiere …“). Sonst „Abbrechen“ (Standard) und „Zuordnen“ (schwarz, „Ordne zu …“, gesperrt ohne Auswahl).
- Der Zweischritt bleibt: Bei einer Rechnung geht vor der Bestätigung keine Anfrage raus.
- [ ] **Step 3: Hygiene, Bau, Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|fin-overlay|fin-close|form-control|form-label" src/components/finance/AssignDialog.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t7" --only dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle drei Schritte laufen durch. Bild: weißer Dialog, Vorschläge als Radiozeilen mit Haarlinien, bei 390 px Bottom-Sheet mit umbrechendem Fuß.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/AssignDialog.jsx
git commit -m "Finanzen: Buchung zuordnen auf Dialog-Baustein (Esc, Sperre, Vorschlaege als Zeilen)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 8: `InvoiceCreateModal` auf `Dialog`
**Files:**
- Modify: `src/components/finance/InvoiceCreateModal.jsx`, `src/styles/global.css` (Rest der Dialog-Region)
**Interfaces:**
- Consumes: `Dialog`, `Field`, `AutocompleteInput` (P1, rendert `.input`), `AddressWarning`, `.fin-foot` (Task 7).
- Produces: `InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer, onShowOverview })` unverändert, samt Vertrag `onCreated(rechnung, info)`.
- [ ] **Step 1: CSS Rechnung schreiben**
- `.fin-items` ohne Rand. `.fin-item` Raster `minmax(0,1fr) 80px 120px 110px auto`, Abstand `8px 12px`, `align-items: end`. `.fin-item .input { width: 100% }` (zwei Klassen schlagen `.input`), `.fin-item .fin-amount` rechts. `.fin-item-label` 12 px `--text-2`, ab der zweiten Zeile visuell versteckt. `.fin-items-foot` Flex `space-between`, `margin-top: 8px`. `.fin-total` Flex, `gap: 16px`, 600.
- `.fin-dialog-row` Raster `180px minmax(0,1fr)`, Abstand 12 px.
- Bei ≤ 860 px: `.fin-item` Raster `minmax(0,.7fr) minmax(0,1fr) minmax(0,1fr) auto` mit Flächen `desc desc desc desc / qty price sum remove`, Labels sichtbar. `.fin-dialog-row` einspaltig.
- `.autocomplete .input { width: 100% }` legt P6 an (`rg -n "\.autocomplete \.input" src/styles/global.css`). Fehlt sie, kommt sie hierher (Prüfung in `dlg-rechnung-kunde`).
- Alte Hüllenregeln löschen: `.fin-close*`, `.fin-overlay`, `.fin-dialog` samt `:focus`, `.fin-dialog-error`, `.fin-dialog-warn`, `.fin-dialog-sub`, `.fin-dialog-actions`, `.fin-icon-btn*`.
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fin-(overlay|close|icon-btn)" src; rg -n "fin-dialog" src | rg -v "fin-dialog-row"`
Expected: Vor Step 2 trifft es noch `InvoiceCreateModal.jsx`, nach Step 2 höchstens den KOMPAT-Block.
- [ ] **Step 2: Komponente umbauen**
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `errorRef` mit `useScrollIntoView`.
- Gerüst `<Dialog as="form" wide title="Rechnung schreiben" onClose={close} busy={disabled} onSubmit={(e) => e.preventDefault()} footer={…}>`. Das Formular hat keinen Submit-Knopf. Jeder Knopf darin trägt `type="button"` (`Button` setzt keinen Standard-Typ): „Abbrechen“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“ und der Knopf in `AddressWarning`. Deshalb sendet Enter nie. `close()` prüft `lockRef` und ruft `onClose()`.
- Body: zuerst Fehler `msg err` mit `ref={errorRef}`. Kunde als `Field` mit `htmlFor="fin-invoice-customer"` (fester Kunde oder `AutocompleteInput`), Hinweise `hint`/`msg warn`, `AddressWarning variant="dialog"` mit dem bisherigen `onShowOverview`-Handler.
- Positionen als `<fieldset className="fin-items">` mit `.fin-item`-Zeilen (`input`, `fin-amount`, `aria-label`s unverändert). „Entfernen“ ersetzt den Papierkorb als `<button type="button" className="link">` mit `aria-label="Position n entfernen"`, gesperrt bei nur einer Position. „+ Position“ ist ein Standardknopf. Zahlungsziel und Notiz als `Field` in `.fin-dialog-row`.
- Fuß `<div className="fin-foot">`: „Abbrechen“ und „Als Entwurf speichern“ (Standard), „Senden“ (schwarz, `disabled` und `title` wie heute). „Speichere …“ und „Sende …“ bleiben.
- Esc: Ein offenes Vorschlagslisten-Popover ruft `preventDefault`, dann schließt `Dialog` nicht. Ohne Liste schließt Esc über `close`.
- [ ] **Step 3: Hygiene, Bau, Prüfung**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|form-control|form-group|form-label|FaTrash|FaPlus" src/components/finance/InvoiceCreateModal.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t8" --only dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,kunde-invoices-adresse --gate p7`
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch (Overlay in Fenstergröße, Esc-Reihenfolge, Enter ohne Request, Feldbreite). Bild bei 390 px: Positionszeilen zweizeilig, Fuß mit umbrechenden Knöpfen, Vorschlagsliste sichtbar.
- [ ] **Step 4: Commit**
```bash
git add src/styles/global.css src/components/finance/InvoiceCreateModal.jsx
git commit -m "Finanzen: Rechnung schreiben auf Dialog-Baustein (breit, Enter sendet nie, Esc schliesst Liste zuerst)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```
### Task 9: Gate P7, 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, `# pass` = N + 2 und `# fail 0`. Die Größen von JS und CSS aus dieser Ausgabe gehen in Step 3.
- [ ] **Step 2: Vollständiger Gate-Lauf, Hygiene und tote Klassen**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7" --gate p7`
Expected: Exit 0. Alle Prüfungen aus P1 sind für alle Ansichten leer, `icon-svg` für alle `FIN`-Ansichten und die der früheren Pakete.
Run:
```bash
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh || exit 1
F="src/components/finance src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx"
finblock() { sed -n '/^\/\* ===== FINANZEN (P7)/,/^\/\* ===== ENDE FINANZEN/p' src/styles/global.css; }
rg -n "react-icons|@tabler|style=\{|size=\"sm\"|ui-btn|form-control|form-group|form-label|overlay-content|overlay-close|spinner|Kpi|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(" $F
finblock | rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(|!important|var\(--(surface|border|text-muted|text-faint|r-|shadow-[123]|warn|info|ok)\b"
finblock | rg -o "\.fin-[a-z0-9-]+" | sort -u | sed 's/^\.//' | while read c; do rg -q "(^|[^A-Za-z0-9_-])${c}([^A-Za-z0-9_-]|$)" src --glob '!*.css' || echo "TOT $c"; done
finblock | wc -l; finblock | rg -c "container-type"
git diff --name-only design-p6..HEAD
```
Expected:
- Die ersten beiden Befehle geben nichts aus.
- Der dritte nennt nur `fin-address-warn-dialog` und `fin-address-warn-page` (per Template-Präfix gebaut, erlaubt).
- Der Finanzblock hat 250 bis 360 Zeilen, `container-type` steht genau einmal da (`.fin-body`).
- Der letzte Befehl (`git diff --name-only design-p6..HEAD`) nennt nur die Dateien aus „Global Constraints / Dateien“ und `tests/unit/financeUi.test.mjs`.
- [ ] **Step 3: Bundle-Größe**
Die JS- und CSS-Größe aus der `check.sh`-Ausgabe von Step 1 wird mit Task 1 Step 1 verglichen. Keiner der beiden Werte ist größer.
- [ ] **Step 4: Code-Review (sonnet)**
Geprüft wird der Diff `design-p6..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P7. Prüfaufträge:
- Verhalten gleich geblieben: Sperren, `window.confirm`, Zweischritt in `AssignDialog`, Rollen und `data-label` der Tabellen.
- `busy` und `close()` sind in beiden Dialogen richtig angeschlossen, kein Dialog sitzt in `.fin-body`.
- Jede Klasse `fin-*` ist definiert oder bewusst entfallen, Rot nur bei Minus, Texte nur wie in „Global Constraints“ geändert.
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Nach Korrekturen kommen Step 1 und Step 2 noch einmal, damit die Bilder den Stand zeigen, der getaggt wird.
- [ ] **Step 5: Tag und Übergabe**
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p7 && git log --oneline origin/test..design-p7 && git rev-parse design-p7`
Der Eigentümer bekommt:
- die Screenshots aus `shots/p7/1440x900` und `shots/p7/390x844` (Auswahl: `finanzen`, `finanzen-minus`, `finanzen-teilausfall`, `dlg-rechnung-kunde`, `dlg-zuordnen-bestaetigung`, `finanzen-einstellungen-fehler`, `kunde-invoices-adresse`)
- den Hinweis, dass negative Beträge jetzt rot sind und beide Dialoge neu auch mit Esc schließen
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p7^{commit}:refs/heads/test'`
- die Rücknahme (nur solange P7 das jüngste Paket ist): `git revert --no-commit design-p6..design-p7`, Commit und Push durch den Eigentümer
- den Hinweis für P10: Die KOMPAT-Zeilen zu `.fin-link` und `.fin-dialog` (760 px) sind tot und fallen mit dem Block weg.
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
- Ein lesender Sichttest im Chrome des Eigentümers: Finanzen mit Statuszeile und Rechnungsliste, Scroll von „Zu den Rechnungen“. Keinen Dialog öffnen, der etwas sendet.