presseportale/resources/views/livewire/admin/press-releases/show.blade.php
Kevin Adametz d2ba22c0cf
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
create PM v0.5
2026-05-20 19:14:39 +02:00

386 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Models\PressRelease;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component
{
#[Locked]
public int $id;
public string $rejectReason = '';
public function mount(int $id): void
{
$this->id = $id;
}
public function publish(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
try {
app(PressReleaseService::class)->publish($pr);
} catch (BlacklistViolationException $e) {
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
Flux::modal('confirm-show-publish')->close();
return;
}
session()->flash('success', __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'));
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 = '';
session()->flash('success', __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'));
Flux::modal('confirm-show-reject')->close();
}
public function archive(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->archive($pr);
session()->flash('success', __('Archiviert.'));
Flux::modal('confirm-show-archive')->close();
}
public function with(): array
{
$pr = PressRelease::withoutGlobalScopes()
->with([
'company:id,name,slug',
'category.translations',
'user:id,name',
'images',
'statusLogs.changedBy:id,name',
])
->findOrFail($this->id);
return [
'pr' => $pr,
'statusLogs' => $pr->statusLogs,
'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',
},
];
}
}; ?>
<div class="space-y-8">
@php
$statusClass = match ($pr->status->value) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
default => 'hub',
};
@endphp
@if (session('success'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="px-4 py-3 rounded-[5px] border text-[12.5px]
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
{{ session('error') }}
</div>
@endif
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Content · Pressemitteilung') }}</span>
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
<span class="badge hub">{{ $pr->portal->label() }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ $pr->title }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Firma') }}:</strong>
{{ $pr->company?->name ?? '' }}
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Kategorie') }}:</strong>
{{ $categoryName }}
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Autor') }}:</strong>
{{ $pr->user?->name ?? '' }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="pencil" href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('admin.press-releases.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
{{-- ============== STATUS-WORKFLOW ============== --}}
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
<span class="badge warn dot">{{ __('Wartet auf Prüfung') }}</span>
</div>
<div class="p-5 flex flex-wrap items-center gap-3">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
{{ __('Diese PM wartet auf Prüfung.') }}
</p>
<flux:modal.trigger name="confirm-show-publish">
<flux:button type="button" variant="primary">{{ __('Veröffentlichen') }}</flux:button>
</flux:modal.trigger>
<flux:modal.trigger name="confirm-show-reject">
<flux:button type="button" variant="danger">{{ __('Ablehnen') }}</flux:button>
</flux:modal.trigger>
</div>
</article>
@endif
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
<span class="badge ok dot">{{ __('Live') }}</span>
</div>
<div class="p-5 flex flex-wrap items-center gap-3">
@if ($pr->hits > 0)
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[200px]">
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</strong>
{{ __('Aufrufe seit Veröffentlichung') }}
</p>
@endif
<flux:modal.trigger name="confirm-show-archive">
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
</flux:modal.trigger>
</div>
</article>
@endif
{{-- ============== TEXT + SIDEBAR ============== --}}
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
</div>
<div class="p-5">
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
{!! $pr->renderedText() !!}
</div>
</div>
</article>
<aside class="space-y-4">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Details') }}</span>
</div>
<dl class="p-5 space-y-2.5 text-[12.5px]">
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Status') }}</dt>
<dd class="font-semibold text-[color:var(--color-ink)]">{{ $pr->status->label() }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Erstellt') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $pr->created_at->format('d.m.Y H:i') }}</dd>
</div>
@if ($pr->published_at)
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Veröffentlicht') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
</div>
@endif
<div class="flex justify-between gap-2">
<dt class="text-[color:var(--color-ink-3)]">{{ __('Aufrufe') }}</dt>
<dd class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits) }}</dd>
</div>
@if ($pr->keywords)
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Stichwörter') }}</dt>
<dd class="text-[color:var(--color-ink)]">{{ $pr->keywords }}</dd>
</div>
@endif
@if ($pr->backlink_url)
<div class="pt-2 border-t border-[color:var(--color-bg-rule)]">
<dt class="text-[color:var(--color-ink-3)] mb-1">{{ __('Backlink') }}</dt>
<dd class="break-all">
<a href="{{ $pr->backlink_url }}" target="_blank"
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->backlink_url }}
</a>
</dd>
</div>
@endif
@if ($pr->no_export)
<div class="mt-2 pt-2 border-t border-[color:var(--color-bg-rule)]">
<span class="badge hub">{{ __('Kein Export') }}</span>
</div>
@endif
</dl>
</article>
@if ($pr->images->isNotEmpty())
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Bilder') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $pr->images->count() }}
</span>
</div>
<div class="p-5 space-y-2">
@foreach ($pr->images as $image)
<div class="flex items-center gap-2 text-[12.5px]">
<flux:icon.photo class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
<span class="truncate text-[color:var(--color-ink-2)]">{{ basename($image->path) }}</span>
@if ($image->is_preview)
<span class="badge hub">{{ __('Preview') }}</span>
@endif
</div>
@endforeach
</div>
</article>
@endif
</aside>
</div>
{{-- ============== STATUS-VERLAUF ============== --}}
@if ($statusLogs->isNotEmpty())
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Verlauf') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $statusLogs->count() }} {{ __('Einträge') }}
</span>
</div>
<div class="p-5">
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
@foreach ($statusLogs as $log)
<li class="text-[12.5px]">
<div class="flex flex-wrap items-center gap-2">
@php
$logClass = match ($log->to_status?->value) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
default => 'hub',
};
@endphp
<span @class(['badge', $logClass])>{{ $log->to_status?->label() ?? $log->to_status }}</span>
@if ($log->from_status)
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('von') }} {{ $log->from_status->label() }}
</span>
@endif
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $log->created_at->format('d.m.Y H:i') }}
</span>
@if ($log->changedBy)
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $log->changedBy->name }}</span>
@endif
@if ($log->source !== 'admin')
<span class="badge hub">{{ $log->source }}</span>
@endif
</div>
@if ($log->reason)
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0">{{ $log->reason }}</p>
@endif
</li>
@endforeach
</ol>
</div>
</article>
@endif
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
<flux:modal name="confirm-show-publish" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung veröffentlichen?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung wird öffentlich sichtbar und der Autor wird benachrichtigt.') }}</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="publish">{{ __('Veröffentlichen') }}</flux:button>
</div>
</div>
</flux:modal>
<flux:modal name="confirm-show-reject" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung ablehnen?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung wird abgelehnt und der Autor wird benachrichtigt. Bitte begründen Sie die Ablehnung.') }}</flux:subheading>
</div>
<flux:field>
<flux:label>{{ __('Begründung (an den Autor sichtbar)') }}</flux:label>
<flux:textarea
wire:model="rejectReason"
rows="5"
placeholder="{{ __('z. B. Werbliche Sprache, fehlende Belege, doppelte Veröffentlichung…') }}"
/>
<flux:error name="rejectReason" />
</flux:field>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" wire:click="reject">{{ __('Ablehnen') }}</flux:button>
</div>
</div>
</flux:modal>
@endif
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
<flux:modal name="confirm-show-archive" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung archivieren?') }}</flux:heading>
<flux:subheading>{{ __('Die Pressemitteilung bleibt intern erhalten, wird aber archiviert.') }}</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="archive">{{ __('Archivieren') }}</flux:button>
</div>
</div>
</flux:modal>
@endif
</div>