22-05-2026 Optimierung der User und Admin Panels
This commit is contained in:
parent
d2ba22c0cf
commit
e8c47b7553
73 changed files with 10282 additions and 1546 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ use App\Models\User;
|
|||
use App\Services\Admin\AdminPerformanceCache;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -157,16 +158,21 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->publish($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||||
Flux::toast(
|
||||
heading: __('Automatisch abgelehnt'),
|
||||
text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]),
|
||||
variant: 'danger',
|
||||
duration: 8000,
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung veröffentlicht.'));
|
||||
Flux::toast(text: __('Pressemitteilung veröffentlicht.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function reject(int $id): void
|
||||
|
|
@ -176,12 +182,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->reject($pr, __('Bitte überarbeiten Sie die Pressemitteilung.'));
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung abgelehnt.'));
|
||||
Flux::toast(text: __('Pressemitteilung abgelehnt.'), variant: 'warning');
|
||||
}
|
||||
|
||||
public function archive(int $id): void
|
||||
|
|
@ -191,12 +197,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
try {
|
||||
app(PressReleaseService::class)->archive($pr);
|
||||
} catch (\LogicException $e) {
|
||||
session()->flash('error', $e->getMessage());
|
||||
Flux::toast(text: $e->getMessage(), variant: 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung archiviert.'));
|
||||
Flux::toast(text: __('Pressemitteilung archiviert.'), variant: 'success');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
|
|
@ -226,7 +232,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
->when($this->companyFilter !== 'all', fn ($q) => $q->where('company_id', (int) $this->companyFilter))
|
||||
->when($this->contactFilter !== 'all', fn ($q) => $q->whereHas('contacts', fn ($contactQuery) => $contactQuery->where('contacts.id', (int) $this->contactFilter)))
|
||||
->orderBy(in_array($this->sortBy, ['title', 'status', 'portal', 'hits', 'created_at']) ? $this->sortBy : 'created_at', $this->sortDir)
|
||||
->simplePaginate(50);
|
||||
->paginate(50);
|
||||
|
||||
return [
|
||||
'pressReleases' => $query,
|
||||
|
|
@ -373,18 +379,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
}; ?>
|
||||
|
||||
<div class="space-y-8">
|
||||
@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
|
||||
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
|
|
@ -905,6 +900,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
<div class="text-[10.5px] text-[color:var(--color-ink-4)] mt-0.5">
|
||||
{{ $dateSubLabel }} · {{ $primaryDate?->format('H:i') }}
|
||||
</div>
|
||||
@if ($status === 'review' && $pr->scheduled_at && $pr->scheduled_at->isFuture())
|
||||
<div class="mt-1 inline-flex items-center gap-1 text-[10.5px] font-mono tabular-nums text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.calendar variant="micro" class="size-3" />
|
||||
<span>{{ __('geplant') }} · {{ $pr->scheduled_at->format('d.m. H:i') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($pr->embargo_at && $pr->embargo_at->isFuture())
|
||||
<div class="mt-1 inline-flex items-center gap-1 text-[10.5px] font-mono tabular-nums text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.lock-closed variant="micro" class="size-3" />
|
||||
<span>{{ __('Embargo bis') }} {{ $pr->embargo_at->format('d.m.') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
|
|
@ -1029,5 +1036,5 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class exten
|
|||
</flux:table>
|
||||
</article>
|
||||
|
||||
{{ $pressReleases->links() }}
|
||||
{{ $pressReleases->links('components.portal.pagination') }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,13 +28,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
try {
|
||||
app(PressReleaseService::class)->publish($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||||
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;
|
||||
}
|
||||
|
||||
session()->flash('success', __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'));
|
||||
Flux::toast(text: __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'), variant: 'success');
|
||||
Flux::modal('confirm-show-publish')->close();
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +54,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
|
||||
$this->rejectReason = '';
|
||||
|
||||
session()->flash('success', __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'));
|
||||
Flux::toast(text: __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'), variant: 'warning');
|
||||
Flux::modal('confirm-show-reject')->close();
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +62,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
{
|
||||
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
|
||||
app(PressReleaseService::class)->archive($pr);
|
||||
session()->flash('success', __('Archiviert.'));
|
||||
Flux::toast(text: __('Archiviert.'), variant: 'success');
|
||||
Flux::modal('confirm-show-archive')->close();
|
||||
}
|
||||
|
||||
|
|
@ -65,17 +70,31 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
{
|
||||
$pr = PressRelease::withoutGlobalScopes()
|
||||
->with([
|
||||
'company:id,name,slug',
|
||||
'company:id,name,email,phone,slug',
|
||||
'category.translations',
|
||||
'user:id,name',
|
||||
'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',
|
||||
])
|
||||
->findOrFail($this->id);
|
||||
|
||||
$latestRejection = null;
|
||||
if ($pr->status->value === 'rejected') {
|
||||
$latestRejection = $pr->statusLogs
|
||||
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
|
||||
}
|
||||
|
||||
return [
|
||||
'pr' => $pr,
|
||||
'statusLogs' => $pr->statusLogs,
|
||||
'contacts' => $pr->contacts,
|
||||
'latestRejection' => $latestRejection,
|
||||
'categoryName' => $pr->category?->translations->firstWhere('locale', 'de')?->name
|
||||
?? $pr->category?->translations->first()?->name
|
||||
?? '–',
|
||||
|
|
@ -100,18 +119,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
};
|
||||
@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
|
||||
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
|
||||
|
||||
{{-- ============== PAGE HEADER ============== --}}
|
||||
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
|
||||
|
|
@ -126,6 +134,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
{{ $pr->title }}
|
||||
</h1>
|
||||
@if ($pr->subtitle)
|
||||
<p class="text-[18px] font-medium tracking-[-0.2px] leading-[1.35] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||||
{{ $pr->subtitle }}
|
||||
</p>
|
||||
@endif
|
||||
<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 ?? '–' }}
|
||||
|
|
@ -148,39 +161,98 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== REJECTION-HINWEIS ============== --}}
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Rejected && $latestRejection)
|
||||
<article class="panel" style="border-color:var(--color-err); border-left-width:3px;">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Diese Pressemitteilung wurde abgelehnt') }}</span>
|
||||
<span class="badge err dot">{{ __('Handlung erforderlich') }}</span>
|
||||
</div>
|
||||
<div class="p-5 flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-err-soft)] border border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||||
<flux:icon.exclamation-triangle class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 text-[13px] text-[color:var(--color-ink-2)]">
|
||||
@if ($latestRejection->reason)
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Begründung') }}:</strong>
|
||||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||||
@else
|
||||
{{ __('Der Autor sollte den Inhalt überarbeiten und erneut einreichen.') }}
|
||||
@endif
|
||||
<span class="mt-2 block text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||||
@if ($latestRejection->changedBy)
|
||||
· {{ __('durch :name', ['name' => $latestRejection->changedBy->name]) }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== STATUS-WORKFLOW ============== --}}
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||||
<article class="panel">
|
||||
<article class="panel" style="border-color:var(--color-warn); border-left-width:3px;">
|
||||
<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 class="p-5 flex flex-wrap items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
|
||||
<flux:icon.clock class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||||
<p class="m-0">{{ __('Diese PM wartet auf eine redaktionelle Entscheidung.') }}</p>
|
||||
@if ($pr->scheduled_at)
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.calendar variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||||
{{ __('Geplante Veröffentlichung: :date', ['date' => $pr->scheduled_at->format('d.m.Y H:i')]) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<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>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||||
<article class="panel">
|
||||
<article class="panel" style="border-color:var(--color-ok); border-left-width:3px;">
|
||||
<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') }}
|
||||
<div class="p-5 flex flex-wrap items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||||
bg-[color:var(--color-ok-soft)] border border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||||
<flux:icon.check-circle class="size-[18px]" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||||
<p class="m-0">
|
||||
{{ __('Veröffentlicht am') }}
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}</strong>
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->embargo_at && $pr->embargo_at->isFuture())
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.lock-closed variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||||
{{ __('Sperrfrist bis: :date', ['date' => $pr->embargo_at->format('d.m.Y H:i')]) }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->hits > 0)
|
||||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits, 0, ',', '.') }}</strong>
|
||||
{{ __('Aufrufe seit Veröffentlichung') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<flux:modal.trigger name="confirm-show-archive">
|
||||
<flux:button type="button" variant="ghost">{{ __('Archivieren') }}</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
|
@ -188,142 +260,256 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== TEXT + SIDEBAR ============== --}}
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr,300px]">
|
||||
{{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
|
||||
<div class="grid gap-6 xl:grid-cols-2">
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||||
<span class="section-eyebrow">{{ __('Zugeordnete Pressekontakte') }}</span>
|
||||
@if ($pr->company)
|
||||
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate>
|
||||
{{ __('Firma') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none text-[color:var(--color-ink)]">
|
||||
{!! $pr->renderedText() !!}
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-4">
|
||||
{{ __('Kontakte, die dieser Pressemitteilung zugeordnet sind.') }}
|
||||
</p>
|
||||
<div class="space-y-2">
|
||||
@forelse ($contacts as $contact)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||||
{{ trim(($contact->first_name ?? '').' '.($contact->last_name ?? '')) ?: __('Kontakt ohne Name') }}
|
||||
</div>
|
||||
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||||
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
@if ($contact->email)
|
||||
<a href="mailto:{{ $contact->email }}"
|
||||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||||
{{ $contact->email }}
|
||||
</a>
|
||||
@endif
|
||||
@if ($contact->phone)
|
||||
<span>{{ $contact->phone }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ __('Dieser Pressemitteilung ist kein Pressekontakt zugeordnet.') }}
|
||||
</div>
|
||||
@endforelse
|
||||
</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>
|
||||
<span class="section-eyebrow">{{ __('Status & Verlauf') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</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 class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Autor') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1 truncate">
|
||||
{{ $pr->user?->name ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Erstellt') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Veröffentlicht') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aufrufe') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ number_format($pr->hits, 0, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
@if ($pr->scheduled_at)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Geplant') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->scheduled_at->format('d.m.Y H:i') }}
|
||||
</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>
|
||||
@endif
|
||||
@if ($pr->embargo_at)
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Sperrfrist bis') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->embargo_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($pr->no_export)
|
||||
<div class="mt-3 flex items-center gap-2 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.no-symbol variant="micro" class="size-3.5" />
|
||||
<span>{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="my-4 border-t border-[color:var(--color-bg-rule)]"></div>
|
||||
|
||||
@if ($statusLogs->isNotEmpty())
|
||||
<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 && $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 whitespace-pre-line">{{ $log->reason }}</p>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
@else
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{{-- ============== INHALT ============== --}}
|
||||
<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>
|
||||
|
||||
@if ($pr->keywords || $pr->backlink_url)
|
||||
<div class="mt-6 space-y-2 border-t border-[color:var(--color-bg-rule)] pt-4 text-[12.5px] text-[color:var(--color-ink-2)]">
|
||||
@if ($pr->keywords)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Stichwörter') }}:</strong>
|
||||
{{ $pr->keywords }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($pr->backlink_url)
|
||||
<p class="m-0">
|
||||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Backlink') }}:</strong>
|
||||
<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>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{-- ============== BOILERPLATE-OVERRIDE ============== --}}
|
||||
@if ($pr->boilerplate_override)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Eigener Abbinder (Boilerplate)') }}</span>
|
||||
<span class="badge hub">{{ __('Override') }}</span>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-3">
|
||||
{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}
|
||||
</p>
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[13px] leading-[1.6] text-[color:var(--color-ink-2)] whitespace-pre-line">
|
||||
{{ $pr->boilerplate_override }}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== MEDIEN ============== --}}
|
||||
@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
|
||||
|
||||
{{-- 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())
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Anhänge') }}</span>
|
||||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||||
{{ $pr->attachments->count() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 space-y-2">
|
||||
@foreach ($pr->attachments as $attachment)
|
||||
<div class="flex items-center gap-2 text-[12.5px]">
|
||||
<flux:icon.paper-clip class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||||
<span class="truncate text-[color:var(--color-ink-2)] flex-1">
|
||||
{{ $attachment->title ?: $attachment->original_name }}
|
||||
</span>
|
||||
<span class="text-[11px] text-[color:var(--color-ink-3)] flex-shrink-0">
|
||||
{{ number_format($attachment->size / 1024, 0, ',', '.') }} KB
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue