386 lines
18 KiB
PHP
386 lines
18 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-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>
|