presseportale/resources/views/livewire/customer/press-releases/show.blade.php
Kevin Adametz 5b8bdf4182
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
12-05-2026 Frontend dev
2026-05-12 18:32:33 +02:00

323 lines
15 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\Enums\PressReleaseStatus;
use App\Models\PressRelease;
use App\Services\Auth\MagicLinkGenerator;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseService;
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 $shareUrl = null;
public ?string $shareExpiresAt = null;
public function mount(int $id): void
{
$this->id = $id;
$pr = $this->getMyPR();
$this->authorize('view', $pr);
}
public function submitForReview(): void
{
$pr = $this->getMyPR();
$this->authorize('submitForReview', $pr);
try {
app(PressReleaseService::class)->submitForReview($pr);
} catch (BlacklistViolationException $e) {
session()->flash('error', __('Pressemitteilung wurde automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
return;
}
session()->flash('success', __('Pressemitteilung zur Prüfung eingereicht.'));
}
public function generateShareLink(MagicLinkGenerator $generator): void
{
$pr = $this->getMyPR();
$this->authorize('view', $pr);
$share = $generator->createPressReleaseShareLink($pr, auth()->user());
$this->shareUrl = $share['url'];
$this->shareExpiresAt = $share['expires_at']->format('d.m.Y H:i');
session()->flash('success', __('Vorschau-Link wurde erzeugt.'));
}
public function with(): array
{
$pr = $this->getMyPR();
$this->authorize('view', $pr);
$categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '';
$latestRejection = null;
if ($pr->status->value === 'rejected') {
$latestRejection = $pr->statusLogs
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
}
return [
'pr' => $pr,
'categoryName' => $categoryName,
'canEdit' => auth()->user()->can('update', $pr)
&& in_array($pr->status->value, ['draft', 'rejected']),
'latestRejection' => $latestRejection,
'contacts' => $pr->contacts,
'statusLogs' => $pr->statusLogs,
'statusColor' => match($pr->status->value) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
},
];
}
private function getMyPR(): PressRelease
{
return PressRelease::withoutGlobalScopes()
->where('user_id', auth()->id())
->with([
'company:id,name,email,phone',
'category.translations',
'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,email',
])
->findOrFail($this->id);
}
}; ?>
<div class="space-y-6">
@if(session('success'))
<div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-300">
{{ session('success') }}
</div>
@endif
<flux:card>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="flex items-center gap-2">
<flux:badge :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
<flux:badge color="zinc" size="sm">{{ strtoupper($pr->language) }}</flux:badge>
</div>
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
<flux:text class="mt-1 text-sm text-zinc-500">
{{ $pr->company?->name ?? '' }} · {{ $categoryName }} · {{ $pr->created_at->format('d.m.Y') }}
</flux:text>
</div>
<div class="flex gap-2">
@if($canEdit)
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@endif
<flux:button variant="ghost" icon="link" wire:click="generateShareLink">
{{ __('Vorschau-Link') }}
</flux:button>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</div>
@if($shareUrl)
<div class="mt-4 rounded-md border border-emerald-300 bg-emerald-50 p-4 dark:border-emerald-700 dark:bg-emerald-900/20">
<flux:heading size="sm" class="mb-2">{{ __('Öffentlicher Vorschau-Link erstellt') }}</flux:heading>
<flux:text class="mb-2 text-xs text-zinc-500">{{ __('Gültig bis :date.', ['date' => $shareExpiresAt]) }}</flux:text>
<flux:input readonly :value="$shareUrl" />
</div>
@endif
</flux:card>
@if($pr->status === PressReleaseStatus::Rejected && $latestRejection)
<flux:callout color="red" icon="exclamation-triangle">
<flux:callout.heading>{{ __('Diese Pressemitteilung wurde abgelehnt') }}</flux:callout.heading>
<flux:callout.text>
@if($latestRejection->reason)
<strong>{{ __('Begründung') }}:</strong>
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
@else
{{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }}
@endif
<span class="mt-2 block text-xs text-red-700/70 dark:text-red-300/70">
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
</span>
</flux:callout.text>
</flux:callout>
@endif
@if($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
<flux:card>
<div class="flex flex-wrap items-center justify-between gap-3">
<flux:text class="text-sm text-zinc-500">
{{ $pr->status === PressReleaseStatus::Rejected
? __('Sie können den Text bearbeiten und erneut zur Prüfung einreichen.')
: __('Reichen Sie den Entwurf ein, sobald er vollständig ist.') }}
</flux:text>
<div class="flex items-center gap-2">
@if($canEdit)
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@endif
<flux:button type="button" variant="primary" wire:click="submitForReview"
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}">
{{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }}
</flux:button>
</div>
</div>
</flux:card>
@endif
@if($pr->status === PressReleaseStatus::Review)
<flux:callout color="yellow" icon="clock">
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
</flux:callout>
@endif
<div class="grid gap-6 xl:grid-cols-2">
<flux:card>
<div class="mb-4 flex items-center justify-between gap-3">
<div>
<flux:heading size="lg">{{ __('Zugeordnete Pressekontakte') }}</flux:heading>
<flux:text class="text-sm text-zinc-500">
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
</flux:text>
</div>
@if($pr->company)
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate>
{{ __('Firma') }}
</flux:button>
@endif
</div>
<div class="space-y-3">
@forelse($contacts as $contact)
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text weight="semibold">
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
</flux:text>
<flux:text class="text-sm text-zinc-500">{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}</flux:text>
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-zinc-500">
@if($contact->email)
<a href="mailto:{{ $contact->email }}" class="text-blue-600 hover:underline dark:text-blue-400">{{ $contact->email }}</a>
@endif
@if($contact->phone)
<span>{{ $contact->phone }}</span>
@endif
</div>
</div>
@empty
<div class="rounded-lg border border-dashed border-zinc-200 p-4 text-sm text-zinc-500 dark:border-zinc-700">
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }}
@if($pr->company)
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate class="font-medium text-blue-600 hover:underline dark:text-blue-400">
{{ __('Kontakte in der Firma prüfen.') }}
</a>
@endif
</div>
@endforelse
</div>
</flux:card>
<flux:card>
<flux:heading size="lg" class="mb-4">{{ __('Status & Verlauf') }}</flux:heading>
<div class="grid gap-3 sm:grid-cols-2">
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Aktueller Status') }}</flux:text>
<flux:badge class="mt-1" :color="$statusColor">{{ $pr->status->label() }}</flux:badge>
</div>
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Erstellt') }}</flux:text>
<flux:text weight="semibold">{{ $pr->created_at?->format('d.m.Y H:i') ?? '' }}</flux:text>
</div>
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Veröffentlicht') }}</flux:text>
<flux:text weight="semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '' }}</flux:text>
</div>
<div class="rounded-lg bg-zinc-50 p-3 dark:bg-zinc-900">
<flux:text class="text-xs text-zinc-500">{{ __('Aufrufe') }}</flux:text>
<flux:text weight="semibold">{{ number_format($pr->hits, 0, ',', '.') }}</flux:text>
</div>
</div>
<flux:separator class="my-4" />
@if($statusLogs->isNotEmpty())
<ol class="space-y-3 border-s border-zinc-200 ps-4 dark:border-zinc-700">
@foreach($statusLogs as $log)
<li class="text-sm">
<div class="flex flex-wrap items-center gap-2">
@php
$color = match($log->to_status?->value) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
};
@endphp
<flux:badge size="sm" :color="$color">{{ $log->to_status?->label() }}</flux:badge>
<span class="text-xs text-zinc-500">
{{ $log->created_at->format('d.m.Y H:i') }}
</span>
@if($log->changedBy)
<span class="text-xs text-zinc-500">
{{ __('durch :name', ['name' => $log->changedBy->name]) }}
</span>
@endif
</div>
@if($log->reason)
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
@endif
</li>
@endforeach
</ol>
@else
<flux:text class="text-sm text-zinc-500">
{{ __('Noch keine Statusänderungen protokolliert.') }}
</flux:text>
@endif
</flux:card>
</div>
<flux:card>
<div class="prose prose-zinc dark:prose-invert max-w-none">
{!! nl2br(e($pr->text)) !!}
</div>
@if($pr->keywords || $pr->backlink_url)
<div class="mt-6 space-y-2 border-t border-zinc-200 pt-4 text-sm text-zinc-500 dark:border-zinc-700">
@if($pr->keywords)
<p><strong>{{ __('Stichwörter') }}:</strong> {{ $pr->keywords }}</p>
@endif
@if($pr->backlink_url)
<p><strong>{{ __('Backlink') }}:</strong>
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline">{{ $pr->backlink_url }}</a>
</p>
@endif
</div>
@endif
</flux:card>
</div>