Das System ist seit 20.07.2026 live; die dev-Doku spiegelt jetzt den Betriebszustand statt der Release-Vorbereitung: - dev/betrieb/: Deploy-Runbook (neu: §10 Betrieb nach Go-Live mit SFTP-Deploy-Checkliste inkl. queue:restart-Pflicht und den Betriebs-Erkenntnissen der Go-Live-Woche), Legacy-API-Parität, Umami. - dev/roadmap.md: offene Nachlauf-Punkte (Go-Live-Mails, Merge nach main, Stripe-Test-Artefakte, Key-Rotation, Alt-System-Abschaltung) und Ideen-Backlog. - dev/archive/: abgeschlossene Phasen — Migration-2026-Planung, Cutover-Runbook (mit Erledigt-Vermerk), Release-Plan/QA-Checkliste, Optimierungsphasen, Frontend-Konzepte + Responsive-Audits vom 16.07., Altsystem-Reste. - dev/README.md als Wegweiser; gitignore-Pfad der SQL-Dumps mitgezogen (dev/archive/migration-2026/sql/ bleibt untracked, ebenso dev/migration/ und dev/api/). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
27 KiB
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
- Management Summary
- Top-Prioritäten (höchster Hebel)
- Backend: Controller, Services, Models, Middleware
- Livewire/Volt: Press-Release-Editoren
- Livewire/Volt: Admin-CRUD, Customer, Settings, Auth
- Web-Frontend (gehört dem User, separat committen)
- Vorgeschlagene neue Bausteine (Traits/Components/Services)
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
nullmal 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 | <x-portal.alert> + <x-session-status> + <x-password-input> / <x-form-field> |
Admin/Auth | Hoch | Niedrig |
| 8 | Portal::badgeColor() + <x-portal.portal-badge> / <x-portal.status-badge> |
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-66feuert 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 inCustomerStatusSummary.php:61,User.php:215,User.php:229. → Einmal berechnen (wiecurrentPlan()memoisiert inUser.php:128-162). LegacySubscriptionService::cancelBundleFor(Mittel) —app/Services/Billing/LegacySubscriptionService.php:45-54macht ein$post->update()pro Iteration. → Bulk-update()oderDB::transaction.selectedCompany()doppelte Query (Mittel) —app/Services/Customer/CustomerCompanyContext.php:49+153-158:exists()undfindFor()führen dieselbe Query aus. → Einmalfirst().LogApiUsagesynchroner INSERT pro Request (Hoch) —app/Http/Middleware/LogApiUsage.php:35-46schreibt 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 inApi/V1/PressReleaseController.php:182-191,CompanyController.php:18-21&:39-42,PressReleaseImageController.php:87; überlappt zusätzlichUser::accessibleCompanyIds()(User.php:406-413) undCustomerCompanyContext::accessibleCompanyQuery()(:143-151). → EinCompany::scopeAccessibleBy()oderCompanyPolicy(die inContactAccessService.php:229schon 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$hasGrandfatheredinCustomerStatusSummary.php:47matcht faktisch Active oder Grandfathered (Mislabel). →UserPaymentOption-Scope + eineUser::hasActivePaymentOption(). - Legacy-Invoice-Eligibility divergiert (Hoch — Korrektheit) —
ApiAccessEligibilityService::hasPaidLegacyInvoice()(:48-56) behandeltnullals deny,LegacyApiCustomerReporter::classification()(:131-142) als needs_review. Beide re-implementieren „latest by invoice_date then id". → EinUser::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;StripeCheckoutServicekonsumiert statt selbst herzuleiten. - Case-insensitive Email-Lookup via
LOWER()(Mittel) —whereRaw('LOWER(email) = ?')inContactAccessService.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'inCheckoutController.php:66,CreditTopupService.php:59,ManualInvoiceService.php:125vs.'eur'inExtraPmPurchaseService.php:50,ProofPdfService.php:69. →config('billing.default_currency').
3.3 Fat Controller / Validierung / Model
Api/V1/PressReleaseController(Mittel) —:201-220re-implementiert Unique-Slug, obwohlPressReleasebereitsHasUniqueSlugmitslugScopeAttributes()nutzt (PressRelease.php:27,32). Create/Update/Reclassify-Orchestrierung (:44-122) gehört inPressReleaseService.LegalRequestController(Mittel) — Inline-Validation:42-57→StoreLegalRequestRequest(Projekt nutztapp/Http/Requests/Api/V1/*bereits). Portal-aus-Host:103-122dupliziert Portal-Detection; „max 1 open per PM+type"-Regel:71-91→LegalRequestService.CheckoutController::singlePm(Niedrig) — bautSinglePurchaseinline:61-67(hardcoded'EUR') → Service-Methode analogCreditTopupService::startTopup.- Fat
UserModel (Mittel) —app/Models/User.phpmischt Auth, Relations, Stripe-Adresse, Plan/Tier, Quota-Mathe, Booking-Gates. → TraitHasBillingProfile/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). → überGate/Policy. - Domain-Auth-Middleware → Gates (Mittel) —
EnsureUserIsAdmin.php:28,EnsureUserIsCustomer.php:13,EnsureApiUserIsActive.php:18rollen$user?->method()+abort von Hand; API-Active-Check dupliziertis_activeauscanAccessAdmin/Customer(User.php:379,392). →Gate::define('access-admin'…)+can:-Middleware. - Impersonation-Logik im Admin-Gate (Mittel) —
EnsureUserIsAdmin.php:20-26mischt Impersonation-Redirect in den Access-Check. → eigene Middleware. Zusätzlich totesloadMissing('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.themeund schreiben Config;SetDomainUrlruft dieselbenURL::forceRootUrl/forceScheme/useAssetOriginerneut. → einDomainContext-Service. (Hinweis:SetDomainUrlscheint nicht inbootstrap/app.phpregistriert — vor Refactor prüfen, ob toter Code.) - Disjunkte Theme-Vokabulare (Hoch — Korrektheits-Smell) —
ThemeMiddleware.php:22-53kenntb2in/b2a/stileigentum/style2own,SetCurrentPortal.php:50-54nurpresseecho/businessportal24(Rest →null-Portal). → Theme→Portal-Map an einer Stelle (Portal-Enum oderconfig/domains.php). ThemeMiddlewaremutiertREQUEST_URI(Mittel) —:43-52schreibt URIs um (fragil),:33-38re-implementiert?theme=-Override mit anderer Whitelist als der Provider. → Route-Prefixes (wieSetEdition) + ein Override-Pfad.CurrentPortalContextmutable static (Mittel) —app/Services/CurrentPortalContext.php:15hältprivate static ?Portalpro Request (Octane-/Test-Risiko). → request-scoped Container-Binding.
3.6 Weitere Korrektheit/Security
BasicAuthMiddleware(Hoch — Security) —:45nutzt!=(timing-unsicher) →hash_equals().bootstrap/app.php:58liestenv('BASIC_AUTH_ENABLED')zur Laufzeit → unterconfig:cachenull⇒ Basic-Auth still deaktiviert. → Config-Key.CreditWalletService::postRace (Mittel) —:88-89firstOrCreate()(unlocked) dann separateslockForUpdate()->find(). → Unique-Index aufcredit_wallets.user_id+ locked Upsert.InvoiceNumberGeneratorErstzeilen-Kollision (Niedrig/Mittel) —:41-63->first()(kein Lock) danninsert(); zwei parallele Erstaufrufe ⇒ doppelteMAN-00001. → Unique-Index aufcircle+ Retry.LegacyInvoicePdfControllerLaufzeit-Schema::hasColumn(Niedrig) —:26Schema-Introspektion pro Download. → nach Migration entfernen.ExtraPmPurchaseServicemagisches* 100(Niedrig) —:50Credit→Cents außerhalbCreditPricingService. → 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 derscheduled*-Block informRules()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 bestehendesInteractsWithPressReleaseTags). ~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: adminportal+companyId exists, customercompanyId integer. →baseRules()im Trait, Komponenten überschreiben nurportal/companyId. - H3 —
presubmitChecks4× dupliziert, mit Drift. 6-Punkt-Checkliste (admin/create:380-425; admin/edit:574-619; customer/create:583-632; customer/edit:561-608).customer/createnutzttrans_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 nutztContact::withoutGlobalScopes(), customerContact::forCompany().tagSuggestionsFor()identisch. →tagSuggestionsFor()in Tags-Trait; Kontakt-Helfer in Trait/Scope. - H5 — Admin-Editoren nutzen
InteractsWithPressReleaseTagsNICHT. Customer (use ...in create:28, edit:29), Admin inlinedaddTag/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 vollePressRelease::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 anCustomerCompanyContext::searchCompaniesFor().supportsFullTextSearch()kopiert (admin/create:493-496, admin/edit:687-690, admin/index:384-388). →Company-ScopesearchByTerm(). - 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:<x-portal.pr-char-meter>,<x-portal.pr-tag-input>,<x-portal.pr-presubmit-check>,<x-portal.pr-publish-schedule>,<x-portal.pr-contact-select>,<x-portal.pr-boilerplate-panel>. Größte Roh-Zeilenersparnis. - M4 — Listenzeilen admin vs. customer dupliziert (admin/index:836-968 ≈ customer/index:421-531). Filter-Chips mit inline-
<svg>~9× in admin/index:662-801, 3× customer/index:320-376. →<x-portal.pr-table-row>+<x-portal.filter-chip>. - M5 — Show-Seiten duplizieren Status-Timeline + Rejection-Banner (admin/show:223-251,431-472 ≈ customer/show:345-370,738-771).
statusColor/$statusClass-matchan 4+ Stellen. → Mappings aufPressReleaseStatus-Enum (badgeClass()/color()) +<x-portal.pr-status-timeline>,<x-portal.pr-rejection-banner>. - M6 — Status-Aktionen dupliziert (
publish/reject/archivein 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 —
statusColorauswith()(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 ReaktivierungManagesPressReleaseMedia-Trait;canChange*gehört inPressReleasePolicy.
5. Livewire/Volt: Admin-CRUD, Customer, Settings, Auth
Tabellen-Plumbing → Concerns (Hoch)
sort($column)wortgleich inadmin/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()nurresetPage()(users:285-313 = 6 Methoden, companies/index:175-227, contacts/index:62-90). →HasResettableFilters+filterProperties()-Array.stats()mitSUM(CASE WHEN is_active...)strukturidentisch (users:187-203, companies/index:122-141). →AdminPerformanceCache::activeInactiveStats().- Count-Hydration (users:205-218
hydrateCompanyCounts, companies/index:143-167hydrateCounts). →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). →
<x-admin.billing-fields>,<x-admin.company-link-picker>,<x-admin.role-checkboxes>. - Kontakt-Validierung/Markup (users/edit:406-412, contacts/edit, customer/profile:147-168, press-kits/show:169-175). →
ValidatesContactData-Concern +<x-contact-fields>(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()+<x-portal.portal-badge>.- Status-Badge (ok/err-dot) (companies/index:519-524, users:558-564, customer press-kits). →
<x-portal.status-badge :active>. pressReleaseStatusColor()(users:161-170) → aufPressReleaseStatus-Enum.formatEuro()(customer/bookings:36-39) →<x-portal.money>/ Helfer.- Datum
?->format('d.m.Y H:i') ?? '–'überall →<x-portal.datetime :value>.
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). →
<x-portal.page-header :eyebrow :title :subtitle>mit Action-Slot. - KPI-Grid-Wrapper (users:340-353, companies/index:323-335). →
<x-portal.stat-cards :stats>. - Filter-Panel + flux:table + Pagination-Footer (users:599-601, companies/index:588-590, payments 4×). →
<x-portal.table-panel>/<x-portal.table-pagination :paginator>. - Empty-State-Zelle (users:582-594, companies/index:569-584). →
<x-portal.empty-state>. - 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). →<x-portal.searchable-select>.
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<x-action-message>. → ein<x-portal.alert :type :dismissible>+<x-session-status>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) →
<x-password-input>(Hoch). - Email-Input +
@error-Markup in allen Auth-Forms →<x-form-field>/<x-email-input>. - 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) →
<x-auth.divider-actions>.
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
<x-portal.portal-badge>/<x-portal.status-badge>. - Company-Logo-Logik (
logoVariant/logoInitials/logoUrl, press-kits/index:221-259,645-650,760-766) →<x-portal.company-logo :company>. - 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) →
<x-portal.step-guide :steps>.
Bereits gute Wiederverwendung:
<x-portal.stat-card>undcomponents.portal.paginationwerden konsistent genutzt — als Muster für die neuen Components übernehmen.
6. Web-Frontend (gehört dem User, separat committen)
Hinweis:
resources/views/web/**undresources/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 ausgrepüberx-web.<name>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-<article>s, derselbe Check/Dash/Cross-SVG ~15×. Es existiert bereits ein datengetriebenes<x-web.pricing-teaser>(components/web/pricing-teaser.blade.php, iteriertPlan::active()), das diese Seite ignoriert. →pricing-teasereinsetzen; ~200 Zeilen → eine Schleife. press-release-cardvs.presseecho-release-card~95 % identisch (je ~160 Zeilen; nur Default-$contentTypeClasses, Gradient-Klassen, Badge unterscheiden sich).presseecho-release-cardhat 0 Verwendungen. → vereinheitlichen auf eine Komponente mitvariant-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 bereitsgetIconPath()) 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-ModelReleaseCardData::from($release)+<x-web.icons.verified-badge>. - Footer standard vs. hub (site-footer:1-82 ≈ hub/site-footer:1-82) → ein
<x-web.site-footer>mitcolumns+variant. - Top-Utility-Bar inline in site-header:47-93 vs. komponentisiert in hub/top-utility-bar:1-55 → gemeinsame
<x-web.top-utility-bar>. - 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 mitsamtweb/examples/entfernen. - Inline-SVGs (CTA-Pfeil
M2 6h8…in 5 Dateien, Building-Icon, verified-Check) → kleinescomponents/web/icons/-Set. - Inline-Array-
@foreach-Sektionen inveroeffentlichen.blade.php:196-211,227-249,265-280,293-310,383-405→<x-web.criteria-item>+ statische Arrays in View-Data.
Niedrig
- Layout-Shell
max-w-layout mx-auto px-8~20× in pressekonto.blade.php →<x-web.container>oder.layout-container-Utility (wo.card/.badgeschon liegen). - Such-Form-Block doppelt in site-header (Desktop :110-130, Mobile :196-226) →
<x-web.search-form :variant>. - 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,ManagesPressReleaseMediaHasSortableTable,HasFullTextSearch,HasResettableFilters,ValidatesContactData,HasInlineFormModalsUser:HasBillingProfile/InteractsWithQuota
Services (app/Services/**)
PressReleaseService::createDraft()/updateContent()(Persistierung + reclassify/rescore)UserUpsertService,LegalRequestServiceDomainContext(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-ScopePortal::badgeColor(),PressReleaseStatus::badgeClass()/color(),Category::localizedName(),Contact::displayName()
Policies/Gates
CompanyPolicydurchgä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
- Risikoarme Extraktionen zuerst (Traits H1/H5, Concerns 6, Components 7/8 oben) — reine Deduplikation ohne Verhaltensänderung, jeweils mit bestehenden Pest-Tests absichern.
- Korrektheits-Divergenzen (Eligibility 3.2, Theme-Vokabular 3.5, Security 3.6) — vor Refactor jeweils einen Test schreiben, der das aktuelle Soll-Verhalten festschreibt.
- Persistierung in Services (M1, UserUpsertService) — mittlerer Aufwand, größter architektonischer Gewinn.
- Web-Frontend separat mit dem User abstimmen.