From 1c5eedec7b7b8186d42b302f983b1c45472d51e9 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Thu, 18 Jun 2026 13:33:59 +0000 Subject: [PATCH] WS-8: doppelte Logik konsolidieren (Tags, Kontakt-Query, Billing) - Themen-Tag-Verwaltung (addTag/removeTag/tagsArray) war in create- und edit-Editor byte-identisch dupliziert -> Trait App\Livewire\Concerns\InteractsWithPressReleaseTags. - Kontaktauswahl je Firma (withoutGlobalScopes + where + Sortierung) aus create/edit/dashboard in Contact::scopeForCompany() zentralisiert. - Rechnungsadress-Pflichtfelder als BillingAddress::REQUIRED_FIELDS; isComplete() und neues completenessPercent() teilen die Liste, Dashboard nutzt es statt eigener Feldliste. Co-Authored-By: Claude Opus 4.8 --- .../InteractsWithPressReleaseTags.php | 68 +++++++++++++++++++ app/Models/BillingAddress.php | 24 +++++-- app/Models/Contact.php | 14 ++++ .../livewire/customer/dashboard.blade.php | 13 +--- .../customer/press-releases/create.blade.php | 65 ++---------------- .../customer/press-releases/edit.blade.php | 65 ++---------------- 6 files changed, 115 insertions(+), 134 deletions(-) create mode 100644 app/Livewire/Concerns/InteractsWithPressReleaseTags.php diff --git a/app/Livewire/Concerns/InteractsWithPressReleaseTags.php b/app/Livewire/Concerns/InteractsWithPressReleaseTags.php new file mode 100644 index 00000000..f7262221 --- /dev/null +++ b/app/Livewire/Concerns/InteractsWithPressReleaseTags.php @@ -0,0 +1,68 @@ +tagsArray(); + + if (count($existing) >= 5) { + return; + } + + if (in_array($tag, $existing, true)) { + return; + } + + $existing[] = $tag; + $this->keywords = implode(', ', $existing); + + unset($this->tags, $this->presubmitChecks); + } + + public function removeTag(string $tag): void + { + $existing = array_values(array_filter( + $this->tagsArray(), + fn (string $existingTag): bool => $existingTag !== $tag, + )); + + $this->keywords = implode(', ', $existing); + + unset($this->tags, $this->presubmitChecks); + } + + /** + * @return list + */ + private function tagsArray(): array + { + if (trim($this->keywords) === '') { + return []; + } + + return collect(explode(',', $this->keywords)) + ->map(fn (string $tag): string => trim($tag)) + ->filter() + ->unique() + ->values() + ->all(); + } +} diff --git a/app/Models/BillingAddress.php b/app/Models/BillingAddress.php index 5d16f2a3..a64627ec 100644 --- a/app/Models/BillingAddress.php +++ b/app/Models/BillingAddress.php @@ -34,13 +34,27 @@ class BillingAddress extends Model /** * Ist die Adresse vollständig genug für eine Rechnung bzw. Buchung? * Maßgeblich: Empfänger (Name), Straße, PLZ, Ort und Land. + * + * @var list */ + public const REQUIRED_FIELDS = ['name', 'address1', 'postal_code', 'city', 'country_code']; + public function isComplete(): bool { - return filled($this->name) - && filled($this->address1) - && filled($this->postal_code) - && filled($this->city) - && filled($this->country_code); + return collect(self::REQUIRED_FIELDS) + ->every(fn (string $field): bool => filled($this->{$field})); + } + + /** + * Anteil der ausgefüllten Pflichtfelder in Prozent (0–100) — für die + * Fortschritts-Anzeige im Dashboard. + */ + public function completenessPercent(): int + { + $filled = collect(self::REQUIRED_FIELDS) + ->filter(fn (string $field): bool => filled($this->{$field})) + ->count(); + + return (int) round(($filled / count(self::REQUIRED_FIELDS)) * 100); } } diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 31ea7d07..a4294034 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Enums\Portal; use App\Scopes\PortalScope; use Database\Factories\ContactFactory; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -21,6 +22,19 @@ class Contact extends Model static::addGlobalScope(new PortalScope); } + /** + * Kontakte einer Firma, portal-unabhängig (ohne GlobalScope) und in der + * fürs Kunden-Panel üblichen Sortierung. Single Source of Truth für die + * Kontaktauswahl in Erstellen/Bearbeiten und für Kontakt-Zählungen. + */ + public function scopeForCompany(Builder $query, int $companyId): Builder + { + return $query->withoutGlobalScopes() + ->where('company_id', $companyId) + ->orderBy('last_name') + ->orderBy('first_name'); + } + protected $fillable = [ 'company_id', 'portal', diff --git a/resources/views/livewire/customer/dashboard.blade.php b/resources/views/livewire/customer/dashboard.blade.php index 7179790f..7c9b7955 100644 --- a/resources/views/livewire/customer/dashboard.blade.php +++ b/resources/views/livewire/customer/dashboard.blade.php @@ -117,14 +117,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C */ private function billingCompleteness(?BillingAddress $address): int { - if (! $address) { - return 0; - } - - $fields = ['name', 'address1', 'postal_code', 'city', 'country_code']; - $filled = collect($fields)->filter(fn (string $field) => filled($address->{$field}))->count(); - - return (int) round(($filled / count($fields)) * 100); + return $address?->completenessPercent() ?? 0; } /** @@ -166,9 +159,7 @@ new #[Layout('components.layouts.app'), Title('Mein Dashboard')] class extends C } if ($selectedCompany) { - $contactsCount = Contact::withoutGlobalScopes() - ->where('company_id', $selectedCompany->id) - ->count(); + $contactsCount = Contact::forCompany($selectedCompany->id)->count(); if ($contactsCount === 0) { $hints[] = [ diff --git a/resources/views/livewire/customer/press-releases/create.blade.php b/resources/views/livewire/customer/press-releases/create.blade.php index 3090a40c..a4dc6b47 100644 --- a/resources/views/livewire/customer/press-releases/create.blade.php +++ b/resources/views/livewire/customer/press-releases/create.blade.php @@ -25,6 +25,8 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseTags; + public string $portal = 'presseecho'; public string $language = 'de'; @@ -299,41 +301,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex } } - public function addTag(string $tag): void - { - $tag = trim($tag); - - if ($tag === '') { - return; - } - - $existing = $this->tagsArray(); - - if (count($existing) >= 5) { - return; - } - - if (in_array($tag, $existing, true)) { - return; - } - - $existing[] = $tag; - $this->keywords = implode(', ', $existing); - - unset($this->tags, $this->presubmitChecks); - } - - public function removeTag(string $tag): void - { - $existing = array_values(array_filter( - $this->tagsArray(), - fn (string $existingTag): bool => $existingTag !== $tag, - )); - - $this->keywords = implode(', ', $existing); - - unset($this->tags, $this->presubmitChecks); - } + // Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait + // App\Livewire\Concerns\InteractsWithPressReleaseTags. /** * Lazy Auto-Draft: legt sofort einen Entwurf an, damit Titelbild und @@ -662,22 +631,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex ]; } - /** - * @return list - */ - private function tagsArray(): array - { - if (trim($this->keywords) === '') { - return []; - } - - return collect(explode(',', $this->keywords)) - ->map(fn (string $tag): string => trim($tag)) - ->filter() - ->unique() - ->values() - ->all(); - } private function defaultContactIdFor(int $companyId): ?int { @@ -689,19 +642,13 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex */ private function companyContacts(int $companyId): Collection { - return Contact::withoutGlobalScopes() - ->where('company_id', $companyId) - ->orderBy('last_name') - ->orderBy('first_name') + return Contact::forCompany($companyId) ->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']); } private function companyContact(int $contactId, int $companyId): ?Contact { - return Contact::withoutGlobalScopes() - ->where('company_id', $companyId) - ->whereKey($contactId) - ->first(); + return Contact::forCompany($companyId)->whereKey($contactId)->first(); } private function selectedCompany(): ?Company diff --git a/resources/views/livewire/customer/press-releases/edit.blade.php b/resources/views/livewire/customer/press-releases/edit.blade.php index 2179ef7b..18297c4e 100644 --- a/resources/views/livewire/customer/press-releases/edit.blade.php +++ b/resources/views/livewire/customer/press-releases/edit.blade.php @@ -26,6 +26,8 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseTags; + #[Locked] public int $id; @@ -182,41 +184,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl $this->validateScheduledAtWhenReady(); } - public function addTag(string $tag): void - { - $tag = trim($tag); - - if ($tag === '') { - return; - } - - $existing = $this->tagsArray(); - - if (count($existing) >= 5) { - return; - } - - if (in_array($tag, $existing, true)) { - return; - } - - $existing[] = $tag; - $this->keywords = implode(', ', $existing); - - unset($this->tags, $this->presubmitChecks); - } - - public function removeTag(string $tag): void - { - $existing = array_values(array_filter( - $this->tagsArray(), - fn (string $existingTag): bool => $existingTag !== $tag, - )); - - $this->keywords = implode(', ', $existing); - - unset($this->tags, $this->presubmitChecks); - } + // Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait + // App\Livewire\Concerns\InteractsWithPressReleaseTags. /** * Live-Re-Validation: sobald für ein Property bereits ein Error im Bag @@ -638,22 +607,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl ]; } - /** - * @return list - */ - private function tagsArray(): array - { - if (trim($this->keywords) === '') { - return []; - } - - return collect(explode(',', $this->keywords)) - ->map(fn (string $tag): string => trim($tag)) - ->filter() - ->unique() - ->values() - ->all(); - } private function defaultContactIdFor(int $companyId): ?int { @@ -665,19 +618,13 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl */ private function companyContacts(int $companyId): Collection { - return Contact::withoutGlobalScopes() - ->where('company_id', $companyId) - ->orderBy('last_name') - ->orderBy('first_name') + return Contact::forCompany($companyId) ->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']); } private function companyContact(int $contactId, int $companyId): ?Contact { - return Contact::withoutGlobalScopes() - ->where('company_id', $companyId) - ->whereKey($contactId) - ->first(); + return Contact::forCompany($companyId)->whereKey($contactId)->first(); } private function getMyPR(): PressRelease