diff --git a/app/Models/PressRelease.php b/app/Models/PressRelease.php index d66c1330..6a426e7a 100644 --- a/app/Models/PressRelease.php +++ b/app/Models/PressRelease.php @@ -480,6 +480,9 @@ class PressRelease extends Model $models = static::query()->withoutGlobalScopes() ->whereKey($pageIds) ->with('company:id,name') + // Für das Einreichungs-Modal je Zeile: ohne eigenes Bild entfällt + // die Bildrechte-Bestätigung. + ->withCount('images') ->orderBy($sortColumn, $sortDir) ->orderBy('id', $sortDir) ->get(); diff --git a/resources/views/components/portal/pr-error-summary.blade.php b/resources/views/components/portal/pr-error-summary.blade.php new file mode 100644 index 00000000..42c731e9 --- /dev/null +++ b/resources/views/components/portal/pr-error-summary.blade.php @@ -0,0 +1,42 @@ +{{-- + Persistente Fehler-Zusammenfassung der PM-Editoren. + + Der Danger-Toast verschwindet nach wenigen Sekunden und die Feld-Errors + verteilen sich über Schreibfläche und Sidebar — diese Box bleibt direkt + bei den Speichern-Buttons stehen, bis alle Fehler behoben sind. Jeder + Eintrag scrollt per Klick zum zugehörigen Panel (data-error-anchor). + + Verwendung: +--}} +@props(['errors']) + +@php + /** Fehler-Keys ohne eigenes Panel springen zum Panel ihres Themas. */ + $anchorAliases = [ + 'scheduledDate' => 'scheduledAt', + 'scheduledTime' => 'scheduledAt', + 'publishMode' => 'scheduledAt', + 'language' => 'backlinkUrl', + 'keywords' => 'backlinkUrl', + ]; +@endphp + +@if ($errors->any()) + +@endif diff --git a/resources/views/components/press-release-submit-modal.blade.php b/resources/views/components/press-release-submit-modal.blade.php index 0d0b4250..2eb8d590 100644 --- a/resources/views/components/press-release-submit-modal.blade.php +++ b/resources/views/components/press-release-submit-modal.blade.php @@ -5,6 +5,7 @@ 'confirmLabel' => null, 'quotaTotal' => null, 'quotaRemaining' => null, + 'hasTitleImage' => true, ]) {{-- @@ -24,6 +25,10 @@ speichert und dann zur Buchungsseite leitet. ZWINGEND in Erstellen/Bearbeiten setzen — sonst ginge der erfasste Inhalt beim Sprung verloren. Fehlt sie (z. B. Detailansicht, dort ist bereits gespeichert), wird direkt verlinkt. + + `has-title-image`: Ohne eigenes Titelbild (nur Platzhalter) entfällt die + Bildrechte-Bestätigung — es gibt nichts zu bestätigen. Default true, damit + die Checkbox im Zweifel lieber einmal zu viel erscheint. --}} @php($bookingRequired = ! (auth()->user()?->hasActiveBooking() ?? true)) @@ -62,7 +67,9 @@ @else -
+ {{-- Ohne eigenes Titelbild startet `images` auf true: die Checkbox ist + ausgeblendet und darf den Bestätigen-Button nicht blockieren. --}} +
{{ __('Veröffentlichung') }} {{ __('Pressemitteilung zur Prüfung einreichen') }} @@ -137,7 +144,7 @@
{{ __('Ihre Bestätigungen') }} + x-text="[agb, {{ $hasTitleImage ? 'images, ' : '' }}contact, waiver, ai].filter(Boolean).length + ' / {{ $hasTitleImage ? 5 : 4 }}'">
@@ -147,13 +154,15 @@ :label="__('Der Inhalt entspricht den AGB und gesetzlichen Vorgaben.')" />
-
- -
+ @if ($hasTitleImage) +
+ +
+ @endif
isUploadFormOpen = true; } + /** + * Wird das Modal clientseitig geschlossen (X-Button), soll der + * Formularstand nicht in die nächste Sitzung „kleben". + */ + public function updatedIsUploadFormOpen(bool $isOpen): void + { + if (!$isOpen) { + $this->resetUploadForm(); + $this->resetErrorBag(); + } + } + /** * Beim Wechsel des Lizenztyps das Detail-Feld leeren — sonst klebt * z. B. der zuvor gewählte CC-Wert (cc_by) im Freitextfeld @@ -73,7 +100,43 @@ new class extends Component { $this->newAiTermsConfirmed = false; $this->resetErrorBag('newLicenseDetail'); + // Automatisch erzeugte Vorschläge weichen dem Vorschlag des neuen Typs. + if ($this->newCopyright === $this->autoCopyright || str_starts_with($this->newCopyright, __('Bild: KI-generiert'))) { + $this->newCopyright = ''; + $this->autoCopyright = ''; + } + + // Lizenztyp „KI-generiert" legt die KI-Kennzeichnung direkt fest. + if ($this->newLicenseType === ImageLicenseType::AiGenerated->value) { + $this->newAiDeclaration = AiImageDeclaration::AiGenerated->value; + $this->resetErrorBag('newAiDeclaration'); + } + $this->suggestAiCopyright(); + $this->suggestCopyrightFromAuthor(); + } + + /** + * Urheber-Angabe in den Bildnachweis-Vorschlag übernehmen, solange der + * Nutzer den Nachweis nicht selbst überschrieben hat. + */ + public function updatedNewAuthor(): void + { + $this->suggestCopyrightFromAuthor(); + } + + private function suggestCopyrightFromAuthor(): void + { + if ($this->newLicenseType === ImageLicenseType::AiGenerated->value) { + return; + } + + if (filled($this->newCopyright) && $this->newCopyright !== $this->autoCopyright) { + return; + } + + $this->autoCopyright = filled($this->newAuthor) ? __('Foto: :author', ['author' => trim($this->newAuthor)]) : ''; + $this->newCopyright = $this->autoCopyright; } /** @@ -143,6 +206,12 @@ new class extends Component { 'newRightsNotes' => ['nullable', 'string', 'max:1000'], 'newRightsConfirmed' => ['accepted'], 'newAiTermsConfirmed' => [$isAiGenerated ? 'accepted' : 'nullable'], + 'newAiDeclaration' => array_filter([ + 'required', + Rule::enum(AiImageDeclaration::class), + // Lizenztyp „KI-generiert" schließt „Originalfoto" aus. + $isAiGenerated ? Rule::in([AiImageDeclaration::AiGenerated->value, AiImageDeclaration::AiEdited->value]) : null, + ]), ], [ 'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'), @@ -154,6 +223,8 @@ new class extends Component { 'newPropertyRightsStatus.required' => __('Bitte angeben, ob Marken, Kunstwerke oder private Orte sichtbar sind.'), 'newRightsConfirmed.accepted' => __('Bitte bestätigen, dass die Bildrechte geklärt sind.'), 'newAiTermsConfirmed.accepted' => __('Bitte bestätigen, dass die Anbieter-Bedingungen die kommerzielle Nutzung erlauben.'), + 'newAiDeclaration.required' => __('Bitte die KI-Kennzeichnung des Bildes wählen: Originalfoto, KI-generiert oder KI-bearbeitet.'), + 'newAiDeclaration.in' => __('Bei Lizenztyp „KI-generiert" muss die Kennzeichnung KI-generiert oder KI-bearbeitet sein.'), ], ); @@ -178,6 +249,7 @@ new class extends Component { 'rights_notes' => $this->newRightsNotes ?: null, 'rights_confirmed_at' => now(), 'is_ai_generated' => $isAiGenerated, + 'ai_declaration' => $this->newAiDeclaration, 'is_preview' => true, 'sort_order' => ((int) $pressRelease->images()->max('sort_order')) + 1, 'width' => $stored['width'], @@ -275,7 +347,7 @@ new class extends Component { private function resetUploadForm(): void { - $this->reset(['newImage', 'newTitle', 'newCopyright', 'newAuthor', 'newLicenseType', 'newLicenseDetail', 'newLicenseUrl', 'newSourceUrl', 'newPeopleRightsStatus', 'newPropertyRightsStatus', 'newRightsNotes', 'newRightsConfirmed', 'newAiTermsConfirmed']); + $this->reset(['newImage', 'newTitle', 'newCopyright', 'newAuthor', 'newLicenseType', 'newLicenseDetail', 'newLicenseUrl', 'newSourceUrl', 'newPeopleRightsStatus', 'newPropertyRightsStatus', 'newRightsNotes', 'newRightsConfirmed', 'newAiTermsConfirmed', 'newAiDeclaration', 'autoCopyright']); } /** @@ -388,24 +460,29 @@ new class extends Component {
@elseif($canEdit) - @if (!$isUploadFormOpen) -
-
- {{ __('Hier fehlt ein Titelbild') }} - - {{ __('Der Platzhalter bleibt aktiv, bis ein eigenes Titelbild hochgeladen wurde.') }} - -
- - - {{ __('Eigenes Titelbild hochladen') }} - +
+
+ {{ __('Hier fehlt ein Titelbild') }} + + {{ __('Der Platzhalter bleibt aktiv, bis ein eigenes Titelbild hochgeladen wurde.') }} +
- @else -
- {{ __('Titelbild hochladen') }} + + + {{ __('Eigenes Titelbild hochladen') }} + +
+ + + @if ($isUploadFormOpen) + +
+ {{ __('Titelbild hochladen') }} + + {{ __('Bild auswählen, Herkunft und Bildrechte angeben — fertig.') }} + +
{{-- ===== Schritt 1 · Bild auswählen ===== --}}
@@ -427,6 +504,10 @@ new class extends Component { class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100"> {{ __('Bitte laden Sie nur Bilder hoch, für die Sie die erforderlichen Nutzungsrechte besitzen. Bilder aus Google, Social Media, Messenger-Gruppen oder fremden Websites dürfen nicht ohne ausdrückliche Erlaubnis verwendet werden.') }}
+ +

+ {{ __('Nach der Bildauswahl folgen drei kurze Schritte zu Herkunft und Bildrechten.') }} +

@else {{-- Große Vorschau im Titelbild-Format, damit das Motiv vor dem Hochladen wirklich beurteilbar ist. --}} @@ -460,151 +541,185 @@ new class extends Component { - {{-- ===== Schritt 2 · Öffentliche Bildinfos ===== --}} -
-
- 2 - {{ __('Bildinformationen (öffentlich sichtbar)') }} -
-
- + {{-- Die weiteren Schritte erscheinen erst, wenn ein Bild gewählt + wurde — so bleibt der Einstieg übersichtlich. --}} + @if ($newImage) + {{-- ===== Schritt 2 · Herkunft & Bildnachweis ===== --}} +
+
+ 2 + {{ __('Herkunft & Bildnachweis') }} +
+ +
+ + {{ __('Bitte wählen…') }} + + @foreach ($licenseTypeOptions as $value => $label) + {{ $label }} + @endforeach + + +
+ + @if ($showsAiSection) + + +
+ {{ __('KI-generierte Bilder haben keinen menschlichen Urheber — maßgeblich sind die Nutzungsbedingungen des KI-Anbieters. Das Bild wird öffentlich als KI-generiert gekennzeichnet (Transparenzpflicht, EU AI Act). Achten Sie darauf, dass keine realen Personen, Marken oder geschützten Werke erkennbar nachgebildet werden.') }} +
+ + + @elseif ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value) + + {{ __('Bitte wählen…') }} + + @foreach ($ccLicenseOptions as $value => $label) + {{ $label }} + @endforeach + + +
+ {{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }} +
+ @elseif($licenseDetailRequired) + + @endif + + @if ($licenseUrlRequired) + + @endif + + {{-- KI-Kennzeichnung je Bild (Art. 50 KI-VO): Pflichtauswahl. + Beim Lizenztyp „KI-generiert" wird sie automatisch gesetzt + und deshalb hier nicht mehr abgefragt. --}} + @if (!$showsAiSection) + + @foreach (\App\Enums\AiImageDeclaration::cases() as $aiImageOption) + + @endforeach + + + @endif + -
-
+ :description="__('Wird öffentlich unter dem Bild angezeigt — Vorschlag anpassbar.')" + :placeholder="__('z. B. Foto: Max Mustermann / Beispiel GmbH')" required /> + - {{-- ===== Schritt 3 · Herkunft & Lizenz ===== --}} -
-
- 3 - {{ __('Herkunft & Lizenz') }} -
-
- - - {{ __('Bitte wählen…') }} - @foreach ($licenseTypeOptions as $value => $label) - {{ $label }} - @endforeach - -
+ {{-- ===== Schritt 3 · Personen & Rechte Dritter ===== --}} +
+
+ 3 + {{ __('Personen & Rechte Dritter') }} +
+
+ + @foreach ($peopleRightsOptions as $value => $label) + + @endforeach + - @if ($showsAiSection) - - -
- {{ __('KI-generierte Bilder haben keinen menschlichen Urheber — maßgeblich sind die Nutzungsbedingungen des KI-Anbieters. Das Bild wird öffentlich als KI-generiert gekennzeichnet (Transparenzpflicht, EU AI Act). Achten Sie darauf, dass keine realen Personen, Marken oder geschützten Werke erkennbar nachgebildet werden.') }} + + @foreach ($propertyRightsOptions as $value => $label) + + @endforeach +
- - @elseif ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value) - - {{ __('Bitte wählen…') }} - @foreach ($ccLicenseOptions as $value => $label) - {{ $label }} - @endforeach - + @if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true)) +
+ {{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }} +
+ @endif -
- {{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }} + @if ($showsRightsWarning) +
+ {{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }} +
+ @endif +
+ + {{-- ===== Optionale Angaben (eingeklappt) ===== --}} +
+ + + {{ __('Optionale Angaben') }} + + — {{ __('Alt-Text, Quellen-URLs, interne Hinweise') }} + + +
+ +
+ @unless ($licenseUrlRequired) + + @endunless + +
+
- @elseif($licenseDetailRequired) - +
+ + {{-- ===== Schritt 4 · Bestätigung ===== --}} +
+
+ 4 + {{ __('Bestätigung') }} +
+ + +
+ @endif + +
+ + {{ __('Abbrechen') }} + @if ($newImage) + + {{ __('Hochladen & einsetzen') }} @endif - -
- - -
-
- - {{-- ===== Schritt 4 · Personen & Rechte Dritter ===== --}} -
-
- 4 - {{ __('Personen & Rechte Dritter') }} -
-
- - @foreach ($peopleRightsOptions as $value => $label) - - @endforeach - - - - @foreach ($propertyRightsOptions as $value => $label) - - @endforeach - -
- - @if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true)) -
- {{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }} -
- @endif - - @if ($showsRightsWarning) -
- {{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }} -
- @endif -
- - {{-- ===== Schritt 5 · Bestätigung ===== --}} -
-
- 5 - {{ __('Bestätigung') }} -
- - - - - -
- - {{ __('Abbrechen') }} - {{ __('Hochladen') }} - -
-
+
- @endif + @endif + @else
diff --git a/resources/views/livewire/customer/press-releases/create.blade.php b/resources/views/livewire/customer/press-releases/create.blade.php index 978638c6..17bdcf85 100644 --- a/resources/views/livewire/customer/press-releases/create.blade.php +++ b/resources/views/livewire/customer/press-releases/create.blade.php @@ -24,6 +24,7 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class extends Component { + use \App\Livewire\Concerns\InteractsWithAiDeclaration; use \App\Livewire\Concerns\InteractsWithPressReleaseContacts; use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; @@ -297,6 +298,12 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex public function save(string $submitStatus = 'draft'): void { + // KI-Kennzeichnung (Art. 50 KI-VO): Pflicht nur beim Einreichen — + // zuerst prüfen, damit die Fehler direkt im Submit-Modal erscheinen. + if ($submitStatus === 'review') { + $this->validateAiDeclaration(); + } + $pr = $this->persistDraft(); if (! $pr) { @@ -374,6 +381,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex ? $this->resolveScheduledAt() : null, 'embargo_at' => null, + ...$this->aiDeclarationAttributes(), ]; } @@ -485,7 +493,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
{{-- 1) FIRMA-SELEKTOR --}} -
+
{{ __('Für Firma') }} @@ -534,8 +542,31 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
- {{-- 2) TITEL --}} -
+ {{-- 2) KATEGORIE (Pflichtfeld — bewusst prominent vor der Headline) --}} +
+
+
+ + {{ __('Kategorie') }} * + +
+ + + @foreach ($categories as $cat) + + @endforeach + +
+
+ + {{ __('Pflichtfeld — steuert, in welcher Rubrik die PM erscheint.') }} + + +
+
+ + {{-- 3) TITEL --}} +
@@ -566,8 +597,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
- {{-- 3) SUBTITLE --}} -
+ {{-- 4) SUBTITLE --}} +
@@ -593,8 +624,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
- {{-- 4) FLIESSTEXT --}} -
+ {{-- 5) FLIESSTEXT --}} +
@@ -636,7 +667,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
- {{-- 5) TITELBILD --}} + {{-- 6) TITELBILD --}}
@@ -692,7 +723,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex {{-- Titelbild-Platzhalter-Auswahl --}} - {{-- 6) ANHÄNGE — TEMPORÄR DEAKTIVIERT + {{-- 7) ANHÄNGE — TEMPORÄR DEAKTIVIERT Datei-Uploads erfordern eine vollständige Sicherheitsprüfung (Virus-/Malware-Scan, MIME-Validierung, Storage-Quoten). Wird in einer späteren Phase aktiviert. @@ -713,8 +744,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
--}} - {{-- 7) BOILERPLATE --}} -
+ {{-- 8) BOILERPLATE --}} +
@@ -775,6 +806,8 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex {{ __('Entwurf') }}
+ + @@ -806,28 +839,6 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
- {{-- Kategorie (Pflichtfeld - prominent direkt unter Status) --}} -
-
- - {{ __('Kategorie') }} - * - -
-
- - - - @foreach ($categories as $cat) - - @endforeach - - - {{ __('Pflichtfeld — steuert, in welcher Rubrik die PM erscheint.') }} - -
-
- {{-- Portal (Read-only, Badge in Portal-Farbe) --}} @php $portalPillClass = 'portal-pill'; @@ -857,7 +868,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex {{-- Pressekontakt --}} -
+
{{ __('Pressekontakt') }}
@@ -966,7 +977,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
{{-- Veröffentlichung --}} -
+
{{ __('Veröffentlichung') }}
@@ -1026,7 +1037,7 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
{{-- Weitere Felder --}} -
+
{{ __('Weitere Felder') }}
@@ -1066,12 +1077,15 @@ new #[Layout('components.layouts.app'), Title('Neue Pressemitteilung')] class ex
{{-- Einreichungs-Modal (öffnet über „Zur Prüfung senden") --}} + {{-- Im Erstellen-Formular gibt es noch keinen Bild-Upload (der folgt im + Editor nach dem Entwurf-Sichern), daher entfällt die Bildrechte-Frage. --}} + :quota-remaining="$quotaRemaining" + :has-title-image="false" /> @endif
diff --git a/resources/views/livewire/customer/press-releases/edit.blade.php b/resources/views/livewire/customer/press-releases/edit.blade.php index eff129d2..98018527 100644 --- a/resources/views/livewire/customer/press-releases/edit.blade.php +++ b/resources/views/livewire/customer/press-releases/edit.blade.php @@ -8,6 +8,7 @@ use App\Models\Company; use App\Models\Contact; use App\Models\PressRelease; use App\Services\Customer\CustomerCompanyContext; +use App\Services\PressRelease\AiDeclarationRequiredException; use App\Services\PressRelease\BlacklistViolationException; use App\Services\PressRelease\BookingRequiredException; use App\Services\PressRelease\PressReleaseCoverImage; @@ -24,6 +25,7 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component { + use \App\Livewire\Concerns\InteractsWithAiDeclaration; use \App\Livewire\Concerns\InteractsWithPressReleaseContacts; use \App\Livewire\Concerns\InteractsWithPressReleaseScheduling; use \App\Livewire\Concerns\InteractsWithPressReleaseTags; @@ -122,6 +124,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl } $this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($pr->placeholder_variant?->value)->value; + + // KI-Grad vorbelegen (bereits deklariert); die verbindliche + // Bestätigung muss bei jedem Submit erneut gesetzt werden. + $this->aiTextDeclaration = $pr->ai_text_declaration?->value; } #[On('placeholder-selected')] @@ -248,6 +254,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl ? $this->resolveScheduledAt() : null, 'embargo_at' => null, + ...$this->aiDeclarationAttributes(), ]); $contentChanged = $pr->wasChanged(['title', 'text']); @@ -287,6 +294,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl public function save(bool $submitAfterSave = false): void { + // KI-Kennzeichnung (Art. 50 KI-VO): Pflicht nur beim Einreichen — + // zuerst prüfen, damit die Fehler direkt im Submit-Modal erscheinen. + if ($submitAfterSave) { + $this->validateAiDeclaration(); + } + $result = $this->persistEdit(); if (! $result) { @@ -311,7 +324,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl $this->redirect(route('me.press-releases.show', $pr->id), navigate: true); return; - } catch (BookingRequiredException|QuotaExceededException $e) { + } catch (AiDeclarationRequiredException|BookingRequiredException|QuotaExceededException $e) { Flux::toast( heading: __('Gespeichert, aber nicht eingereicht'), text: $e->getMessage(), @@ -487,7 +500,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{-- 1) FIRMA-SELEKTOR --}} -
+
{{ __('Für Firma') }}
@@ -534,8 +547,31 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
- {{-- 2) TITEL --}} -
+ {{-- 2) KATEGORIE (Pflichtfeld — bewusst prominent vor der Headline) --}} +
+
+
+ + {{ __('Kategorie') }} * + +
+ + + @foreach ($categories as $cat) + + @endforeach + +
+ + {{ __('Pflichtfeld — steuert, in welcher Rubrik die PM erscheint.') }} + +
+ +
+
+ + {{-- 3) TITEL --}} +
@@ -566,8 +602,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
- {{-- 3) SUBTITLE --}} -
+ {{-- 4) SUBTITLE --}} +
@@ -593,8 +629,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
- {{-- 4) FLIESSTEXT --}} -
+ {{-- 5) FLIESSTEXT --}} +
@@ -637,7 +673,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
@if ($coverIsPlaceholder) - {{-- 5) TITELBILD-PLATZHALTER --}} + {{-- 6) TITELBILD-PLATZHALTER --}}
@@ -661,18 +697,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl @endif - {{-- 6) MEDIEN — Image-Manager direkt eingebunden --}} + {{-- 7) MEDIEN — Image-Manager direkt eingebunden --}} - {{-- 6) ANHÄNGE — TEMPORÄR DEAKTIVIERT + {{-- 7) ANHÄNGE — TEMPORÄR DEAKTIVIERT Datei-Uploads erfordern eine vollständige Sicherheitsprüfung (Virus-/Malware-Scan, MIME-Validierung, Storage-Quoten). Wird in einer späteren Phase aktiviert. --}} - {{-- 7) BOILERPLATE --}} -
+ {{-- 8) BOILERPLATE --}} +
@@ -737,6 +773,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl @endif
+ + @@ -804,28 +842,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl @endif - {{-- Kategorie (Pflichtfeld - prominent direkt unter Status) --}} -
-
- - {{ __('Kategorie') }} - * - -
-
- - - - @foreach ($categories as $cat) - - @endforeach - - - {{ __('Pflichtfeld — steuert, in welcher Rubrik die PM erscheint.') }} - -
-
- {{-- Portal (Read-only, Badge in Portal-Farbe) --}} @php $portalPillClass = 'portal-pill'; @@ -855,7 +871,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl {{-- Pressekontakt --}} -
+
{{ __('Pressekontakt') }}
@@ -964,7 +980,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{-- Veröffentlichung --}} -
+
{{ __('Veröffentlichung') }}
@@ -1024,7 +1040,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{-- Weitere Felder --}} -
+
{{ __('Weitere Felder') }}
@@ -1070,5 +1086,6 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl booking-action="saveDraftAndChooseBooking" :confirm-label="$currentStatus === 'rejected' ? __('Speichern & erneut einreichen') : __('Speichern & einreichen')" :quota-total="$quotaTotal" - :quota-remaining="$quotaRemaining" /> + :quota-remaining="$quotaRemaining" + :has-title-image="! $coverIsPlaceholder" />
diff --git a/resources/views/livewire/customer/press-releases/index.blade.php b/resources/views/livewire/customer/press-releases/index.blade.php index e41ddff6..88689e29 100644 --- a/resources/views/livewire/customer/press-releases/index.blade.php +++ b/resources/views/livewire/customer/press-releases/index.blade.php @@ -4,6 +4,7 @@ use App\Enums\Portal; use App\Enums\PressReleaseStatus; use App\Models\PressRelease; use App\Services\Customer\CustomerCompanyContext; +use App\Services\PressRelease\AiDeclarationRequiredException; use App\Services\PressRelease\BlacklistViolationException; use App\Services\PressRelease\BookingRequiredException; use App\Services\PressRelease\PressReleaseService; @@ -17,6 +18,7 @@ use Livewire\Volt\Component; use Livewire\WithPagination; new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class extends Component { + use \App\Livewire\Concerns\InteractsWithAiDeclaration; use WithPagination; public string $search = ''; @@ -87,25 +89,25 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class return; } + // KI-Kennzeichnung (Art. 50 KI-VO): Pflicht-Deklaration aus dem + // Submit-Modal validieren und vor dem Einreichen persistieren. + $this->validateAiDeclaration(); + $pr->update($this->aiDeclarationAttributes()); + try { app(PressReleaseService::class)->submitForReview($pr); - Flux::toast( - heading: __('Eingereicht'), - text: __('Pressemitteilung zur Prüfung eingereicht.'), - variant: 'success', - ); + Flux::toast(heading: __('Eingereicht'), text: __('Pressemitteilung zur Prüfung eingereicht.'), variant: 'success'); } catch (BlacklistViolationException $e) { - Flux::toast( - heading: __('Automatisch abgelehnt'), - text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]), - variant: 'danger', - duration: 8000, - ); - } catch (BookingRequiredException|QuotaExceededException $e) { + Flux::toast(heading: __('Automatisch abgelehnt'), text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]), variant: 'danger', duration: 8000); + } catch (AiDeclarationRequiredException | BookingRequiredException | QuotaExceededException $e) { Flux::toast(text: $e->getMessage(), variant: 'warning', duration: 8000); } catch (\LogicException $e) { Flux::toast(text: $e->getMessage(), variant: 'danger'); } + + // Deklaration nicht zwischen PMs „verschleppen": Die Listen-Modals + // teilen sich die Properties, daher nach jedem Versuch zurücksetzen. + $this->reset('aiTextDeclaration', 'aiDeclarationConfirmed'); } public function with(): array @@ -119,7 +121,8 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class // auf die Status-Counts als auch die Liste angewandt, damit beide // deckungsgleich filtern. $applyCompanyFilter = function ($query) use ($selectedCompanyId): void { - $query->when($selectedCompanyId !== null, fn($q) => $q->where('company_id', $selectedCompanyId)) + $query + ->when($selectedCompanyId !== null, fn($q) => $q->where('company_id', $selectedCompanyId)) ->when($selectedCompanyId === null && $this->companyFilter === 'assigned', fn($q) => $q->whereNotNull('company_id')) ->when($selectedCompanyId === null && $this->companyFilter === 'unassigned', fn($q) => $q->whereNull('company_id')); }; @@ -139,9 +142,10 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class // beide UNION-Zweige gezogen (siehe PressRelease::accessiblePageFor()). $applyListFilters = function ($query) use ($applyCompanyFilter): void { $applyCompanyFilter($query); - $query->when(filled($this->search), fn ($q) => $q->where('title', 'like', '%'.$this->search.'%')) - ->when($this->statusFilter !== 'all', fn ($q) => $q->where('status', $this->statusFilter)) - ->when($this->portalFilter !== 'all', fn ($q) => $q->where('portal', $this->portalFilter)); + $query + ->when(filled($this->search), fn($q) => $q->where('title', 'like', '%' . $this->search . '%')) + ->when($this->statusFilter !== 'all', fn($q) => $q->where('status', $this->statusFilter)) + ->when($this->portalFilter !== 'all', fn($q) => $q->where('portal', $this->portalFilter)); }; $prs = PressRelease::accessiblePageFor($user, $applyListFilters, $this->sortBy, $this->sortDir); @@ -410,141 +414,141 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class {{ __('Status') }} - - {{ __('Titel') }} - - {{ __('Portal') }} - {{ __('Firma') }} - - {{ __('Datum') }} - - {{ __('Aktionen') }} - + + {{ __('Titel') }} + + {{ __('Portal') }} + {{ __('Firma') }} + + {{ __('Datum') }} + + {{ __('Aktionen') }} + - @foreach ($pressReleases as $pr) - @php - $status = $pr->status->value; - $rowClass = match ($status) { - 'review' => 'is-row-warn', - 'rejected' => 'is-row-err', - default => '', - }; - $badgeClass = $pr->status->badgeClass(); - $portal = $pr->portal?->value ?? 'both'; - $showPe = in_array($portal, ['presseecho', 'both'], true); - $showBp = in_array($portal, ['businessportal24', 'both'], true); - $dateSubLabel = match ($status) { - 'published' => __('veröffentlicht'), - 'review' => __('eingereicht'), - 'rejected' => __('abgelehnt'), - 'draft' => __('erstellt'), - 'archived' => __('archiviert'), - default => __('erstellt'), - }; - $primaryDate = match (true) { - $status === 'published' && $pr->published_at => $pr->published_at, - default => $pr->created_at, - }; - @endphp + @foreach ($pressReleases as $pr) + @php + $status = $pr->status->value; + $rowClass = match ($status) { + 'review' => 'is-row-warn', + 'rejected' => 'is-row-err', + default => '', + }; + $badgeClass = $pr->status->badgeClass(); + $portal = $pr->portal?->value ?? 'both'; + $showPe = in_array($portal, ['presseecho', 'both'], true); + $showBp = in_array($portal, ['businessportal24', 'both'], true); + $dateSubLabel = match ($status) { + 'published' => __('veröffentlicht'), + 'review' => __('eingereicht'), + 'rejected' => __('abgelehnt'), + 'draft' => __('erstellt'), + 'archived' => __('archiviert'), + default => __('erstellt'), + }; + $primaryDate = match (true) { + $status === 'published' && $pr->published_at => $pr->published_at, + default => $pr->created_at, + }; + @endphp - - -
- {{ $pr->status->label() }} - @if ($status === 'draft') - {{-- Öffnet das gemeinsame Einreichungs-Modal (rechtliche + + +
+ {{ $pr->status->label() }} + @if ($status === 'draft') + {{-- Öffnet das gemeinsame Einreichungs-Modal (rechtliche Bestätigungen inkl. Erlöschensklausel) statt eines Schnell-Submits, der die Bestätigungen umgehen würde. --}} - - - - - @endif -
-
- - - - {{ $pr->title }} - -
- PM-{{ $pr->id }} -
-
- - -
- @if ($showPe) - presseecho - @endif - @if ($showBp) - businessportal24 - @endif -
-
- - - @if ($pr->company) - - {{ $pr->company->name }} - - @else - {{ __('— keine —') }} + + + + @endif - +
+
- -
- {{ $primaryDate?->format('d.m.Y') }} -
-
- {{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }} -
- @if ($status === 'review' && $pr->scheduled_at && $pr->scheduled_at->isFuture()) -
- - {{ __('geplant') }} · {{ $pr->scheduledAtLocal()->format('d.m. H:i') }} -
- @endif - @if ($pr->embargo_at && $pr->embargo_at->isFuture()) -
- - {{ __('Embargo bis') }} {{ $pr->embargoAtLocal()->format('d.m.') }} -
- @endif -
+ + + {{ $pr->title }} + +
+ PM-{{ $pr->id }} +
+
- -
- - @if (in_array($status, ['draft', 'rejected'])) - - @endif + +
+ @if ($showPe) + presseecho + @endif + @if ($showBp) + businessportal24 + @endif +
+
+ + + @if ($pr->company) + + {{ $pr->company->name }} + + @else + {{ __('— keine —') }} + @endif + + + +
+ {{ $primaryDate?->format('d.m.Y') }} +
+
+ {{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }} +
+ @if ($status === 'review' && $pr->scheduled_at && $pr->scheduled_at->isFuture()) +
+ + {{ __('geplant') }} · + {{ $pr->scheduledAtLocal()->format('d.m. H:i') }}
-
- - @endforeach - + @endif + @if ($pr->embargo_at && $pr->embargo_at->isFuture()) +
+ + {{ __('Embargo bis') }} {{ $pr->embargoAtLocal()->format('d.m.') }} +
+ @endif + + + +
+ + @if (in_array($status, ['draft', 'rejected'])) + + @endif +
+
+ + @endforeach + @elseif ($hasAnyPR && $search !== '') {{-- Empty: Suche ohne Treffer --}} - + @@ -559,11 +563,9 @@ new #[Layout('components.layouts.app'), Title('Meine Pressemitteilungen')] class @elseif ($hasAnyPR && $hasAnyFilter) {{-- Empty: Filter ohne Treffer --}} - + @else {{-- Empty: gar keine PMs überhaupt --}} - + diff --git a/resources/views/livewire/customer/press-releases/show.blade.php b/resources/views/livewire/customer/press-releases/show.blade.php index 706601b9..4983e242 100644 --- a/resources/views/livewire/customer/press-releases/show.blade.php +++ b/resources/views/livewire/customer/press-releases/show.blade.php @@ -12,6 +12,7 @@ use App\Models\SinglePurchase; use App\Services\Auth\MagicLinkGenerator; use App\Services\Billing\CreditPricingService; use App\Services\Billing\ProofPdfService; +use App\Services\PressRelease\AiDeclarationRequiredException; use App\Services\PressRelease\BlacklistViolationException; use App\Services\PressRelease\BookingRequiredException; use App\Services\PressRelease\BoostService; @@ -26,6 +27,8 @@ use Livewire\Volt\Component; new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component { + use \App\Livewire\Concerns\InteractsWithAiDeclaration; + #[Locked] public int $id; @@ -48,6 +51,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends $this->id = $id; $pr = $this->getMyPR(); $this->authorize('view', $pr); + + // KI-Grad vorbelegen (bereits deklariert); die verbindliche + // Bestätigung muss bei jedem Submit erneut gesetzt werden. + $this->aiTextDeclaration = $pr->ai_text_declaration?->value; } public function submitForReview(): void @@ -55,6 +62,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends $pr = $this->getMyPR(); $this->authorize('submitForReview', $pr); + // KI-Kennzeichnung (Art. 50 KI-VO): Pflicht-Deklaration aus dem + // Submit-Modal validieren und vor dem Einreichen persistieren. + $this->validateAiDeclaration(); + $pr->update($this->aiDeclarationAttributes()); + try { app(PressReleaseService::class)->submitForReview($pr); } catch (BlacklistViolationException $e) { @@ -68,7 +80,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends ); return; - } catch (BookingRequiredException|QuotaExceededException $e) { + } catch (AiDeclarationRequiredException|BookingRequiredException|QuotaExceededException $e) { Flux::modal('confirm-submit-review')->close(); Flux::toast( @@ -854,13 +866,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends {{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}
- @elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated)) + @elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated || $titleImage->ai_declaration?->publicLabel())) {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
@if ($titleImage->is_ai_generated) {{ __('KI-generiert') }} @endif {{ $titleImage->copyright ?? __('Bild: KI-generiert') }} + @if ($aiImageHint = $titleImage->ai_declaration?->publicLabel()) + · {{ $aiImageHint }} + @endif
@endif @@ -871,6 +886,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends {{ __('Inhalt') }}
+ {{-- KI-Label (Art. 50 KI-VO): erscheint so auch in der + Veröffentlichung — am Anfang des Inhalts, in Einreicher-Stimme. --}} +
{!! $pr->renderedText() !!}
@@ -884,7 +902,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends action="submitForReview" :confirm-label="__('Veröffentlichung anfordern')" :quota-total="$quotaTotal" - :quota-remaining="$quotaRemaining" /> + :quota-remaining="$quotaRemaining" + :has-title-image="! $coverIsPlaceholder" /> @endif {{-- ============== BOILERPLATE-OVERRIDE ============== --}} diff --git a/tests/Feature/PressReleaseImageLicenseTest.php b/tests/Feature/PressReleaseImageLicenseTest.php index d22024dc..7c19f927 100644 --- a/tests/Feature/PressReleaseImageLicenseTest.php +++ b/tests/Feature/PressReleaseImageLicenseTest.php @@ -1,5 +1,6 @@ $pr->id]) ->call('openUploadForm') + ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) ->assertSet('newLicenseType', '') ->assertSee('Bitte wählen') ->assertSee('Eigene Aufnahme'); }); +test('detail fields only appear after an image was selected', function () { + /** @var TestCase $this */ + ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); + $this->actingAs($owner); + + LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) + ->call('openUploadForm') + ->assertSee('Bild hierher ziehen oder klicken') + ->assertDontSee('Herkunft & Bildnachweis') + ->assertDontSee('Personen & Rechte Dritter') + ->assertDontSee('Rechte bestätigt') + ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) + ->assertSee('Herkunft & Bildnachweis') + ->assertSee('Personen & Rechte Dritter') + ->assertSee('Rechte bestätigt'); +}); + test('title image upload form is collapsed until requested', function () { /** @var TestCase $this */ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); @@ -180,6 +199,7 @@ test('valid upload stores license metadata and confirms rights', function () { ->set('newCopyright', 'Foto: Jane Doe / Beispiel GmbH') ->set('newAuthor', 'Jane Doe') ->set('newLicenseType', ImageLicenseType::Own->value) + ->set('newAiDeclaration', AiImageDeclaration::Original->value) ->set('newSourceUrl', 'https://example.com/source') ->set('newPeopleRightsStatus', 'consent') ->set('newPropertyRightsStatus', 'cleared') @@ -228,6 +248,7 @@ test('valid cc upload stores license detail and license url', function () { ->set('newAuthor', 'Jane Doe') ->set('newCopyright', 'Foto: Jane Doe (CC BY 4.0)') ->set('newLicenseType', ImageLicenseType::CreativeCommons->value) + ->set('newAiDeclaration', AiImageDeclaration::Original->value) ->set('newLicenseDetail', 'cc_by') ->set('newLicenseUrl', 'https://creativecommons.org/licenses/by/4.0/') ->set('newPeopleRightsStatus', 'none') @@ -244,7 +265,7 @@ test('valid cc upload stores license detail and license url', function () { expect($image->license_url)->toBe('https://creativecommons.org/licenses/by/4.0/'); }); -test('the public image credit is required', function () { +test('the public image credit is required even when the suggestion was cleared', function () { /** @var TestCase $this */ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); $this->actingAs($owner); @@ -253,6 +274,8 @@ test('the public image credit is required', function () { ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) ->set('newAuthor', 'Jane Doe') ->set('newLicenseType', ImageLicenseType::Own->value) + // Der Vorschlag aus dem Urheber wird bewusst entfernt. + ->set('newCopyright', '') ->set('newPeopleRightsStatus', 'none') ->set('newPropertyRightsStatus', 'none') ->set('newRightsConfirmed', true) @@ -311,9 +334,42 @@ test('a valid ai generated upload stores tool and ai flag', function () { expect($image->license_type)->toBe(ImageLicenseType::AiGenerated); expect($image->license_detail)->toBe('Midjourney v7'); expect($image->is_ai_generated)->toBeTrue(); + // Lizenztyp „KI-generiert" setzt die KI-Kennzeichnung automatisch. + expect($image->ai_declaration)->toBe(AiImageDeclaration::AiGenerated); expect($image->copyright)->toBe('Bild: KI-generiert (Midjourney v7)'); }); +test('the copyright suggestion follows the author but respects manual input', function () { + /** @var TestCase $this */ + ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); + $this->actingAs($owner); + + LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) + ->set('newLicenseType', ImageLicenseType::Own->value) + ->set('newAuthor', 'Jane Doe') + ->assertSet('newCopyright', 'Foto: Jane Doe') + ->set('newAuthor', 'John Smith') + ->assertSet('newCopyright', 'Foto: John Smith') + ->set('newCopyright', 'Eigener Nachweis') + ->set('newAuthor', 'Jane Doe') + ->assertSet('newCopyright', 'Eigener Nachweis'); +}); + +test('switching to ai replaces an automatically suggested author copyright', function () { + /** @var TestCase $this */ + ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); + $this->actingAs($owner); + + LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) + ->set('newLicenseType', ImageLicenseType::Own->value) + ->set('newAuthor', 'Jane Doe') + ->assertSet('newCopyright', 'Foto: Jane Doe') + ->set('newLicenseType', ImageLicenseType::AiGenerated->value) + ->assertSet('newCopyright', 'Bild: KI-generiert') + ->set('newLicenseType', ImageLicenseType::Own->value) + ->assertSet('newCopyright', 'Foto: Jane Doe'); +}); + test('the ai copyright suggestion follows the tool but respects manual input', function () { /** @var TestCase $this */ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); diff --git a/tests/Feature/PressReleasePublishModalPhase8iTest.php b/tests/Feature/PressReleasePublishModalPhase8iTest.php index f6ee5ddb..f26d0d82 100644 --- a/tests/Feature/PressReleasePublishModalPhase8iTest.php +++ b/tests/Feature/PressReleasePublishModalPhase8iTest.php @@ -1,5 +1,7 @@ actingAs($customer); LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) + // KI-Kennzeichnung (Art. 50 KI-VO) ist Pflicht im Submit-Modal. + ->set('aiTextDeclaration', AiTextDeclaration::None->value) + ->set('aiDeclarationConfirmed', true) ->call('submitForReview') ->assertHasNoErrors(); @@ -66,6 +71,39 @@ test('submitting from the show modal moves the draft into review without consumi expect($customer->fresh()->press_release_quota_used_this_month)->toBe(0); }); +test('the image rights confirmation is hidden while only the placeholder is active', function () { + /** @var TestCase $this */ + ['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a(); + $this->actingAs($customer); + + LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) + ->assertSee('Ihre Bestätigungen') + ->assertDontSee('Alle Bildrechte sind geklärt.') + ->assertSeeHtml("' / 4'"); +}); + +test('the image rights confirmation appears once an own title image exists', function () { + /** @var TestCase $this */ + ['customer' => $customer, 'pr' => $pr] = makeCustomerForShowPhase8a(); + + $pr->images()->create([ + 'disk' => 'public', + 'path' => 'press-releases/'.$pr->id.'/images/title.jpg', + 'variants' => ['cover' => 'press-releases/'.$pr->id.'/images/title-cover.jpg'], + 'author' => 'Jane Doe', + 'license_type' => ImageLicenseType::Own->value, + 'rights_confirmed_at' => now(), + 'is_preview' => true, + 'sort_order' => 1, + ]); + + $this->actingAs($customer); + + LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id]) + ->assertSee('Alle Bildrechte sind geklärt.') + ->assertSeeHtml("' / 5'"); +}); + test('the modal shows a booking notice instead of the submit flow when the gate is enforced', function () { /** @var TestCase $this */ config()->set('billing.enforce_booking', true);