# System-Optimierungs-Analyse (Pressekonto) > Erstellt von Claude Code am 2026-06-18. Read-only-Analyse — **es wurden keine Dateien geändert.** > Fokus: Code-Doppelungen, geteilter/wiederverwendbarer Code, Effizienz- und Best-Practice-Potenzial über Controller, Services, Models, Middleware, Livewire/Volt-Komponenten und Blade-Views. ## Inhaltsverzeichnis 1. [Management Summary](#1-management-summary) 2. [Top-Prioritäten (höchster Hebel)](#2-top-prioritäten-höchster-hebel) 3. [Backend: Controller, Services, Models, Middleware](#3-backend-controller-services-models-middleware) 4. [Livewire/Volt: Press-Release-Editoren](#4-livewirevolt-press-release-editoren) 5. [Livewire/Volt: Admin-CRUD, Customer, Settings, Auth](#5-livewirevolt-admin-crud-customer-settings-auth) 6. [Web-Frontend (gehört dem User, separat committen)](#6-web-frontend-gehört-dem-user-separat-committen) 7. [Vorgeschlagene neue Bausteine (Traits/Components/Services)](#7-vorgeschlagene-neue-bausteine) --- ## 1. Management Summary Das System ist **architektonisch bereits sauber** aufgesetzt: eine umfangreiche Service-Schicht (`app/Services/**`), Domain-/Theme-System, Concerns-Verzeichnis, geteilte Portal-Komponenten (`x-portal.stat-card`, `components.portal.pagination`) und ein `PressReleaseService` existieren. Die Probleme liegen nicht in der Struktur, sondern in **Duplikation an den Rändern**: - **Die größten Brocken sind die vier Press-Release-Editoren** (admin + customer, create + edit) mit zusammen ~5.500 Zeilen Blade/PHP, die zu ~80–95 % identische Validierung, Scheduling-Logik, Persistierung und Formular-Markup enthalten. - **Querschnittslogik ist mehrfach implementiert**: Firmenzugriff/Ownership (3–4×), Zahlungsoption-Eligibility (4×), VAT-Normalisierung (3×), Volltextsuche-Fallback (6×), Tabellen-Sortierung/Filter (3×). - **Diverging-Risk (Korrektheit!):** Mehrere dieser Duplikate sind bereits leicht auseinandergedriftet (z. B. Legacy-Invoice-Eligibility liefert bei `null` mal *deny*, mal *needs_review*). Das ist nicht nur Wartungslast, sondern eine latente Fehlerquelle. - **Domain/Theme/Portal-Auflösung** ist über 4 Stellen (Provider + 3 Middleware) verteilt und teils mit unterschiedlichen Theme-Vokabularen. - **Wenige echte Performance-/Security-Punkte**: synchrones API-Logging pro Request, Query-Sturm im Status-Widget, timing-unsicherer Basic-Auth-Vergleich, Race-Conditions bei Wallet-/Rechnungsnummern-Erstellung. Mehrheit der Findings ist **risikoarm** (reine Extraktion ohne Verhaltensänderung) bei hohem Zeilen-Einsparpotenzial. --- ## 2. Top-Prioritäten (höchster Hebel) | # | Maßnahme | Bereich | Severity | Aufwand | |---|----------|---------|----------|---------| | 1 | `InteractsWithPressReleaseScheduling`-Trait + Admin-Editoren nutzen bestehenden Tags-Trait | PR-Editoren | Hoch | Niedrig | | 2 | Persistierung + `formRules()` der PR-Editoren in `PressReleaseService` / geteilten Trait verlagern | PR-Editoren | Hoch | Mittel | | 3 | `Company::scopeAccessibleBy()` / `CompanyPolicy` — Firmenzugriff zentralisieren | Backend | Hoch | Mittel | | 4 | Eine `User::hasActivePaymentOption()` + `latestLegacyInvoice()` (Eligibility-Divergenz beheben) | Backend | Hoch (Korrektheit) | Mittel | | 5 | Domain/Theme/Portal-Auflösung in einen `DomainContext`-Service konsolidieren | Middleware | Hoch | Mittel | | 6 | Concerns: `HasSortableTable`, `HasFullTextSearch`, `HasResettableFilters` | Admin-CRUD | Hoch | Niedrig | | 7 | `` + `` + `` / `` | Admin/Auth | Hoch | Niedrig | | 8 | `Portal::badgeColor()` + `` / `` | Querschnitt | Hoch | Niedrig | | 9 | Synchrones `LogApiUsage` → terminable/queued; Status-Widget-Queries memoisieren | Performance | Hoch | Mittel | | 10 | `BasicAuthMiddleware` → `hash_equals()`; `BASIC_AUTH_ENABLED` aus `env()` in Config | Security | Hoch | Niedrig | --- ## 3. Backend: Controller, Services, Models, Middleware ### 3.1 N+1 / Query-Effizienz - **Status-Widget-Query-Sturm (Hoch)** — `app/Services/Customer/CustomerStatusSummary.php:35-66` feuert pro Request viele Einzelqueries (`currentPlan`, `subscription`, `exists()`, `hasActiveBooking()` mit 3 Sub-Queries, offene Single-Purchases). Da das Widget always-on im Header sitzt, summiert sich das. → Sub-Ergebnisse memoisieren/eager-loaden und durchreichen. - **Offene Single-Purchase 3× gezählt (Mittel)** — gleiche `COUNT`-Query in `CustomerStatusSummary.php:61`, `User.php:215`, `User.php:229`. → Einmal berechnen (wie `currentPlan()` memoisiert in `User.php:128-162`). - **`LegacySubscriptionService::cancelBundleFor` (Mittel)** — `app/Services/Billing/LegacySubscriptionService.php:45-54` macht ein `$post->update()` pro Iteration. → Bulk-`update()` oder `DB::transaction`. - **`selectedCompany()` doppelte Query (Mittel)** — `app/Services/Customer/CustomerCompanyContext.php:49` + `153-158`: `exists()` und `findFor()` führen dieselbe Query aus. → Einmal `first()`. - **`LogApiUsage` synchroner INSERT pro Request (Hoch)** — `app/Http/Middleware/LogApiUsage.php:35-46` schreibt bei jedem API-Response. → Terminable Middleware oder Queued Job. ### 3.2 Duplizierte Querschnittslogik - **Firmen-Ownership-Query 3–4× (Hoch)** — identische `owner_user_id == me OR whereHas('users', me)`-Closure in `Api/V1/PressReleaseController.php:182-191`, `CompanyController.php:18-21` & `:39-42`, `PressReleaseImageController.php:87`; überlappt zusätzlich `User::accessibleCompanyIds()` (`User.php:406-413`) und `CustomerCompanyContext::accessibleCompanyQuery()` (`:143-151`). → Ein `Company::scopeAccessibleBy()` **oder** `CompanyPolicy` (die in `ContactAccessService.php:229` schon referenziert wird). - **Zahlungsoption-Eligibility 4× (Hoch)** — gleicher Status-Set, leicht abweichende Periodenlogik in `Api/ApiAccessEligibilityService.php:28-46`, `CustomerStatusSummary.php:47-52`, `User::hasUnlimitedPressReleaseQuota()` (`User.php:191-196`), `User::hasActiveBooking()` (`User.php:256-261`). Variable `$hasGrandfathered` in `CustomerStatusSummary.php:47` matcht faktisch Active *oder* Grandfathered (Mislabel). → `UserPaymentOption`-Scope + eine `User::hasActivePaymentOption()`. - **Legacy-Invoice-Eligibility divergiert (Hoch — Korrektheit)** — `ApiAccessEligibilityService::hasPaidLegacyInvoice()` (`:48-56`) behandelt `null` als *deny*, `LegacyApiCustomerReporter::classification()` (`:131-142`) als *needs_review*. Beide re-implementieren „latest by invoice_date then id". → Ein `User::latestLegacyInvoice()` + eine Eligibility-Methode. - **VAT-ID-Normalisierung + EL/GR-Mapping 3× (Mittel)** — `VatResolver.php:30,56,63`, `VatIdValidationService.php:37,44,69`, `StripeCheckoutService.php:48,54,56`. → `VatResolver::normalize()` / `isEuCountry()` zentral; `StripeCheckoutService` konsumiert statt selbst herzuleiten. - **Case-insensitive Email-Lookup via `LOWER()` (Mittel)** — `whereRaw('LOWER(email) = ?')` in `ContactAccessService.php:50,130-148,176`, `SocialAuthService.php:32`; defeated den Email-Index. → Beim Schreiben normalisieren + `User::findByEmail()`-Scope. - **Lazy-Account-Erstellung doppelt (Mittel)** — `ContactAccessService::createAccount()` (`:188-208`) ≈ `SocialAuthService::createUser()` (`:65-82`). → gemeinsame Provisioning-Methode. - **Currency-Literal inkonsistent (Mittel)** — `'EUR'` in `CheckoutController.php:66`, `CreditTopupService.php:59`, `ManualInvoiceService.php:125` vs. `'eur'` in `ExtraPmPurchaseService.php:50`, `ProofPdfService.php:69`. → `config('billing.default_currency')`. ### 3.3 Fat Controller / Validierung / Model - **`Api/V1/PressReleaseController` (Mittel)** — `:201-220` re-implementiert Unique-Slug, obwohl `PressRelease` bereits `HasUniqueSlug` mit `slugScopeAttributes()` nutzt (`PressRelease.php:27,32`). Create/Update/Reclassify-Orchestrierung (`:44-122`) gehört in `PressReleaseService`. - **`LegalRequestController` (Mittel)** — Inline-Validation `:42-57` → `StoreLegalRequestRequest` (Projekt nutzt `app/Http/Requests/Api/V1/*` bereits). Portal-aus-Host `:103-122` dupliziert Portal-Detection; „max 1 open per PM+type"-Regel `:71-91` → `LegalRequestService`. - **`CheckoutController::singlePm` (Niedrig)** — baut `SinglePurchase` inline `:61-67` (hardcoded `'EUR'`) → Service-Methode analog `CreditTopupService::startTopup`. - **Fat `User` Model (Mittel)** — `app/Models/User.php` mischt Auth, Relations, Stripe-Adresse, Plan/Tier, Quota-Mathe, Booking-Gates. → Trait `HasBillingProfile`/`InteractsWithQuota` (`:86-262, 352-355`). ### 3.4 Authorization → Policies/Gates - **Firmenzugriff umgeht bestehende `CompanyPolicy` (Hoch)** — `CustomerCompanyContext.php:143-151,244-249` + 3 API-Controller (s. 3.2). → über `Gate`/Policy. - **Domain-Auth-Middleware → Gates (Mittel)** — `EnsureUserIsAdmin.php:28`, `EnsureUserIsCustomer.php:13`, `EnsureApiUserIsActive.php:18` rollen `$user?->method()`+abort von Hand; API-Active-Check dupliziert `is_active` aus `canAccessAdmin/Customer` (`User.php:379,392`). → `Gate::define('access-admin'…)` + `can:`-Middleware. - **Impersonation-Logik im Admin-Gate (Mittel)** — `EnsureUserIsAdmin.php:20-26` mischt Impersonation-Redirect in den Access-Check. → eigene Middleware. Zusätzlich totes `loadMissing('roles')` `:16-18` (Niedrig). ### 3.5 Middleware-Overlap (Domain/Theme/Portal/Edition) - **Vierfach duplizierte Host→Config-Auflösung (Hoch)** — `ThemeServiceProvider::boot` (`:29-88`), `ThemeMiddleware::handle` (`:16-56`), `SetDomainUrl::handle` (`:22-58`), `SetCurrentPortal::resolvePortal` (`:36-55`) lesen alle Host/`app.theme` und schreiben Config; `SetDomainUrl` ruft dieselben `URL::forceRootUrl/forceScheme/useAssetOrigin` erneut. → ein `DomainContext`-Service. *(Hinweis: `SetDomainUrl` scheint nicht in `bootstrap/app.php` registriert — vor Refactor prüfen, ob toter Code.)* - **Disjunkte Theme-Vokabulare (Hoch — Korrektheits-Smell)** — `ThemeMiddleware.php:22-53` kennt `b2in/b2a/stileigentum/style2own`, `SetCurrentPortal.php:50-54` nur `presseecho/businessportal24` (Rest → `null`-Portal). → Theme→Portal-Map an *einer* Stelle (`Portal`-Enum oder `config/domains.php`). - **`ThemeMiddleware` mutiert `REQUEST_URI` (Mittel)** — `:43-52` schreibt URIs um (fragil), `:33-38` re-implementiert `?theme=`-Override mit anderer Whitelist als der Provider. → Route-Prefixes (wie `SetEdition`) + ein Override-Pfad. - **`CurrentPortalContext` mutable static (Mittel)** — `app/Services/CurrentPortalContext.php:15` hält `private static ?Portal` pro Request (Octane-/Test-Risiko). → request-scoped Container-Binding. ### 3.6 Weitere Korrektheit/Security - **`BasicAuthMiddleware` (Hoch — Security)** — `:45` nutzt `!=` (timing-unsicher) → `hash_equals()`. `bootstrap/app.php:58` liest `env('BASIC_AUTH_ENABLED')` zur Laufzeit → unter `config:cache` `null` ⇒ Basic-Auth still deaktiviert. → Config-Key. - **`CreditWalletService::post` Race (Mittel)** — `:88-89` `firstOrCreate()` (unlocked) dann separates `lockForUpdate()->find()`. → Unique-Index auf `credit_wallets.user_id` + locked Upsert. - **`InvoiceNumberGenerator` Erstzeilen-Kollision (Niedrig/Mittel)** — `:41-63` `->first()` (kein Lock) dann `insert()`; zwei parallele Erstaufrufe ⇒ doppelte `MAN-00001`. → Unique-Index auf `circle` + Retry. - **`LegacyInvoicePdfController` Laufzeit-`Schema::hasColumn` (Niedrig)** — `:26` Schema-Introspektion pro Download. → nach Migration entfernen. - **`ExtraPmPurchaseService` magisches `* 100` (Niedrig)** — `:50` Credit→Cents außerhalb `CreditPricingService`. → in Pricing-Service. --- ## 4. Livewire/Volt: Press-Release-Editoren > Vier Dateien, ~5.500 Zeilen, ~80–95 % Überschneidung. Größter einzelner Optimierungs-Hebel im Projekt. > `admin/press-releases/{create,edit,index,show}.blade.php`, `customer/press-releases/{create,edit,index,show}.blade.php`. ### Hoch - **H1 — Scheduling-Maschinerie 4× wortgleich.** `updatedPublishMode/ScheduledDate/ScheduledTime()`, `syncScheduledAt()`, `resolveScheduledAt()`, `validateScheduledAtWhenReady()`, `updated()`, `notifyValidationError()` und der `scheduled*`-Block in `formRules()` sind identisch (admin/create:62-89,169-255; admin/edit:121-143,215-339; customer/create:123-145,216-302; customer/edit:163-185,253-339). → **`InteractsWithPressReleaseScheduling`-Trait** (spiegelt bestehendes `InteractsWithPressReleaseTags`). ~150 Zeilen × 3. - **H2 — `formRules()` ~95 % identisch** (admin/create:152-194; admin/edit:198-240; customer/create:197-241; customer/edit:237-278). Einziger Unterschied: admin `portal`+`companyId exists`, customer `companyId integer`. → `baseRules()` im Trait, Komponenten überschreiben nur `portal`/`companyId`. - **H3 — `presubmitChecks` 4× dupliziert, mit Drift.** 6-Punkt-Checkliste (admin/create:380-425; admin/edit:574-619; customer/create:583-632; customer/edit:561-608). `customer/create` nutzt `trans_choice(...)` + `selectedCompany()?->name`, die anderen drei `__(':n Zeichen')` + statischen Text — versehentliche Copy-Paste-Drift. → eine kanonische Version im Trait. - **H4 — Kontakt-Helfer dupliziert, divergente Query-Styles.** `defaultContactIdFor()`, `companyContacts()`, `companyContact()` in allen vier; admin nutzt `Contact::withoutGlobalScopes()`, customer `Contact::forCompany()`. `tagSuggestionsFor()` identisch. → `tagSuggestionsFor()` in Tags-Trait; Kontakt-Helfer in Trait/Scope. - **H5 — Admin-Editoren nutzen `InteractsWithPressReleaseTags` NICHT.** Customer (`use ...` in create:28, edit:29), Admin inlined `addTag/removeTag/tagsArray` + `tags`-Computed (admin/create:113-144,374-378,430-442; admin/edit:162-193,...). → `use InteractsWithPressReleaseTags;` + ~35 Zeilen je Datei löschen. ### Mittel - **M1 — Persistierung dupliziert; Customer zeigt das bessere Muster.** Customer zentralisiert in `persistDraft()`/`persistEdit()` + `pressReleaseAttributes()` (customer/create:521-543); Admin inlined volle `PressRelease::create([...])`/`update([...])` (admin/create:310-328; admin/edit:355-370). Slug + `HtmlSanitizer::clean()` + `contacts()->sync()` + `contentChanged`→reclassify/rescore (admin/edit:383-388; customer/edit:435-440,497-502) überall wiederholt. → `PressReleaseService::createDraft()` / `updateContent()`. - **M2 — Firmen-Combobox-Suche dupliziert, zwei Backends.** Admin baut Query inline in `with()` (admin/create:344-360, admin/edit:526-542), customer delegiert an `CustomerCompanyContext::searchCompaniesFor()`. `supportsFullTextSearch()` kopiert (admin/create:493-496, admin/edit:687-690, admin/index:384-388). → `Company`-Scope `searchByTerm()`. - **M3 — Formular-Markup ~85 % identisch** (Firma-Selektor, Titel mit Char-Meter `@php`, Untertitel, Fließtext-Meter, Boilerplate, Tag-Input-Chips, Veröffentlichungs-Panel, Pressekontakt, Pre-Submit-Check). → Blade-Components: ``, ``, ``, ``, ``, ``. **Größte Roh-Zeilenersparnis.** - **M4 — Listenzeilen admin vs. customer dupliziert** (admin/index:836-968 ≈ customer/index:421-531). Filter-Chips mit inline-`` ~9× in admin/index:662-801, 3× customer/index:320-376. → `` + ``. - **M5 — Show-Seiten duplizieren Status-Timeline + Rejection-Banner** (admin/show:223-251,431-472 ≈ customer/show:345-370,738-771). `statusColor`/`$statusClass`-`match` an 4+ Stellen. → Mappings auf `PressReleaseStatus`-Enum (`badgeClass()`/`color()`) + ``, ``. - **M6 — Status-Aktionen dupliziert** (`publish/reject/archive` in admin/index:163-215, admin/edit:446-486, admin/show:25-68). `withoutGlobalScopes()->findOrFail($id)` ~10× in Admin-Code. → `HandlesAdminPressReleaseActions`-Trait + `findAdminPR()`. ### Niedrig - **L1** — `categoryOptions()` admin gecacht (`AdminPerformanceCache`), customer uncached pro Render (customer/create:557-561, customer/edit:531-535). → gemeinsamer Helfer + Cache. - **L2** — Kategorie-Anzeigename `->translations->firstWhere('locale','de')?->name ?? ...` ~10× → `Category::localizedName()`. - **L3** — Kontakt-Name `trim(first.' '.last) ?: ...` mehrfach → `Contact::displayName()`. - **L4** — `statusColor` aus `with()` (admin/edit:558-564, admin/show:111-117) scheinbar ungenutzt (Blade nutzt lokales `$statusClass`). → prüfen/entfernen. - **L5** — Image-/Attachment-Manager teilen `getPressRelease()`/`canChange*()` (images-manager:257-269, attachments-manager:214-239). Attachments aktuell deaktiviert → bei Reaktivierung `ManagesPressReleaseMedia`-Trait; `canChange*` gehört in `PressReleasePolicy`. --- ## 5. Livewire/Volt: Admin-CRUD, Customer, Settings, Auth ### Tabellen-Plumbing → Concerns (Hoch) - **`sort($column)` wortgleich** in `admin/users:39-48`, `companies/index:45-54`, `contacts/index:51-60`. → `HasSortableTable` (`$sortBy`/`$sortDir`/`sort()` + Whitelist-Helfer). - **`supportsFullTextSearch()` 6×+ byte-gleich** (users/edit:627-631, users/create:216-220, companies/index:169-173, contacts/index:262-266, contacts/edit:143-147, companies/show:178-182) + „whereFullText else multi-LIKE"-Block mehrfach. → `HasFullTextSearch` + `applyFuzzySearch($query,$columns,$term)`. - **`updatedXxxFilter()` nur `resetPage()`** (users:285-313 = 6 Methoden, companies/index:175-227, contacts/index:62-90). → `HasResettableFilters` + `filterProperties()`-Array. - **`stats()` mit `SUM(CASE WHEN is_active...)`** strukturidentisch (users:187-203, companies/index:122-141). → `AdminPerformanceCache::activeInactiveStats()`. - **Count-Hydration** (users:205-218 `hydrateCompanyCounts`, companies/index:143-167 `hydrateCounts`). → `hydrateRelationCounts($paginator, $map)`. ### Create vs. Edit Duplikation (Hoch) - **User create/edit teilen Rule-Sets + Billing/Company-Logik** (create:110-130 ⊂ edit:377-421; `billingIsComplete()` create:222-229 = edit:708-715; `availableRoles()` create:209-214 = edit:620-625). `save()` trägt ~175 Zeilen Business-Logik (edit:375-550). → `UserFormRules`-Provider + `UserUpsertService`/`SaveUserAction`. - **Billing-/Company-/Role-Markup dupliziert** (Rechnungsadresse create:386-436 ≈ edit:1177-1228; Company-Combobox create:322-376 ≈ edit:1003-1069; Rollen create:261-313 ≈ edit:842-995). → ``, ``, ``. - **Kontakt-Validierung/Markup** (users/edit:406-412, contacts/edit, customer/profile:147-168, press-kits/show:169-175). → `ValidatesContactData`-Concern + `` (admin+customer geteilt). ### Formatierungs-Helfer → Components/Enum (Hoch/Mittel) - **`portalBadgeColor(?Portal)` 3–4×** (companies/index:229-237, companies/show:184-192, contacts/index:383-391, contacts/edit:170-178). → `Portal::badgeColor()` + ``. - **Status-Badge (ok/err-dot)** (companies/index:519-524, users:558-564, customer press-kits). → ``. - **`pressReleaseStatusColor()`** (users:161-170) → auf `PressReleaseStatus`-Enum. - **`formatEuro()`** (customer/bookings:36-39) → `` / Helfer. - **Datum `?->format('d.m.Y H:i') ?? '–'`** überall → ``. ### Seiten-Struktur → Components (Mittel) - **Page-Header-Block** (users:318-337, users/create:234-253, users/edit:720-746, companies/index:301-320, contacts/index:395-421, payments/index:158-180, companies/show:215-268, companies/edit:199-220). → `` mit Action-Slot. - **KPI-Grid-Wrapper** (users:340-353, companies/index:323-335). → ``. - **Filter-Panel + flux:table + Pagination-Footer** (users:599-601, companies/index:588-590, payments 4×). → `` / ``. - **Empty-State-Zelle** (users:582-594, companies/index:569-584). → ``. - **Combobox-Pattern** (`flux:select variant=combobox` + Clear-Button: users/create:322-352, users/edit:1003-1043,1074-1121, companies/index:369-445, companies/show:399-432, contacts/edit:211-243). → ``. ### Flash/Notification (Hoch) - **Auto-Dismiss-Toast** (`x-data="{show:true}" x-init=setTimeout`) in users/edit:748-754, contacts/index:423-437, payments/index:182-196; Session-Varianten companies/show:196-213. - **`@if(session('status'))`** in auth/login:128-132, register:57-61, forgot-password:30-34, verify-email:33-37, magic-link:87-91; settings nutzen ``. → ein `` + `` app-weit. ### Auth & Settings (Hoch/Mittel) - **Password show/hide-Feld** (auth/login:134-180, register:63-143, confirm-password:40-72, reset-password:73-135) → `` (Hoch). - **Email-Input + `@error`-Markup** in allen Auth-Forms → ``/``. - **Rate-Limiting inline & divergent** (auth/login:100-124, magic-link:54-83) → `ensureNotRateLimited()`/`throttleKey()`-Trait. - **Heavy Auth-Logik in Volt** (2FA/Activation/Redirect: auth/login:28-95, settings/profile:19-73) → Fortify-Actions/Response-Contracts (architektonisch). - **Divider + Secondary-Action** (auth/login:196-218 ≈ register:174-192) → ``. ### Customer-Bereich (Mittel/Niedrig) - Portal-Pill/Status-Badge wiederholt (press-kits/index:669-685,778-786, dashboard:352-358, press-kits/show:298-307) → dieselben ``/``. - Company-Logo-Logik (`logoVariant/logoInitials/logoUrl`, press-kits/index:221-259,645-650,760-766) → ``. - Inline-Edit-Form-Toggles (`showCompanyForm`/`showContactForm`) in customer/profile, press-kits/show → `HasInlineFormModals`-Concern. - Onboarding-Step-Grid (dashboard:382-398, press-kits/index:611-630) → ``. > **Bereits gute Wiederverwendung:** `` und `components.portal.pagination` werden konsistent genutzt — als Muster für die neuen Components übernehmen. --- ## 6. Web-Frontend (gehört dem User, separat committen) > **Hinweis:** `resources/views/web/**` und `resources/views/components/web/**` gehören laut Projekt-Memory dem User und werden separat committet. Findings sind beratend; nicht in Feature-Commits bündeln. *(Vor jedem Löschen vom User bestätigen lassen — „unused"-Zählungen stammen aus `grep` über `x-web.` und prüfen keine dynamische Komponenten-Auflösung.)* ### Hoch - **Pricing-Cards 3× handcodiert** in `web/pressekonto.blade.php:401-466,469-535,538-~610` — fast identische ~67-Zeilen-`
`s, derselbe Check/Dash/Cross-SVG ~15×. **Es existiert bereits ein datengetriebenes ``** (`components/web/pricing-teaser.blade.php`, iteriert `Plan::active()`), das diese Seite ignoriert. → `pricing-teaser` einsetzen; ~200 Zeilen → eine Schleife. - **`press-release-card` vs. `presseecho-release-card` ~95 % identisch** (je ~160 Zeilen; nur Default-`$contentTypeClasses`, Gradient-Klassen, Badge unterscheiden sich). `presseecho-release-card` hat **0 Verwendungen**. → vereinheitlichen auf eine Komponente mit `variant`-Prop oder löschen. ### Mittel - **Industrie-Icon-Map 3× dupliziert** (press-release-card:15-31, presseecho-release-card:15-31, highlight-card:5-22). → in `CategoryService` (zentralisiert bereits `getIconPath()`) oder Config. - **Release-Normalisierung 3×** (feed-item:8-50, feed-top-item:6-36, focus-side-item:8-33): gleiche Kategorie-/Firma-/Datum-/Bild-`@php`-Blöcke; „verified"-SVG verbatim. → View-Model `ReleaseCardData::from($release)` + ``. - **Footer standard vs. hub** (site-footer:1-82 ≈ hub/site-footer:1-82) → ein `` mit `columns`+`variant`. - **Top-Utility-Bar** inline in site-header:47-93 vs. komponentisiert in hub/top-utility-bar:1-55 → gemeinsame ``. - **Tote, array-getriebene Legacy-Cards** (0 Produktiv-Verwendung, nur `web/examples/`): `presseecho-release-card`, `press-release-list-item`, `dossier-card`, `article-card`, `highlight-card`+`highlights-slider`, `main-navigation`. → nach Bestätigung mitsamt `web/examples/` entfernen. - **Inline-SVGs** (CTA-Pfeil `M2 6h8…` in 5 Dateien, Building-Icon, verified-Check) → kleines `components/web/icons/`-Set. - **Inline-Array-`@foreach`-Sektionen** in `veroeffentlichen.blade.php:196-211,227-249,265-280,293-310,383-405` → `` + statische Arrays in View-Data. ### Niedrig - Layout-Shell `max-w-layout mx-auto px-8` ~20× in pressekonto.blade.php → `` oder `.layout-container`-Utility (wo `.card`/`.badge` schon liegen). - Such-Form-Block doppelt in site-header (Desktop :110-130, Mobile :196-226) → ``. - Sidebar-Familie (`sidebar-widget` + `-categories/-companies/-newsletter/-rss`) ist **gut faktorisiert**, aktuell aber 0 Produktiv-Verwendung — vor Behalten prüfen. --- ## 7. Vorgeschlagene neue Bausteine **Traits/Concerns** (`app/Livewire/Concerns/`, `app/Models/Concerns/`) - `InteractsWithPressReleaseScheduling`, `HandlesAdminPressReleaseActions`, `ManagesPressReleaseMedia` - `HasSortableTable`, `HasFullTextSearch`, `HasResettableFilters`, `ValidatesContactData`, `HasInlineFormModals` - `User`: `HasBillingProfile` / `InteractsWithQuota` **Services** (`app/Services/**`) - `PressReleaseService::createDraft()` / `updateContent()` (Persistierung + reclassify/rescore) - `UserUpsertService`, `LegalRequestService` - `DomainContext` (konsolidiert Theme/Portal/Domain-Auflösung) - Zentralisierung in bestehenden Services: `VatResolver::normalize/isEuCountry`, `User::hasActivePaymentOption/latestLegacyInvoice/findByEmail` **Query-Scopes / Enum-Methoden** - `Company::scopeAccessibleBy()`, `Company::scopeSearchByTerm()`, `Contact::scopeForCompany()` (vereinheitlicht), `UserPaymentOption`-Active-Scope - `Portal::badgeColor()`, `PressReleaseStatus::badgeClass()/color()`, `Category::localizedName()`, `Contact::displayName()` **Policies/Gates** - `CompanyPolicy` durchgängig nutzen; `Gate::define('access-admin'|'access-customer')` + `can:`-Middleware **Blade-Components** - Portal: `pr-char-meter`, `pr-tag-input`, `pr-presubmit-check`, `pr-publish-schedule`, `pr-contact-select`, `pr-boilerplate-panel`, `pr-table-row`, `pr-status-timeline`, `pr-rejection-banner`, `filter-chip`, `alert`, `page-header`, `stat-cards`, `table-panel`, `table-pagination`, `searchable-select`, `empty-state`, `portal-badge`, `status-badge`, `money`, `datetime`, `company-logo`, `step-guide` - Admin: `billing-fields`, `company-link-picker`, `role-checkboxes` - Auth/Shared: `password-input`, `form-field`, `session-status`, `contact-fields`, `auth.divider-actions` --- ### Empfohlenes Vorgehen 1. **Risikoarme Extraktionen zuerst** (Traits H1/H5, Concerns 6, Components 7/8 oben) — reine Deduplikation ohne Verhaltensänderung, jeweils mit bestehenden Pest-Tests absichern. 2. **Korrektheits-Divergenzen** (Eligibility 3.2, Theme-Vokabular 3.5, Security 3.6) — vor Refactor jeweils einen Test schreiben, der das aktuelle Soll-Verhalten festschreibt. 3. **Persistierung in Services** (M1, UserUpsertService) — mittlerer Aufwand, größter architektonischer Gewinn. 4. **Web-Frontend** separat mit dem User abstimmen.