From 26d5f1a942126bbf8c01dfba0ebc71519667b854 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 19 Jun 2026 08:41:21 +0000 Subject: [PATCH] Optimierung 2.2: InteractsWithPressReleaseScheduling-Trait Die in allen vier PM-Editoren wortgleiche Scheduling-Maschinerie (updatedPublishMode/ScheduledDate/ScheduledTime, updated, syncScheduledAt, resolveScheduledAt, validateScheduledAtWhenReady, notifyValidationError) in einen gemeinsamen Concern ausgelagert (spiegelt InteractsWith- PressReleaseTags). ~85 Zeilen je Datei entfernt. Tests: volle Suite gruen (794 passed, 1 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../InteractsWithPressReleaseScheduling.php | 145 ++++++++++++++++++ .../admin/press-releases/create.blade.php | 113 +------------- .../admin/press-releases/edit.blade.php | 111 +------------- .../customer/press-releases/create.blade.php | 131 +--------------- .../customer/press-releases/edit.blade.php | 128 +--------------- 5 files changed, 149 insertions(+), 479 deletions(-) create mode 100644 app/Livewire/Concerns/InteractsWithPressReleaseScheduling.php diff --git a/app/Livewire/Concerns/InteractsWithPressReleaseScheduling.php b/app/Livewire/Concerns/InteractsWithPressReleaseScheduling.php new file mode 100644 index 00000000..97eb67b0 --- /dev/null +++ b/app/Livewire/Concerns/InteractsWithPressReleaseScheduling.php @@ -0,0 +1,145 @@ +syncScheduledAt(); + + if ($this->publishMode === 'now') { + $this->scheduledDate = null; + $this->scheduledTime = null; + $this->scheduledAt = null; + $this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']); + } + } + + public function updatedScheduledDate(): void + { + $this->syncScheduledAt(); + $this->validateScheduledAtWhenReady(); + } + + public function updatedScheduledTime(): void + { + $this->syncScheduledAt(); + $this->validateScheduledAtWhenReady(); + } + + /** + * Live-Re-Validation: sobald für ein Property bereits ein Error im Bag + * liegt, wird es bei jeder Änderung neu geprüft. Die Termin-Synchronisierung + * liegt vollständig in den spezifischen `updated{PublishMode,ScheduledDate, + * ScheduledTime}`-Hooks; hier bleibt nur die generische Re-Validierung. + */ + public function updated(string $property): void + { + if (! $this->getErrorBag()->has($property)) { + return; + } + + try { + $this->validateOnly($property, $this->formRules()); + } catch (ValidationException) { + // Feld bleibt invalid — Error-Bag wird automatisch befüllt. + } + } + + protected function syncScheduledAt(): void + { + if ($this->publishMode !== 'scheduled') { + $this->scheduledAt = null; + + return; + } + + if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) { + $scheduledAt = Carbon::parse($this->scheduledAt); + $this->scheduledDate = $scheduledAt->format('Y-m-d'); + $this->scheduledTime = $scheduledAt->format('H:i'); + + return; + } + + if (blank($this->scheduledDate) || blank($this->scheduledTime)) { + $this->scheduledAt = null; + + return; + } + + $this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}"; + } + + /** + * Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin). + * Null, wenn kein Termin gesetzt ist. + */ + protected function resolveScheduledAt(): ?Carbon + { + if (blank($this->scheduledAt)) { + return null; + } + + return Carbon::parse($this->scheduledAt); + } + + protected function validateScheduledAtWhenReady(): void + { + if (blank($this->scheduledAt)) { + return; + } + + $this->resetErrorBag('scheduledAt'); + + $scheduledAt = $this->resolveScheduledAt(); + + if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) { + $this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.')); + + return; + } + + try { + $this->validateOnly('scheduledAt', $this->formRules()); + } catch (ValidationException) { + // Termin bleibt invalid — Error-Bag wird automatisch befüllt. + } + } + + /** + * Toast mit Sammelhinweis nach fehlgeschlagener Validierung. Die einzelnen + * Feld-Errors werden weiterhin direkt am Input angezeigt. + */ + protected function notifyValidationError(?ValidationException $exception = null): void + { + $count = $exception + ? array_sum(array_map('count', $exception->errors())) + : count($this->getErrorBag()->all()); + + Flux::toast( + heading: __('Bitte Eingaben prüfen'), + text: $count > 1 + ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) + : __('Ein Feld benötigt deine Aufmerksamkeit.'), + variant: 'danger', + duration: 6000, + ); + } +} diff --git a/resources/views/livewire/admin/press-releases/create.blade.php b/resources/views/livewire/admin/press-releases/create.blade.php index 8bc419a9..93527744 100644 --- a/resources/views/livewire/admin/press-releases/create.blade.php +++ b/resources/views/livewire/admin/press-releases/create.blade.php @@ -19,6 +19,7 @@ use Livewire\Attributes\Title; use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; public string $portal = 'presseecho'; @@ -66,30 +67,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex $this->resetErrorBag('companyId'); } - public function updatedPublishMode(): void - { - $this->syncScheduledAt(); - - if ($this->publishMode === 'now') { - $this->scheduledDate = null; - $this->scheduledTime = null; - $this->scheduledAt = null; - $this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']); - } - } - - public function updatedScheduledDate(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - - public function updatedScheduledTime(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - public function updatedCompanyId(): void { if (!$this->companyId) { @@ -162,94 +139,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex return $rules; } - protected function syncScheduledAt(): void - { - if ($this->publishMode !== 'scheduled') { - $this->scheduledAt = null; - - return; - } - - if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) { - $scheduledAt = \Carbon\Carbon::parse($this->scheduledAt); - $this->scheduledDate = $scheduledAt->format('Y-m-d'); - $this->scheduledTime = $scheduledAt->format('H:i'); - - return; - } - - if (blank($this->scheduledDate) || blank($this->scheduledTime)) { - $this->scheduledAt = null; - - return; - } - - $this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}"; - } - - /** - * Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin). - * Null, wenn kein Termin gesetzt ist. - */ - protected function resolveScheduledAt(): ?\Carbon\Carbon - { - if (blank($this->scheduledAt)) { - return null; - } - - return \Carbon\Carbon::parse($this->scheduledAt); - } - - protected function validateScheduledAtWhenReady(): void - { - if (blank($this->scheduledAt)) { - return; - } - - $this->resetErrorBag('scheduledAt'); - - $scheduledAt = $this->resolveScheduledAt(); - - if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) { - $this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.')); - - return; - } - - try { - $this->validateOnly('scheduledAt', $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Termin bleibt invalid; Bag wird automatisch befüllt. - } - } - - /** - * Live-Re-Validation für bereits invalide Felder. - * - * Die Termin-Synchronisierung liegt vollständig in den spezifischen - * `updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks; hier bleibt - * nur die generische Re-Validierung bereits fehlerhafter Felder. - */ - public function updated(string $property): void - { - if (!$this->getErrorBag()->has($property)) { - return; - } - - try { - $this->validateOnly($property, $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Field bleibt invalid; Bag wird automatisch befüllt. - } - } - - protected function notifyValidationError(?\Illuminate\Validation\ValidationException $exception = null): void - { - $count = $exception ? array_sum(array_map('count', $exception->errors())) : count($this->getErrorBag()->all()); - - Flux::toast(heading: __('Bitte Eingaben prüfen'), text: $count > 1 ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) : __('Ein Feld benötigt deine Aufmerksamkeit.'), variant: 'danger', duration: 6000); - } - public function save(string $submitStatus = 'draft'): void { $this->syncScheduledAt(); diff --git a/resources/views/livewire/admin/press-releases/edit.blade.php b/resources/views/livewire/admin/press-releases/edit.blade.php index fb989143..cf683532 100644 --- a/resources/views/livewire/admin/press-releases/edit.blade.php +++ b/resources/views/livewire/admin/press-releases/edit.blade.php @@ -24,6 +24,7 @@ use Livewire\Attributes\Title; use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; #[Locked] @@ -120,30 +121,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl $this->resetErrorBag('companyId'); } - public function updatedPublishMode(): void - { - $this->syncScheduledAt(); - - if ($this->publishMode === 'now') { - $this->scheduledDate = null; - $this->scheduledTime = null; - $this->scheduledAt = null; - $this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']); - } - } - - public function updatedScheduledDate(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - - public function updatedScheduledTime(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - public function updatedCompanyId(): void { if (!$this->companyId) { @@ -208,92 +185,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl return $rules; } - protected function syncScheduledAt(): void - { - if ($this->publishMode !== 'scheduled') { - $this->scheduledAt = null; - - return; - } - - if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) { - $scheduledAt = \Carbon\Carbon::parse($this->scheduledAt); - $this->scheduledDate = $scheduledAt->format('Y-m-d'); - $this->scheduledTime = $scheduledAt->format('H:i'); - - return; - } - - if (blank($this->scheduledDate) || blank($this->scheduledTime)) { - $this->scheduledAt = null; - - return; - } - - $this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}"; - } - - /** - * Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin). - * Null, wenn kein Termin gesetzt ist. - */ - protected function resolveScheduledAt(): ?\Carbon\Carbon - { - if (blank($this->scheduledAt)) { - return null; - } - - return \Carbon\Carbon::parse($this->scheduledAt); - } - - protected function validateScheduledAtWhenReady(): void - { - if (blank($this->scheduledAt)) { - return; - } - - $this->resetErrorBag('scheduledAt'); - - $scheduledAt = $this->resolveScheduledAt(); - - if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) { - $this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.')); - - return; - } - - try { - $this->validateOnly('scheduledAt', $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Termin bleibt invalid. - } - } - - /** - * Die Termin-Synchronisierung liegt vollständig in den spezifischen - * `updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks; hier bleibt - * nur die generische Re-Validierung bereits fehlerhafter Felder. - */ - public function updated(string $property): void - { - if (!$this->getErrorBag()->has($property)) { - return; - } - - try { - $this->validateOnly($property, $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Field bleibt invalid. - } - } - - protected function notifyValidationError(?\Illuminate\Validation\ValidationException $exception = null): void - { - $count = $exception ? array_sum(array_map('count', $exception->errors())) : count($this->getErrorBag()->all()); - - Flux::toast(heading: __('Bitte Eingaben prüfen'), text: $count > 1 ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) : __('Ein Feld benötigt deine Aufmerksamkeit.'), variant: 'danger', duration: 6000); - } - public function save(): void { $this->syncScheduledAt(); diff --git a/resources/views/livewire/customer/press-releases/create.blade.php b/resources/views/livewire/customer/press-releases/create.blade.php index a4dc6b47..8f494d19 100644 --- a/resources/views/livewire/customer/press-releases/create.blade.php +++ b/resources/views/livewire/customer/press-releases/create.blade.php @@ -25,6 +25,7 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; public string $portal = 'presseecho'; @@ -120,75 +121,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex $this->resetErrorBag('companyId'); } - public function updatedPublishMode(): void - { - $this->syncScheduledAt(); - - if ($this->publishMode === 'now') { - $this->scheduledDate = null; - $this->scheduledTime = null; - $this->scheduledAt = null; - $this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']); - } - } - - public function updatedScheduledDate(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - - public function updatedScheduledTime(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - - /** - * Live-Re-Validation: sobald für ein Property bereits ein Error im Bag - * liegt, wird es bei jeder Änderung neu geprüft. So verschwindet ein - * roter Hinweis sofort, wenn der User das Feld korrekt ausfüllt — und - * der User muss nicht erst auf „Entwurf speichern" klicken. - * - * Die Termin-Synchronisierung liegt vollständig in den spezifischen - * `updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks; hier bleibt - * nur die generische Re-Validierung bereits fehlerhafter Felder. - */ - public function updated(string $property): void - { - if (! $this->getErrorBag()->has($property)) { - return; - } - - try { - $this->validateOnly($property, $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Field bleibt invalid — Error-Bag wird automatisch befüllt. - } - } - - /** - * Toast mit Sammelhinweis nach fehlgeschlagener Validierung. - * Die einzelnen Feld-Errors werden weiterhin direkt am Input angezeigt, - * der Toast dient als zusätzlicher Wegweiser, falls der erste Fehler - * außerhalb des Viewports liegt. - */ - protected function notifyValidationError(?\Illuminate\Validation\ValidationException $exception = null): void - { - $count = $exception - ? array_sum(array_map('count', $exception->errors())) - : count($this->getErrorBag()->all()); - - Flux::toast( - heading: __('Bitte Eingaben prüfen'), - text: $count > 1 - ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) - : __('Ein Feld benötigt deine Aufmerksamkeit.'), - variant: 'danger', - duration: 6000, - ); - } - /** * Single Source of Truth für die Validierungsregeln. * @@ -240,67 +172,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex return $rules; } - protected function syncScheduledAt(): void - { - if ($this->publishMode !== 'scheduled') { - $this->scheduledAt = null; - - return; - } - - if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) { - $scheduledAt = \Carbon\Carbon::parse($this->scheduledAt); - $this->scheduledDate = $scheduledAt->format('Y-m-d'); - $this->scheduledTime = $scheduledAt->format('H:i'); - - return; - } - - if (blank($this->scheduledDate) || blank($this->scheduledTime)) { - $this->scheduledAt = null; - - return; - } - - $this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}"; - } - - /** - * Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin). - * Null, wenn kein Termin gesetzt ist. - */ - protected function resolveScheduledAt(): ?\Carbon\Carbon - { - if (blank($this->scheduledAt)) { - return null; - } - - return \Carbon\Carbon::parse($this->scheduledAt); - } - - protected function validateScheduledAtWhenReady(): void - { - if (blank($this->scheduledAt)) { - return; - } - - $this->resetErrorBag('scheduledAt'); - - $scheduledAt = $this->resolveScheduledAt(); - - if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) { - $this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.')); - - return; - } - - try { - $this->validateOnly('scheduledAt', $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Termin bleibt invalid — Error-Bag wird automatisch befüllt. - } - } - // Tag-Verwaltung (addTag/removeTag/tagsArray) liegt im Trait // App\Livewire\Concerns\InteractsWithPressReleaseTags. diff --git a/resources/views/livewire/customer/press-releases/edit.blade.php b/resources/views/livewire/customer/press-releases/edit.blade.php index 18297c4e..234ff10c 100644 --- a/resources/views/livewire/customer/press-releases/edit.blade.php +++ b/resources/views/livewire/customer/press-releases/edit.blade.php @@ -26,6 +26,7 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component { + use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; #[Locked] @@ -160,75 +161,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl $this->resetErrorBag('companyId'); } - public function updatedPublishMode(): void - { - $this->syncScheduledAt(); - - if ($this->publishMode === 'now') { - $this->scheduledDate = null; - $this->scheduledTime = null; - $this->scheduledAt = null; - $this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']); - } - } - - public function updatedScheduledDate(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - - public function updatedScheduledTime(): void - { - $this->syncScheduledAt(); - $this->validateScheduledAtWhenReady(); - } - // 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 - * liegt, wird es bei jeder Änderung neu geprüft. So verschwindet ein - * roter Hinweis sofort, wenn der User das Feld korrekt ausfüllt — und - * der User muss nicht erst auf „Speichern" klicken. - * - * Die Termin-Synchronisierung liegt vollständig in den spezifischen - * `updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks; hier bleibt - * nur die generische Re-Validierung bereits fehlerhafter Felder. - */ - public function updated(string $property): void - { - if (! $this->getErrorBag()->has($property)) { - return; - } - - try { - $this->validateOnly($property, $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Field bleibt invalid — Error-Bag wird automatisch befüllt. - } - } - - /** - * Toast mit Sammelhinweis nach fehlgeschlagener Validierung. - */ - protected function notifyValidationError(?\Illuminate\Validation\ValidationException $exception = null): void - { - $count = $exception - ? array_sum(array_map('count', $exception->errors())) - : count($this->getErrorBag()->all()); - - Flux::toast( - heading: __('Bitte Eingaben prüfen'), - text: $count > 1 - ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) - : __('Ein Feld benötigt deine Aufmerksamkeit.'), - variant: 'danger', - duration: 6000, - ); - } - /** * Single Source of Truth für die Validierungsregeln. * @@ -277,67 +212,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl return $rules; } - protected function syncScheduledAt(): void - { - if ($this->publishMode !== 'scheduled') { - $this->scheduledAt = null; - - return; - } - - if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) { - $scheduledAt = \Carbon\Carbon::parse($this->scheduledAt); - $this->scheduledDate = $scheduledAt->format('Y-m-d'); - $this->scheduledTime = $scheduledAt->format('H:i'); - - return; - } - - if (blank($this->scheduledDate) || blank($this->scheduledTime)) { - $this->scheduledAt = null; - - return; - } - - $this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}"; - } - - /** - * Parst den erfassten Termin in der Anwendungs-Zeitzone (Europe/Berlin). - * Null, wenn kein Termin gesetzt ist. - */ - protected function resolveScheduledAt(): ?\Carbon\Carbon - { - if (blank($this->scheduledAt)) { - return null; - } - - return \Carbon\Carbon::parse($this->scheduledAt); - } - - protected function validateScheduledAtWhenReady(): void - { - if (blank($this->scheduledAt)) { - return; - } - - $this->resetErrorBag('scheduledAt'); - - $scheduledAt = $this->resolveScheduledAt(); - - if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) { - $this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.')); - - return; - } - - try { - $this->validateOnly('scheduledAt', $this->formRules()); - } catch (\Illuminate\Validation\ValidationException) { - // Termin bleibt invalid — Error-Bag wird automatisch befüllt. - } - } - /** * Validiert und speichert die Bearbeitung. Gibt [PressRelease, contentChanged] * zurück oder null bei Firma-/Kontakt-Fehlern (Validierung wirft).