331 lines
14 KiB
PHP
331 lines
14 KiB
PHP
<?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-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 class="flex-1">
|
||
<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>
|
||
<flux:badge color="zinc" size="sm">{{ $pr->portal->label() }}</flux:badge>
|
||
</div>
|
||
<flux:heading size="xl" class="mt-2">{{ $pr->title }}</flux:heading>
|
||
<flux:text class="mt-1 text-sm text-zinc-500">
|
||
{{ __('Firma') }}: {{ $pr->company?->name ?? '–' }} ·
|
||
{{ __('Kategorie') }}: {{ $categoryName }} ·
|
||
{{ __('Autor') }}: {{ $pr->user?->name ?? '–' }}
|
||
</flux:text>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<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>
|
||
</div>
|
||
</flux:card>
|
||
|
||
{{-- Status-Aktionen --}}
|
||
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||
<flux:card>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<flux:text weight="medium" class="text-yellow-700 dark:text-yellow-400">
|
||
{{ __('Diese PM wartet auf Prüfung.') }}
|
||
</flux:text>
|
||
<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>
|
||
</flux:card>
|
||
@endif
|
||
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||
<flux:card>
|
||
<div class="flex items-center gap-3">
|
||
<flux:modal.trigger name="confirm-show-archive">
|
||
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
@if($pr->hits > 0)
|
||
<flux:text class="text-sm text-zinc-500">{{ number_format($pr->hits) }} {{ __('Aufrufe') }}</flux:text>
|
||
@endif
|
||
</div>
|
||
</flux:card>
|
||
@endif
|
||
|
||
<div class="grid gap-6 lg:grid-cols-[1fr,280px]">
|
||
{{-- Text --}}
|
||
<flux:card>
|
||
<div class="prose prose-zinc dark:prose-invert max-w-none">
|
||
{!! nl2br(e($pr->text)) !!}
|
||
</div>
|
||
</flux:card>
|
||
|
||
{{-- Details --}}
|
||
<div class="space-y-4">
|
||
<flux:card>
|
||
<flux:heading size="sm" class="mb-3">{{ __('Details') }}</flux:heading>
|
||
<dl class="space-y-2 text-sm">
|
||
<div class="flex justify-between gap-2">
|
||
<dt class="text-zinc-500">{{ __('Status') }}</dt>
|
||
<dd class="font-medium">{{ $pr->status->label() }}</dd>
|
||
</div>
|
||
<div class="flex justify-between gap-2">
|
||
<dt class="text-zinc-500">{{ __('Erstellt') }}</dt>
|
||
<dd>{{ $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-zinc-500">{{ __('Veröffentlicht') }}</dt>
|
||
<dd>{{ $pr->published_at->format('d.m.Y H:i') }}</dd>
|
||
</div>
|
||
@endif
|
||
<div class="flex justify-between gap-2">
|
||
<dt class="text-zinc-500">{{ __('Aufrufe') }}</dt>
|
||
<dd>{{ number_format($pr->hits) }}</dd>
|
||
</div>
|
||
@if($pr->keywords)
|
||
<div>
|
||
<dt class="text-zinc-500">{{ __('Stichwörter') }}</dt>
|
||
<dd class="mt-1">{{ $pr->keywords }}</dd>
|
||
</div>
|
||
@endif
|
||
@if($pr->backlink_url)
|
||
<div>
|
||
<dt class="text-zinc-500">{{ __('Backlink') }}</dt>
|
||
<dd class="mt-1 break-all">
|
||
<a href="{{ $pr->backlink_url }}" target="_blank" class="text-blue-600 underline dark:text-blue-400">
|
||
{{ $pr->backlink_url }}
|
||
</a>
|
||
</dd>
|
||
</div>
|
||
@endif
|
||
@if($pr->no_export)
|
||
<div class="rounded bg-zinc-100 px-2 py-1 text-xs text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300">
|
||
{{ __('Kein Export') }}
|
||
</div>
|
||
@endif
|
||
</dl>
|
||
</flux:card>
|
||
|
||
@if($pr->images->isNotEmpty())
|
||
<flux:card>
|
||
<flux:heading size="sm" class="mb-3">{{ __('Bilder') }}</flux:heading>
|
||
<div class="space-y-2">
|
||
@foreach($pr->images as $image)
|
||
<div class="flex items-center gap-2 text-sm">
|
||
<flux:icon.photo class="size-4 text-zinc-400" />
|
||
<span class="truncate text-zinc-600 dark:text-zinc-400">{{ basename($image->path) }}</span>
|
||
@if($image->is_preview)
|
||
<flux:badge size="sm" color="blue">{{ __('Preview') }}</flux:badge>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</flux:card>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@if($statusLogs->isNotEmpty())
|
||
<flux:card>
|
||
<flux:heading size="sm" class="mb-3">{{ __('Status-Verlauf') }}</flux:heading>
|
||
<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() ?? $log->to_status }}</flux:badge>
|
||
@if($log->from_status)
|
||
<span class="text-xs text-zinc-500">
|
||
{{ __('von') }} {{ $log->from_status->label() }}
|
||
</span>
|
||
@endif
|
||
<span class="text-xs text-zinc-500">·</span>
|
||
<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">·</span>
|
||
<span class="text-xs text-zinc-500">{{ $log->changedBy->name }}</span>
|
||
@endif
|
||
@if($log->source !== 'admin')
|
||
<flux:badge size="xs" color="zinc">{{ $log->source }}</flux:badge>
|
||
@endif
|
||
</div>
|
||
@if($log->reason)
|
||
<p class="mt-1 text-zinc-600 dark:text-zinc-400">{{ $log->reason }}</p>
|
||
@endif
|
||
</li>
|
||
@endforeach
|
||
</ol>
|
||
</flux:card>
|
||
@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>
|