id = $id; } public function publish(): void { $pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id); try { app(PressReleaseService::class)->publish($pr); } catch (BlacklistViolationException $e) { Flux::toast( heading: __('Automatisch abgelehnt'), text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]), variant: 'danger', duration: 8000, ); Flux::modal('confirm-show-publish')->close(); return; } Flux::toast(text: __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'), variant: 'success'); Flux::modal('confirm-show-publish')->close(); } public function reject(): void { $this->validate([ 'rejectReason' => ['required', 'string', 'min:5', 'max:2000'], ]); $pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id); app(PressReleaseService::class)->reject($pr, trim($this->rejectReason)); $this->rejectReason = ''; Flux::toast(text: __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'), variant: 'warning'); Flux::modal('confirm-show-reject')->close(); } public function archive(): void { $pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id); app(PressReleaseService::class)->archive($pr); Flux::toast(text: __('Archiviert.'), variant: 'success'); Flux::modal('confirm-show-archive')->close(); } public function with(): array { $pr = PressRelease::withoutGlobalScopes() ->with([ 'company:id,name,email,phone,slug', 'category.translations', 'user:id,name,email', 'images', 'attachments', 'contacts' => fn ($query) => $query ->withoutGlobalScopes() ->orderBy('last_name') ->orderBy('first_name') ->select(['contacts.id', 'contacts.company_id', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']), 'statusLogs.changedBy:id,name', 'kiAudits', ]) ->findOrFail($this->id); $latestClassification = $pr->kiAudits ->firstWhere('type', \App\Models\KiAudit::TYPE_CLASSIFICATION); $latestRejection = null; if ($pr->status->value === 'rejected') { $latestRejection = $pr->statusLogs ->firstWhere(fn ($log) => $log->to_status?->value === 'rejected'); } $cover = app(PressReleaseCoverImage::class); return [ 'pr' => $pr, 'statusLogs' => $pr->statusLogs, 'contacts' => $pr->contacts, 'latestClassification' => $latestClassification, 'latestRejection' => $latestRejection, 'coverUrl' => $cover->coverUrl($pr, 'cover'), 'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr), 'categoryName' => $pr->category?->translations->firstWhere('locale', 'de')?->name ?? $pr->category?->translations->first()?->name ?? '–', 'statusColor' => match ($pr->status->value) { 'published' => 'green', 'review' => 'yellow', 'rejected' => 'red', 'archived' => 'blue', default => 'zinc', }, ]; } }; ?>
@php $statusClass = match ($pr->status->value) { 'published' => 'ok', 'review' => 'warn', 'rejected' => 'err', default => 'hub', }; @endphp {{-- Flash-Banner ersetzt durch im Layout. --}} {{-- ============== PAGE HEADER ============== --}} {{-- ============== TITELBILD (Hero) ============== --}} {{-- Harte Obergrenze 1280x580 px: Container deckelt Breite und Seitenverhältnis, damit das Bild auf großen Screens nicht über die Detailgröße hinauswächst. --}}
{{ $pr->title }}
@if ($coverIsPlaceholder)
{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}
@else @php $adminTitleImage = $pr->images->sortByDesc('is_preview')->first(); @endphp @if ($adminTitleImage && ($adminTitleImage->copyright || $adminTitleImage->is_ai_generated)) {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
@if ($adminTitleImage->is_ai_generated) {{ __('KI-generiert') }} @endif {{ $adminTitleImage->copyright ?? __('Bild: KI-generiert') }}
@endif @endif
{{-- ============== REJECTION-HINWEIS ============== --}} @if ($pr->status === \App\Enums\PressReleaseStatus::Rejected && $latestRejection)
{{ __('Diese Pressemitteilung wurde abgelehnt') }} {{ __('Handlung erforderlich') }}
@if ($latestRejection->reason) {{ __('Begründung') }}: {{ $latestRejection->reason }} @else {{ __('Der Autor sollte den Inhalt überarbeiten und erneut einreichen.') }} @endif {{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }} @if ($latestRejection->changedBy) · {{ __('durch :name', ['name' => $latestRejection->changedBy->name]) }} @endif
@endif {{-- ============== STATUS-WORKFLOW ============== --}} @if ($pr->status === \App\Enums\PressReleaseStatus::Review)
{{ __('Status-Workflow') }} {{ __('Wartet auf Prüfung') }}

{{ __('Diese PM wartet auf eine redaktionelle Entscheidung.') }}

@if ($latestClassification && $latestClassification->reason)

{{ __('KI-Hinweis') }}: {{ $latestClassification->reason }}

@endif @if ($pr->scheduled_at)

{{ __('Geplante Veröffentlichung: :date', ['date' => $pr->scheduledAtLocal()->format('d.m.Y H:i')]) }}

@endif
{{ __('Veröffentlichen') }} {{ __('Ablehnen') }}
@endif @if ($pr->status === \App\Enums\PressReleaseStatus::Published)
{{ __('Status-Workflow') }} {{ __('Live') }}

{{ __('Veröffentlicht am') }} {{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}

@if ($pr->embargo_at && $pr->embargo_at->isFuture())

{{ __('Sperrfrist bis: :date', ['date' => $pr->embargoAtLocal()->format('d.m.Y H:i')]) }}

@endif @if ($pr->hits > 0)

{{ number_format($pr->hits, 0, ',', '.') }} {{ __('Aufrufe seit Veröffentlichung') }}

@endif
{{ __('Archivieren') }}
@endif {{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
{{ __('Zugeordnete Pressekontakte') }} @if ($pr->company) {{ __('Firma') }} @endif

{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}

@forelse ($contacts as $contact)
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
@if ($contact->email) {{ $contact->email }} @endif @if ($contact->phone) {{ $contact->phone }} @endif
@empty
{{ __('Dieser Pressemitteilung ist kein Pressekontakt zugeordnet.') }}
@endforelse
{{ __('Status & Verlauf') }} {{ $pr->status->label() }}
{{ __('Autor') }}
{{ $pr->user?->name ?? '–' }}
{{ __('Erstellt') }}
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
{{ __('Veröffentlicht') }}
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
{{ __('Aufrufe') }}
{{ number_format($pr->hits, 0, ',', '.') }}
@if ($pr->scheduled_at)
{{ __('Geplant') }}
{{ $pr->scheduledAtLocal()->format('d.m.Y H:i') }}
@endif @if ($pr->embargo_at)
{{ __('Sperrfrist bis') }}
{{ $pr->embargoAtLocal()->format('d.m.Y H:i') }}
@endif
@if ($pr->no_export)
{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}
@endif
@if ($statusLogs->isNotEmpty())
    @foreach ($statusLogs as $log)
  1. @php $logClass = match ($log->to_status?->value) { 'published' => 'ok', 'review' => 'warn', 'rejected' => 'err', default => 'hub', }; @endphp {{ $log->to_status?->label() ?? $log->to_status }} @if ($log->from_status) {{ __('von') }} {{ $log->from_status->label() }} @endif · {{ $log->created_at->format('d.m.Y H:i') }} @if ($log->changedBy) · {{ $log->changedBy->name }} @endif @if ($log->source && $log->source !== 'admin') {{ $log->source }} @endif
    @if ($log->reason)

    {{ $log->reason }}

    @endif
  2. @endforeach
@else

{{ __('Noch keine Statusänderungen protokolliert.') }}

@endif
{{-- ============== INHALT ============== --}}
{{ __('Inhalt') }}
{!! $pr->renderedText() !!}
@if ($pr->keywords || $pr->backlink_url)
@if ($pr->keywords)

{{ __('Stichwörter') }}: {{ $pr->keywords }}

@endif @if ($pr->backlink_url)

{{ __('Backlink') }}: {{ $pr->backlink_url }}

@endif
@endif
{{-- ============== BOILERPLATE-OVERRIDE ============== --}} @if ($pr->boilerplate_override)
{{ __('Eigener Abbinder (Boilerplate)') }} {{ __('Override') }}

{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}

{{ $pr->boilerplate_override }}
@endif {{-- ============== MEDIEN ============== --}} @if ($pr->images->isNotEmpty())
{{ __('Bilder') }} {{ $pr->images->count() }}
@foreach ($pr->images as $image)
{{ basename($image->path) }} @if ($image->is_preview) {{ __('Preview') }} @endif
@endforeach
@endif {{-- ANHÄNGE-ANZEIGE — TEMPORÄR DEAKTIVIERT Datei-Uploads erfordern eine vollständige Sicherheitsprüfung. Wird mit dem Anhang-Manager in einer späteren Phase wieder aktiviert. @if ($pr->attachments->isNotEmpty())
{{ __('Anhänge') }} {{ $pr->attachments->count() }}
@foreach ($pr->attachments as $attachment)
{{ $attachment->title ?: $attachment->original_name }} {{ number_format($attachment->size / 1024, 0, ',', '.') }} KB
@endforeach
@endif --}} @if($pr->status === \App\Enums\PressReleaseStatus::Review)
{{ __('Pressemitteilung veröffentlichen?') }} {{ __('Die Pressemitteilung wird öffentlich sichtbar und der Autor wird benachrichtigt.') }}
{{ __('Abbrechen') }} {{ __('Veröffentlichen') }}
{{ __('Pressemitteilung ablehnen?') }} {{ __('Die Pressemitteilung wird abgelehnt und der Autor wird benachrichtigt. Bitte begründen Sie die Ablehnung.') }}
{{ __('Begründung (an den Autor sichtbar)') }}
{{ __('Abbrechen') }} {{ __('Ablehnen') }}
@endif @if($pr->status === \App\Enums\PressReleaseStatus::Published)
{{ __('Pressemitteilung archivieren?') }} {{ __('Die Pressemitteilung bleibt intern erhalten, wird aber archiviert.') }}
{{ __('Abbrechen') }} {{ __('Archivieren') }}
@endif