Plan: Phase 5 vollstaendig abgeschlossen (5.2/5.3/5.5 auf erledigt)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-02 10:44:15 +00:00
parent 349991a4e8
commit 90f3245e41

View file

@ -7,15 +7,20 @@
## Stand & Wiedereinstieg (zuletzt: 02.07.2026) ## Stand & Wiedereinstieg (zuletzt: 02.07.2026)
**Fertig:** Phasen **0–4** vollständig · **5.1** · **5.2 Teil 1** (Form-Rules-Concern + `User::syncCompaniesWithRoles()`) · **5.3 Teil 1+2** (`Contact::displayName()`, `Category::localizedName()`) + `portalBadgeColor()`-Toter-Code entfernt · **5.4 vollständig** (page-header 33 Seiten, empty-state 5 States, table-pagination 13 Wrapper) · **5.5 vollständig** (`<x-portal.alert>`, `<x-session-status>`, `<x-password-input>` über login/register/reset/confirm — **funktional gegengetestet 02.07.2026**). Zusätzlich Backfill-Command `users:backfill-portal-role` + Import-Nachlauf (rollenlose aktive Nutzer). **Phase 5 vollständig abgeschlossen.** Fertig: Phasen **0–5**.
Zusätzlich **Perf-Fix** auf `/admin/me/press-releases`: `User::accessibleCompanyIds()` hydratisierte ~107k Company-Models (jetzt `toBase()`-pluck + memoisiert), `accessibleBy()` nutzt company_id-Subquery, Status-Counts via UNION statt OR-GROUP-BY; neue `PressRelease::accessibleStatusCountsFor()`. Und Fix: fehlendes `.search-wrap`-CSS (Suchfeld /admin/me/firmen). - **5.1** · **5.2** (Form-Rules-Concern + `User::syncCompaniesWithRoles()` + Feldraster-Components `<x-portal.user-role-fields>`/`<x-portal.user-billing-fields>` in create+edit) · **5.3** (`Contact::displayName()`, `Category::localizedName()`, `PressReleaseStatus::badgeClass()` aus 7 divergenten match-Blöcken; toter `portalBadgeColor()`- und `statusColor`-Code entfernt) · **5.4** (page-header 33 Seiten, empty-state 5 States, table-pagination 13 Wrapper) · **5.5** (`<x-portal.alert>`, `<x-session-status>`, `<x-password-input>` — **funktional gegengetestet**).
- Zusätzlich: Backfill-Command `users:backfill-portal-role` + Import-Nachlauf; **Perf-Fix** `/admin/me/press-releases` (107k Company-Hydration weg via `toBase()`-pluck + Memoisierung, `accessibleBy()`-Subquery, Status-Counts via UNION; neue `PressRelease::accessibleStatusCountsFor()`); Fix fehlendes `.search-wrap`-CSS.
Pint sauber, betroffene Test-Filter grün. Pint sauber, betroffene Test-Filter grün.
**Hier geht es weiter** — der Rest ist teils UI-Extraktion (visuelle/funktionale QA nötig), teils backend-testbar. Empfehlung weiterhin: **eine Component/ein Schritt pro Commit**, im Browser gegenchecken (`vendor/bin/sail npm run dev`). **Bewusst NICHT gemacht (dokumentierte Entscheidungen, kein offener Rest):**
- `UserUpsertService`/`SaveUserAction` — als Over-Engineering verworfen (Anlegen vs. Bearbeiten divergieren stark); nur der geteilte Kern `syncCompaniesWithRoles()` gezogen.
- `company-link-picker` + `contact-fields` bleiben inline — company-picker ist zwischen create/edit optisch divergiert (Clear-Button, flux:text vs. CSS-Vars, Grid-Spalten), contact-fields existiert nur in edit → keine Duplizierung zu eliminieren.
- Rate-Limit-Trait (`throttleKey()`/`ensureNotRateLimited()`) — login vs. magic-link teilen zu wenig Fläche.
- `flux:badge :color` in admin/users behält sein eigenes Flux-Farbvokabular (Einzel-Call-Site).
- `<x-portal.money>`/`<x-portal.datetime>` — geringer Hebel, nicht umgesetzt.
1. **5.3 Rest**: `PressReleaseStatus`-Badge-Methode + `<x-portal.status-badge>`/`<x-portal.money>`/`<x-portal.datetime>` (die Status-Badge-Call-Sites nutzen divergente Farb-Vokabulare — vor Zentralisierung klären). Letzter sinnvoller Schliff von Phase 5. **Nächster echter Block: Phase 6** (Web-Frontend) — separat, dem User gehörend, vor jedem Löschen bestätigen lassen.
2. **5.2 Rest**: Blade-Feld-Components (`billing-fields`, `company-link-picker`, `role-checkboxes`, `contact-fields`) in den User-Formularen. Der gemeinsame `UserUpsertService` wurde als Over-Engineering verworfen — nur der wirklich geteilte Kern (`syncCompaniesWithRoles`) wurde gezogen. Rate-Limit-Trait bleibt zurückgestellt (login vs. magic-link divergieren zu stark).
3. **Phase 6** (Web-Frontend) — separat, dem User gehörend, vor jedem Löschen bestätigen lassen.
**Wichtig beim Wiedereinstieg:** `resources/views/components/layouts/app/sidebar.blade.php` gehört dem User und bleibt aus allen Commits ausgeschlossen (`git reset -q -- …` vor jedem `git add`). `resources/views/web/**` und `components/web/**` erst in Phase 6. **Wichtig beim Wiedereinstieg:** `resources/views/components/layouts/app/sidebar.blade.php` gehört dem User und bleibt aus allen Commits ausgeschlossen (`git reset -q -- …` vor jedem `git add`). `resources/views/web/**` und `components/web/**` erst in Phase 6.
@ -100,10 +105,10 @@ Keine Fachlogik-Änderung. Reduziert erreichbare Demo-Komponenten und Altlasten.
## Phase 5 — Admin-CRUD & Auth: Concerns + Blade-Components (risikoarm) ## Phase 5 — Admin-CRUD & Auth: Concerns + Blade-Components (risikoarm)
- [x] **5.1 Tabellen-Concerns** — `HasSortableTable::sort()` (5 byte-identische Kopien zentralisiert; `$sortBy`/`$sortDir`-Defaults bleiben in der Komponente, da sie divergieren) und `HasFullTextSearch::supportsFullTextSearch()` (9 byte-identische Kopien — 4 Index-Listen + 5 Firmen-Comboboxen; nicht mehr genutzte `DB`-Imports entfernt). `AdminPerformanceCache::activeInactiveStats(Builder)` bündelt den identischen `SUM(CASE WHEN is_active)`-Block der Benutzer- und Firmenliste. Tests: erweiterte `AdminPerformanceCacheTest`; abgesichert durch 99 `Admin/`-Tests. **Bewusst zurückgestellt:** `HasResettableFilters` (die `updatedXxxFilter()` divergieren — manche normalisieren zusätzlich Werte; ein generischer `updated()`-Hook wäre eine Verhaltensänderung) und eine gemeinsame `hydrateRelationCounts()` (Count-Hydration unterscheidet sich je Tabelle real). - [x] **5.1 Tabellen-Concerns** — `HasSortableTable::sort()` (5 byte-identische Kopien zentralisiert; `$sortBy`/`$sortDir`-Defaults bleiben in der Komponente, da sie divergieren) und `HasFullTextSearch::supportsFullTextSearch()` (9 byte-identische Kopien — 4 Index-Listen + 5 Firmen-Comboboxen; nicht mehr genutzte `DB`-Imports entfernt). `AdminPerformanceCache::activeInactiveStats(Builder)` bündelt den identischen `SUM(CASE WHEN is_active)`-Block der Benutzer- und Firmenliste. Tests: erweiterte `AdminPerformanceCacheTest`; abgesichert durch 99 `Admin/`-Tests. **Bewusst zurückgestellt:** `HasResettableFilters` (die `updatedXxxFilter()` divergieren — manche normalisieren zusätzlich Werte; ein generischer `updated()`-Hook wäre eine Verhaltensänderung) und eine gemeinsame `hydrateRelationCounts()` (Count-Hydration unterscheidet sich je Tabelle real).
- [~] **5.2 User create/edit entdoppeln** — **Teil 1 erledigt:** `ProvidesUserFormRules`-Concern bündelt die zwischen Anlegen/Bearbeiten identischen Validierungsblöcke (`userIdentityRules(?int $ignore)`, `userRolesAndCompaniesRules()`, `userBillingRules()`); die nur im Bearbeiten genutzten Profil-/Kontakt-Regeln bleiben dort inline. Beide Formulare nutzen die Spreads, 31 User-Tests grün. **Offen (bewusst, mit User abzustimmen):** gemeinsamer `UserUpsertService`/`SaveUserAction` für die ~175-Zeilen-`save()` (Anlegen/Bearbeiten divergieren stark — Bearbeiten trägt zusätzlich Profil, Kontakte, Passwort, 2FA), sowie die Blade-Feld-Components (`billing-fields`, `company-link-picker`, `role-checkboxes`, `contact-fields`) — Markup-Extraktion braucht visuelle QA (analog zur zurückgestellten 2.8). - [x] **5.2 User create/edit entdoppeln** — **Erledigt:** `ProvidesUserFormRules`-Concern bündelt die identischen Validierungsblöcke (`userIdentityRules(?int $ignore)`, `userRolesAndCompaniesRules()`, `userBillingRules()`); `User::syncCompaniesWithRoles()` als geteilter Persistenz-Kern; Feldraster-Components `<x-portal.user-role-fields>` + `<x-portal.user-billing-fields>` (nur das innere Grid, Wrapper bleibt pro Seite) in create+edit — via 26 UserManagement-Feature-Tests end-to-end abgedeckt; Nebeneffekt: create zeigt jetzt den zuvor verschluckten `billing.name`-Fehler. **Bewusst verworfen:** gemeinsamer `UserUpsertService`/`SaveUserAction` (Over-Engineering — Bearbeiten trägt zusätzlich Profil/Kontakte/Passwort/2FA); `company-link-picker` (create/edit optisch divergiert) und `contact-fields` (nur in edit) bleiben inline.
- [~] **5.3 Formatierungs-Components/Enum** — **Erledigt (Model-Methoden, getestet):** `Contact::displayName()` (6 Relation-Stellen ersetzt; `ContactDisplayNameTest`) und `Category::localizedName()` (4 Stellen; `CategoryLocalizedNameTest`). **Geklärt & erledigt:** Die `portalBadgeColor()`-Helfer in `companies/index`, `companies/show`, `contacts/index` waren toter Code (kein Aufruf) und wurden entfernt — eine `Portal::badgeColor()`-Zentralisierung ist damit gegenstandslos. **Offen (UI-/Blade-lastig, mit User abzustimmen):** `PressReleaseStatus`-Badge-Methode sowie die Blade-Components `<x-portal.portal-badge>`, `<x-portal.status-badge>`, `<x-portal.money>`, `<x-portal.datetime>` (Markup-Extraktion braucht visuelle QA). - [x] **5.3 Formatierungs-Components/Enum** — **Erledigt (Model-/Enum-Methoden, getestet):** `Contact::displayName()` (6 Stellen; `ContactDisplayNameTest`), `Category::localizedName()` (4 Stellen; `CategoryLocalizedNameTest`), `PressReleaseStatus::badgeClass()` (7 divergente `match`-Blöcke in customer/admin index+show+edit+dashboard zusammengeführt; `PressReleaseStatusBadgeClassTest`). Archiviert/Entwurf jetzt konsistent `muted` (User-Entscheidung 02.07.). Toter Code entfernt: `portalBadgeColor()` (3×) und `statusColor` (Flux-Farben, customer/show). **Bewusst nicht zentralisiert:** `flux:badge :color` in admin/users (eigenes Flux-Farbvokabular, Einzel-Call-Site); `<x-portal.money>`/`<x-portal.datetime>` (geringer Hebel).
- [x] **5.4 Seiten-Struktur-Components** — **erledigt.** `<x-portal.page-header>` (Props size sm/md/lg + descriptionWidth, Slots :meta/:actions) über **33 Seiten** ausgerollt; `<x-portal.empty-state>` (Icon-Slot, variant warm/err, Default-Slot für Onboarding) über **5 States**; `<x-portal.table-pagination>` (:paginator) über **13 Wrapper**. **Bewusst nicht gebaut:** `<x-portal.table-panel>` (nur triviales `<article class="panel">`, keine wiederholte Innenstruktur → Over-Engineering) und `<x-portal.searchable-select>` (existiert nirgends — keine `flux:combobox`). **Bewusst bespoke:** companies/show + press-releases/show (Sonder-Header mit Logo/Untertitel). - [x] **5.4 Seiten-Struktur-Components** — **erledigt.** `<x-portal.page-header>` (Props size sm/md/lg + descriptionWidth, Slots :meta/:actions) über **33 Seiten** ausgerollt; `<x-portal.empty-state>` (Icon-Slot, variant warm/err, Default-Slot für Onboarding) über **5 States**; `<x-portal.table-pagination>` (:paginator) über **13 Wrapper**. **Bewusst nicht gebaut:** `<x-portal.table-panel>` (nur triviales `<article class="panel">`, keine wiederholte Innenstruktur → Over-Engineering) und `<x-portal.searchable-select>` (existiert nirgends — keine `flux:combobox`). **Bewusst bespoke:** companies/show + press-releases/show (Sonder-Header mit Logo/Untertitel).
- [~] **5.5 Flash/Auth-Components** — **Erledigt:** `<x-portal.alert variant=success|error>` (6 Flash-Blöcke: categories/create+edit, footer-codes/index, presets/index) und `<x-session-status>` (login, forgot-password, register, magic-link). **Bewusst bespoke:** companies/show (icon-loses Flash-Design + info-Variante), verify-email/settings-profile (eigene Status-Logik). **Erledigt (funktional gegengetestet 02.07.2026):** `<x-password-input>` über login/register/reset/confirm — Alpine-`show`-State in die Component verlagert, `:placeholder`-Prop, optionaler `labelAction`-Slot für den „Passwort vergessen?"-Link, `show-error`-Schalter für Bestätigungsfelder. Login/Register/Reset/Confirm im Browser durchgetestet (Show/Hide-Toggle + Placeholder + Fehler). `ensureNotRateLimited()/throttleKey()`-Trait bleibt zurückgestellt (login: 1 Key/5 Versuche/Lockout-Event/clear-on-success vs. magic-link: 2 Keys/3+15 Versuche/Stunden-Decay/kein Event — zu wenig gemeinsame Fläche). `<x-form-field>`/`<x-email-input>`/`<x-auth.divider-actions>` optional, geringer Hebel. - [x] **5.5 Flash/Auth-Components** — **Erledigt:** `<x-portal.alert variant=success|error>` (6 Flash-Blöcke: categories/create+edit, footer-codes/index, presets/index) und `<x-session-status>` (login, forgot-password, register, magic-link). **Bewusst bespoke:** companies/show (icon-loses Flash-Design + info-Variante), verify-email/settings-profile (eigene Status-Logik). **Erledigt (funktional gegengetestet 02.07.2026):** `<x-password-input>` über login/register/reset/confirm — Alpine-`show`-State in die Component verlagert, `:placeholder`-Prop, optionaler `labelAction`-Slot für den „Passwort vergessen?"-Link, `show-error`-Schalter für Bestätigungsfelder. Login/Register/Reset/Confirm im Browser durchgetestet (Show/Hide-Toggle + Placeholder + Fehler). `ensureNotRateLimited()/throttleKey()`-Trait bleibt zurückgestellt (login: 1 Key/5 Versuche/Lockout-Event/clear-on-success vs. magic-link: 2 Keys/3+15 Versuche/Stunden-Decay/kein Event — zu wenig gemeinsame Fläche). `<x-form-field>`/`<x-email-input>`/`<x-auth.divider-actions>` optional, geringer Hebel.
--- ---