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 ============== --}}
{{ __('Admin Backend') }}
{{ __('Content · Pressemitteilung') }}
{{ $pr->status->label() }}
@if ($pr->classification)
@php
$kiBadgeClass = match ($pr->classification) {
\App\Enums\PressReleaseClassification::Green => 'ok',
\App\Enums\PressReleaseClassification::Yellow => 'warn',
\App\Enums\PressReleaseClassification::Red => 'err',
};
@endphp
{{ __('KI: :label', ['label' => $pr->classification->label()]) }}
@endif
@if (! is_null($pr->content_score) && $pr->content_tier)
@php
$tierBadge = match ($pr->content_tier) {
\App\Enums\PressReleaseContentTier::Hochwertig => 'ok',
\App\Enums\PressReleaseContentTier::Geprueft => 'hub',
\App\Enums\PressReleaseContentTier::Standard => 'muted',
};
@endphp
{{ __('Score :score · :tier', ['score' => $pr->content_score, 'tier' => $pr->content_tier->label()]) }}
@endif
{{ strtoupper($pr->language) }}
{{ $pr->portal->label() }}
{{ $pr->title }}
@if ($pr->subtitle)
{{ $pr->subtitle }}
@endif
{{ __('Firma') }}:
{{ $pr->company?->name ?? '–' }}
·
{{ __('Kategorie') }}:
{{ $categoryName }}
·
{{ __('Autor') }}:
{{ $pr->user?->name ?? '–' }}
{{ __('Bearbeiten') }}
{{ __('Zurück') }}
{{-- ============== 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. --}}
@if ($coverIsPlaceholder)
{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}
@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)
-
@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
@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