719 lines
31 KiB
PHP
719 lines
31 KiB
PHP
<?php
|
||
|
||
use App\Enums\Portal;
|
||
use App\Enums\PressReleaseStatus;
|
||
use App\Models\Category;
|
||
use App\Models\Company;
|
||
use App\Models\Contact;
|
||
use App\Models\PressRelease;
|
||
use App\Models\User;
|
||
use App\Services\Admin\AdminPerformanceCache;
|
||
use App\Services\PressRelease\BlacklistViolationException;
|
||
use App\Services\PressRelease\PressReleaseService;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Attributes\Url;
|
||
use Livewire\Volt\Component;
|
||
use Livewire\WithPagination;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Pressemitteilungen')] class extends Component
|
||
{
|
||
use WithPagination;
|
||
|
||
public string $search = '';
|
||
|
||
#[Url(as: 'status', except: 'all')]
|
||
public string $statusFilter = 'all';
|
||
|
||
public string $portalFilter = 'all';
|
||
|
||
public string $languageFilter = 'all';
|
||
|
||
#[Url(as: 'category', except: 'all')]
|
||
public string $categoryFilter = 'all';
|
||
|
||
#[Url(as: 'user', except: 'all')]
|
||
public string $userFilter = 'all';
|
||
|
||
#[Url(as: 'company', except: 'all')]
|
||
public string $companyFilter = 'all';
|
||
|
||
#[Url(as: 'contact', except: 'all')]
|
||
public string $contactFilter = 'all';
|
||
|
||
public string $userLookup = '';
|
||
|
||
public string $companyLookup = '';
|
||
|
||
public string $contactLookup = '';
|
||
|
||
public string $sortBy = 'created_at';
|
||
|
||
public string $sortDir = 'desc';
|
||
|
||
public function sort(string $column): void
|
||
{
|
||
if ($this->sortBy === $column) {
|
||
$this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
$this->sortBy = $column;
|
||
$this->sortDir = 'asc';
|
||
}
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedStatusFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedPortalFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedLanguageFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedCategoryFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedUserFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedCompanyFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedContactFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function clearUserFilter(): void
|
||
{
|
||
$this->userFilter = 'all';
|
||
$this->userLookup = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function clearCompanyFilter(): void
|
||
{
|
||
$this->companyFilter = 'all';
|
||
$this->companyLookup = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function clearContactFilter(): void
|
||
{
|
||
$this->contactFilter = 'all';
|
||
$this->contactLookup = '';
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function resetEntityFilters(): void
|
||
{
|
||
$this->clearUserFilter();
|
||
$this->clearCompanyFilter();
|
||
$this->clearContactFilter();
|
||
}
|
||
|
||
public function publish(int $id): void
|
||
{
|
||
$pr = PressRelease::withoutGlobalScopes()->findOrFail($id);
|
||
|
||
try {
|
||
app(PressReleaseService::class)->publish($pr);
|
||
} catch (BlacklistViolationException $e) {
|
||
session()->flash('error', __('Automatisch abgelehnt: unzulässiges Wort ":word".', ['word' => $e->word]));
|
||
|
||
return;
|
||
} catch (\LogicException $e) {
|
||
session()->flash('error', $e->getMessage());
|
||
|
||
return;
|
||
}
|
||
|
||
session()->flash('success', __('Pressemitteilung veröffentlicht.'));
|
||
}
|
||
|
||
public function reject(int $id): void
|
||
{
|
||
$pr = PressRelease::withoutGlobalScopes()->findOrFail($id);
|
||
|
||
try {
|
||
app(PressReleaseService::class)->reject($pr, __('Bitte überarbeiten Sie die Pressemitteilung.'));
|
||
} catch (\LogicException $e) {
|
||
session()->flash('error', $e->getMessage());
|
||
|
||
return;
|
||
}
|
||
|
||
session()->flash('success', __('Pressemitteilung abgelehnt.'));
|
||
}
|
||
|
||
public function archive(int $id): void
|
||
{
|
||
$pr = PressRelease::withoutGlobalScopes()->findOrFail($id);
|
||
|
||
try {
|
||
app(PressReleaseService::class)->archive($pr);
|
||
} catch (\LogicException $e) {
|
||
session()->flash('error', $e->getMessage());
|
||
|
||
return;
|
||
}
|
||
|
||
session()->flash('success', __('Pressemitteilung archiviert.'));
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$query = PressRelease::withoutGlobalScopes()
|
||
->with(['company:id,name', 'category.translations', 'user:id,name'])
|
||
->when(filled($this->search), function ($q): void {
|
||
$term = trim($this->search);
|
||
$q->where(function ($q) use ($term): void {
|
||
if ($this->supportsFullTextSearch($term)) {
|
||
$q->whereFullText(['title', 'keywords'], $term)
|
||
->orWhereHas('company', fn ($q) => $q->whereFullText(['name', 'email', 'slug'], $term));
|
||
|
||
return;
|
||
}
|
||
|
||
$q->where('title', 'like', '%'.$term.'%')
|
||
->orWhere('keywords', 'like', '%'.$term.'%')
|
||
->orWhereHas('company', fn ($q) => $q->where('name', 'like', '%'.$term.'%'));
|
||
});
|
||
})
|
||
->when($this->statusFilter !== 'all', fn ($q) => $q->where('status', $this->statusFilter))
|
||
->when($this->portalFilter !== 'all', fn ($q) => $q->where('portal', $this->portalFilter))
|
||
->when($this->languageFilter !== 'all', fn ($q) => $q->where('language', $this->languageFilter))
|
||
->when($this->categoryFilter !== 'all', fn ($q) => $q->where('category_id', (int) $this->categoryFilter))
|
||
->when($this->userFilter !== 'all', fn ($q) => $q->where('user_id', (int) $this->userFilter))
|
||
->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);
|
||
|
||
return [
|
||
'pressReleases' => $query,
|
||
'stats' => $this->pressReleaseStats(),
|
||
'statusOptions' => PressReleaseStatus::cases(),
|
||
'portalOptions' => Portal::cases(),
|
||
'categoryOptions' => $this->categoryOptions(),
|
||
'userLookupResults' => $this->userLookupResults(),
|
||
'companyLookupResults' => $this->companyLookupResults(),
|
||
'contactLookupResults' => $this->contactLookupResults(),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array{total: int, published: int, review: int, draft: int}
|
||
*/
|
||
private function pressReleaseStats(): array
|
||
{
|
||
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::PressReleaseStats, AdminPerformanceCache::StatsTtl, function (): array {
|
||
$stats = PressRelease::withoutGlobalScopes()
|
||
->toBase()
|
||
->selectRaw('COUNT(*) as total')
|
||
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as published', [PressReleaseStatus::Published->value])
|
||
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as review', [PressReleaseStatus::Review->value])
|
||
->selectRaw('SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as draft', [PressReleaseStatus::Draft->value])
|
||
->first();
|
||
|
||
return [
|
||
'total' => (int) ($stats->total ?? 0),
|
||
'published' => (int) ($stats->published ?? 0),
|
||
'review' => (int) ($stats->review ?? 0),
|
||
'draft' => (int) ($stats->draft ?? 0),
|
||
];
|
||
});
|
||
}
|
||
|
||
private function categoryOptions()
|
||
{
|
||
return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::PressReleaseCategoryOptions, AdminPerformanceCache::OptionsTtl, fn () => Category::query()
|
||
->select(['id', 'is_active'])
|
||
->with(['translations:id,category_id,locale,name,slug'])
|
||
->where('is_active', true)
|
||
->orderBy('id')
|
||
->get());
|
||
}
|
||
|
||
private function userLookupResults()
|
||
{
|
||
$term = trim($this->userLookup);
|
||
|
||
if ($term === '' && $this->userFilter === 'all') {
|
||
return collect();
|
||
}
|
||
|
||
return User::query()
|
||
->select(['id', 'name', 'email'])
|
||
->where(function ($query) use ($term): void {
|
||
if ($this->userFilter !== 'all') {
|
||
$query->where('id', (int) $this->userFilter);
|
||
}
|
||
|
||
if ($term !== '') {
|
||
$query->orWhere(function ($searchQuery) use ($term): void {
|
||
$searchQuery
|
||
->where('name', 'like', '%'.$term.'%')
|
||
->orWhere('email', 'like', '%'.$term.'%');
|
||
});
|
||
}
|
||
})
|
||
->orderBy('name')
|
||
->limit(20)
|
||
->get();
|
||
}
|
||
|
||
private function companyLookupResults()
|
||
{
|
||
$term = trim($this->companyLookup);
|
||
|
||
if ($term === '' && $this->companyFilter === 'all') {
|
||
return collect();
|
||
}
|
||
|
||
return Company::withoutGlobalScopes()
|
||
->select(['id', 'name', 'slug', 'email'])
|
||
->where(function ($query) use ($term): void {
|
||
if ($this->companyFilter !== 'all') {
|
||
$query->where('id', (int) $this->companyFilter);
|
||
}
|
||
|
||
if ($term !== '') {
|
||
$query->orWhere(function ($searchQuery) use ($term): void {
|
||
$searchQuery
|
||
->where('name', 'like', '%'.$term.'%')
|
||
->orWhere('slug', 'like', '%'.$term.'%')
|
||
->orWhere('email', 'like', '%'.$term.'%');
|
||
});
|
||
}
|
||
})
|
||
->orderBy('name')
|
||
->limit(20)
|
||
->get();
|
||
}
|
||
|
||
private function contactLookupResults()
|
||
{
|
||
$term = trim($this->contactLookup);
|
||
|
||
if ($term === '' && $this->contactFilter === 'all') {
|
||
return collect();
|
||
}
|
||
|
||
return Contact::withoutGlobalScopes()
|
||
->select(['id', 'company_id', 'first_name', 'last_name', 'email'])
|
||
->with('company:id,name')
|
||
->where(function ($query) use ($term): void {
|
||
if ($this->contactFilter !== 'all') {
|
||
$query->where('id', (int) $this->contactFilter);
|
||
}
|
||
|
||
if ($term !== '') {
|
||
$query->orWhere(function ($searchQuery) use ($term): void {
|
||
$searchQuery
|
||
->where('first_name', 'like', '%'.$term.'%')
|
||
->orWhere('last_name', 'like', '%'.$term.'%')
|
||
->orWhere('email', 'like', '%'.$term.'%');
|
||
});
|
||
}
|
||
})
|
||
->orderBy('last_name')
|
||
->orderBy('first_name')
|
||
->limit(20)
|
||
->get();
|
||
}
|
||
|
||
private function supportsFullTextSearch(string $term): bool
|
||
{
|
||
return mb_strlen($term) >= 3
|
||
&& in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
|
||
}
|
||
}; ?>
|
||
|
||
<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
|
||
|
||
{{-- Statistiken --}}
|
||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||
<flux:card>
|
||
<flux:text class="text-xs text-zinc-500">{{ __('Gesamt') }}</flux:text>
|
||
<flux:text size="xl" weight="bold">{{ $stats['total'] }}</flux:text>
|
||
</flux:card>
|
||
<flux:card>
|
||
<flux:text class="text-xs text-green-600">{{ __('Veröffentlicht') }}</flux:text>
|
||
<flux:text size="xl" weight="bold">{{ $stats['published'] }}</flux:text>
|
||
</flux:card>
|
||
<flux:card>
|
||
<flux:text class="text-xs text-yellow-600">{{ __('In Prüfung') }}</flux:text>
|
||
<flux:text size="xl" weight="bold">{{ $stats['review'] }}</flux:text>
|
||
</flux:card>
|
||
<flux:card>
|
||
<flux:text class="text-xs text-zinc-500">{{ __('Entwürfe') }}</flux:text>
|
||
<flux:text size="xl" weight="bold">{{ $stats['draft'] }}</flux:text>
|
||
</flux:card>
|
||
</div>
|
||
|
||
<div class="flex justify-end">
|
||
<flux:button icon="plus" variant="primary" href="{{ route('admin.press-releases.create') }}" wire:navigate>
|
||
{{ __('Neue PM') }}
|
||
</flux:button>
|
||
</div>
|
||
|
||
{{-- Filter --}}
|
||
<flux:card>
|
||
<div class="flex flex-col gap-3">
|
||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-6">
|
||
<flux:input
|
||
wire:model.live.debounce.300ms="search"
|
||
placeholder="{{ __('Titel, Stichwort, Firma…') }}"
|
||
icon="magnifying-glass"
|
||
class="lg:col-span-2"
|
||
/>
|
||
|
||
<flux:select wire:model.live="statusFilter" class="w-full">
|
||
<option value="all">{{ __('Alle Status') }}</option>
|
||
@foreach ($statusOptions as $s)
|
||
<option value="{{ $s->value }}">{{ $s->label() }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
|
||
<flux:select wire:model.live="portalFilter" class="w-full">
|
||
<option value="all">{{ __('Alle Portale') }}</option>
|
||
@foreach ($portalOptions as $p)
|
||
@if ($p !== \App\Enums\Portal::Both)
|
||
<option value="{{ $p->value }}">{{ $p->label() }}</option>
|
||
@endif
|
||
@endforeach
|
||
</flux:select>
|
||
|
||
<flux:select wire:model.live="languageFilter" class="w-full">
|
||
<option value="all">{{ __('Alle Sprachen') }}</option>
|
||
<option value="de">DE</option>
|
||
<option value="en">EN</option>
|
||
</flux:select>
|
||
|
||
<flux:select wire:model.live="categoryFilter" class="w-full">
|
||
<option value="all">{{ __('Alle Kategorien') }}</option>
|
||
@foreach ($categoryOptions as $categoryOption)
|
||
@php($categoryName = $categoryOption->translations->firstWhere('locale', 'de')?->name ?? '#' . $categoryOption->id)
|
||
<option value="{{ $categoryOption->id }}">{{ $categoryName }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
</div>
|
||
|
||
<div class="grid gap-3 lg:grid-cols-3">
|
||
<div class="flex gap-2">
|
||
<flux:select
|
||
wire:model.live="userFilter"
|
||
variant="combobox"
|
||
:filter="false"
|
||
placeholder="{{ __('User suchen…') }}"
|
||
class="min-w-0 flex-1"
|
||
>
|
||
<x-slot name="input">
|
||
<flux:select.input
|
||
wire:model.live.debounce.300ms="userLookup"
|
||
placeholder="{{ __('User suchen…') }}"
|
||
/>
|
||
</x-slot>
|
||
|
||
<flux:select.option value="all">{{ __('Alle User') }}</flux:select.option>
|
||
@foreach($userLookupResults as $userOption)
|
||
<flux:select.option :value="$userOption->id" wire:key="pm-user-{{ $userOption->id }}">
|
||
{{ $userOption->name }}
|
||
<span class="ml-1 text-zinc-400">· {{ $userOption->email }}</span>
|
||
</flux:select.option>
|
||
@endforeach
|
||
|
||
<x-slot name="empty">
|
||
<flux:select.option.empty>
|
||
{{ blank(trim($userLookup)) ? __('Zum Laden Usernamen oder E-Mail eingeben.') : __('Kein User gefunden.') }}
|
||
</flux:select.option.empty>
|
||
</x-slot>
|
||
</flux:select>
|
||
|
||
<flux:button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon="x-mark"
|
||
wire:click="clearUserFilter"
|
||
title="{{ __('Usersuche zurücksetzen') }}"
|
||
/>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<flux:select
|
||
wire:model.live="companyFilter"
|
||
variant="combobox"
|
||
:filter="false"
|
||
placeholder="{{ __('Firma suchen…') }}"
|
||
class="min-w-0 flex-1"
|
||
>
|
||
<x-slot name="input">
|
||
<flux:select.input
|
||
wire:model.live.debounce.300ms="companyLookup"
|
||
placeholder="{{ __('Firma, Slug oder E-Mail…') }}"
|
||
/>
|
||
</x-slot>
|
||
|
||
<flux:select.option value="all">{{ __('Alle Firmen') }}</flux:select.option>
|
||
@foreach($companyLookupResults as $companyOption)
|
||
<flux:select.option :value="$companyOption->id" wire:key="pm-company-{{ $companyOption->id }}">
|
||
{{ $companyOption->name }}
|
||
@if($companyOption->email)<span class="ml-1 text-zinc-400">· {{ $companyOption->email }}</span>@endif
|
||
</flux:select.option>
|
||
@endforeach
|
||
|
||
<x-slot name="empty">
|
||
<flux:select.option.empty>
|
||
{{ blank(trim($companyLookup)) ? __('Zum Laden Firmennamen, Slug oder E-Mail eingeben.') : __('Keine Firma gefunden.') }}
|
||
</flux:select.option.empty>
|
||
</x-slot>
|
||
</flux:select>
|
||
|
||
<flux:button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon="x-mark"
|
||
wire:click="clearCompanyFilter"
|
||
title="{{ __('Firmensuche zurücksetzen') }}"
|
||
/>
|
||
</div>
|
||
|
||
<div class="flex gap-2">
|
||
<flux:select
|
||
wire:model.live="contactFilter"
|
||
variant="combobox"
|
||
:filter="false"
|
||
placeholder="{{ __('Kontakt suchen…') }}"
|
||
class="min-w-0 flex-1"
|
||
>
|
||
<x-slot name="input">
|
||
<flux:select.input
|
||
wire:model.live.debounce.300ms="contactLookup"
|
||
placeholder="{{ __('Kontakt oder E-Mail…') }}"
|
||
/>
|
||
</x-slot>
|
||
|
||
<flux:select.option value="all">{{ __('Alle Kontakte') }}</flux:select.option>
|
||
@foreach($contactLookupResults as $contactOption)
|
||
@php($contactName = trim(($contactOption->first_name ?? '').' '.($contactOption->last_name ?? '')) ?: __('Kontakt ohne Name'))
|
||
<flux:select.option :value="$contactOption->id" wire:key="pm-contact-{{ $contactOption->id }}">
|
||
{{ $contactName }}
|
||
<span class="ml-1 text-zinc-400">
|
||
@if($contactOption->email)· {{ $contactOption->email }} @endif
|
||
· {{ $contactOption->company?->name ?? __('Unbekannte Firma') }}
|
||
</span>
|
||
</flux:select.option>
|
||
@endforeach
|
||
|
||
<x-slot name="empty">
|
||
<flux:select.option.empty>
|
||
{{ blank(trim($contactLookup)) ? __('Zum Laden Kontaktname oder E-Mail eingeben.') : __('Kein Kontakt gefunden.') }}
|
||
</flux:select.option.empty>
|
||
</x-slot>
|
||
</flux:select>
|
||
|
||
<flux:button
|
||
type="button"
|
||
size="sm"
|
||
variant="ghost"
|
||
icon="x-mark"
|
||
wire:click="clearContactFilter"
|
||
title="{{ __('Kontaktsuche zurücksetzen') }}"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</flux:card>
|
||
|
||
{{-- Tabelle --}}
|
||
<flux:card class="overflow-hidden">
|
||
<flux:table>
|
||
<flux:table.columns>
|
||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||
|
||
<flux:table.column sortable :sorted="$sortBy === 'title'" :direction="$sortDir"
|
||
wire:click="sort('title')">{{ __('Titel') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'created_at'" :direction="$sortDir"
|
||
wire:click="sort('created_at')">{{ __('Erstellt') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'status'" :direction="$sortDir"
|
||
wire:click="sort('status')">{{ __('Status') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'portal'" :direction="$sortDir"
|
||
wire:click="sort('portal')">{{ __('Portal') }}</flux:table.column>
|
||
<flux:table.column sortable :sorted="$sortBy === 'hits'" :direction="$sortDir"
|
||
wire:click="sort('hits')">
|
||
{{ __('Hits') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Aktionen') }}</flux:table.column>
|
||
|
||
</flux:table.columns>
|
||
|
||
@forelse($pressReleases as $pr)
|
||
<flux:table.row wire:key="{{ $pr->id }}">
|
||
<flux:table.cell>
|
||
<div class="flex items-center gap-1">
|
||
<flux:button size="sm" variant="ghost" icon="eye"
|
||
href="{{ route('admin.press-releases.show', $pr->id) }}" wire:navigate />
|
||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||
href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate />
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="max-w-xs">
|
||
<p class="truncate font-medium">{{ $pr->title ?? '–' }}</p>
|
||
<p class="text-sm truncate text-zinc-400">
|
||
{{ $pr->company?->name ?? '–' . ' | ' . strtoupper($pr->language) }}
|
||
</p>
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:text class="text-sm text-zinc-500">{{ $pr->created_at->format('d.m.Y H:i') }}</flux:text>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@php($categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '–')
|
||
<div class="max-w-48">
|
||
<flux:text class="truncate text-sm" title="{{ $categoryName }}">{{ $categoryName }}
|
||
</flux:text>
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:badge
|
||
color="{{ match ($pr->status->value) {
|
||
'published' => 'green',
|
||
'review' => 'yellow',
|
||
'draft' => 'zinc',
|
||
'rejected' => 'red',
|
||
'archived' => 'blue',
|
||
} }}">
|
||
{{ $pr->status->label() }}
|
||
</flux:badge>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:text class="text-sm">{{ $pr->portal->label() }}</flux:text>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<flux:text class="text-sm">{{ number_format($pr->hits) }}</flux:text>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<div class="flex items-center gap-1">
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||
<flux:modal.trigger name="confirm-index-publish-{{ $pr->id }}">
|
||
<flux:button size="sm" variant="ghost" icon="check-circle"
|
||
class="text-green-600" />
|
||
</flux:modal.trigger>
|
||
<flux:modal.trigger name="confirm-index-reject-{{ $pr->id }}">
|
||
<flux:button size="sm" variant="ghost" icon="x-circle" class="text-red-600" />
|
||
</flux:modal.trigger>
|
||
@endif
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||
<flux:modal.trigger name="confirm-index-archive-{{ $pr->id }}">
|
||
<flux:button size="sm" variant="ghost" icon="archive-box"
|
||
class="text-zinc-500" />
|
||
</flux:modal.trigger>
|
||
@endif
|
||
</div>
|
||
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||
<flux:modal name="confirm-index-publish-{{ $pr->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung veröffentlichen?') }}
|
||
</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Diese Aktion veröffentlicht die Pressemitteilung ":title".', ['title' => \Illuminate\Support\Str::limit($pr->title, 80)]) }}
|
||
</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({{ $pr->id }})">
|
||
{{ __('Veröffentlichen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
<flux:modal name="confirm-index-reject-{{ $pr->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung ablehnen?') }}
|
||
</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Diese Aktion lehnt die Pressemitteilung ":title" ab.', ['title' => \Illuminate\Support\Str::limit($pr->title, 80)]) }}
|
||
</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="danger" wire:click="reject({{ $pr->id }})">
|
||
{{ __('Ablehnen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
@endif
|
||
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||
<flux:modal name="confirm-index-archive-{{ $pr->id }}" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung archivieren?') }}
|
||
</flux:heading>
|
||
<flux:subheading>
|
||
{{ __('Diese Aktion archiviert die Pressemitteilung ":title".', ['title' => \Illuminate\Support\Str::limit($pr->title, 80)]) }}
|
||
</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({{ $pr->id }})">
|
||
{{ __('Archivieren') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
@endif
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@empty
|
||
<flux:table.row>
|
||
<flux:table.cell colspan="8">
|
||
<div class="flex flex-col items-center justify-center py-10">
|
||
<flux:icon.newspaper class="size-10 text-zinc-300" />
|
||
<flux:text class="mt-3 text-zinc-500">{{ __('Keine Pressemitteilungen gefunden.') }}
|
||
</flux:text>
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforelse
|
||
</flux:table>
|
||
</flux:card>
|
||
|
||
{{ $pressReleases->links() }}
|
||
</div>
|