256 lines
12 KiB
PHP
256 lines
12 KiB
PHP
<?php
|
||
|
||
use App\Enums\Portal;
|
||
use App\Models\FooterCode;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Volt\Component;
|
||
use Livewire\WithPagination;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Footer-Codes')] class extends Component
|
||
{
|
||
use WithPagination;
|
||
|
||
public string $search = '';
|
||
|
||
public string $portalFilter = 'all';
|
||
|
||
public string $statusFilter = 'all';
|
||
|
||
public function updatedSearch(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedPortalFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function updatedStatusFilter(): void
|
||
{
|
||
$this->resetPage();
|
||
}
|
||
|
||
public function toggleActive(int $id): void
|
||
{
|
||
$footerCode = FooterCode::query()->find($id);
|
||
|
||
if (! $footerCode) {
|
||
return;
|
||
}
|
||
|
||
$footerCode->update(['is_active' => ! $footerCode->is_active]);
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$term = trim($this->search);
|
||
|
||
$codes = FooterCode::query()
|
||
->withCount('categories')
|
||
->when($this->portalFilter !== 'all', fn ($q) => $q->where('portal', $this->portalFilter))
|
||
->when($this->statusFilter === 'active', fn ($q) => $q->where('is_active', true))
|
||
->when($this->statusFilter === 'inactive', fn ($q) => $q->where('is_active', false))
|
||
->when($this->statusFilter === 'global', fn ($q) => $q->where('is_global', true))
|
||
->when(filled($term), function ($q) use ($term): void {
|
||
$q->where(function ($q) use ($term): void {
|
||
$q->where('title', 'like', '%'.$term.'%')
|
||
->orWhere('content', 'like', '%'.$term.'%');
|
||
});
|
||
})
|
||
->orderByDesc('is_global')
|
||
->orderBy('priority')
|
||
->orderBy('title')
|
||
->paginate(25);
|
||
|
||
return [
|
||
'codes' => $codes,
|
||
'portalOptions' => Portal::cases(),
|
||
'totals' => [
|
||
'total' => FooterCode::query()->count(),
|
||
'active' => FooterCode::query()->where('is_active', true)->count(),
|
||
'global' => FooterCode::query()->where('is_global', true)->count(),
|
||
],
|
||
];
|
||
}
|
||
}; ?>
|
||
|
||
<div class="space-y-8">
|
||
{{-- ============== 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-nowrap whitespace-nowrap">
|
||
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
|
||
<span class="eyebrow muted">{{ __('Administration · Operations') }}</span>
|
||
</div>
|
||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||
{{ __('Footer-Codes') }}
|
||
</h1>
|
||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
|
||
{{ __('Snippets, die unter Pressemitteilungen ausgespielt werden.') }}
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<flux:button variant="primary" icon="plus" :href="route('admin.footer-codes.create')" wire:navigate>
|
||
{{ __('Footer-Code anlegen') }}
|
||
</flux:button>
|
||
</div>
|
||
</header>
|
||
|
||
@if (session('success'))
|
||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
|
||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
|
||
{{ session('success') }}
|
||
</div>
|
||
@endif
|
||
@if (session('error'))
|
||
<div class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-start gap-3
|
||
bg-[color:var(--color-err-soft)] border-[color:var(--color-err)]/30 text-[color:var(--color-ink-2)]">
|
||
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-err)]" />
|
||
<div class="flex-1">{{ session('error') }}</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- ============== KPI-Reihe ============== --}}
|
||
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||
<x-portal.stat-card variant="primary" :label="__('Gesamt')" :value="number_format($totals['total'])">
|
||
<x-slot:meta>{{ __('Snippets') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('alle Portale') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
<x-portal.stat-card variant="ok" :label="__('Aktiv')" :value="number_format($totals['active'])">
|
||
<x-slot:meta>{{ __('live ausgespielt') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('in PMs sichtbar') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
<x-portal.stat-card variant="muted" :label="__('Global')" :value="number_format($totals['global'])">
|
||
<x-slot:meta>{{ __('portalübergreifend') }}</x-slot:meta>
|
||
<x-slot:trend>{{ __('ohne Kategorie-Bindung') }}</x-slot:trend>
|
||
</x-portal.stat-card>
|
||
</section>
|
||
|
||
{{-- ============== FILTER ============== --}}
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Filter & Suche') }}</span>
|
||
</div>
|
||
<div class="p-5 grid gap-3 md:grid-cols-[1fr,auto,auto]">
|
||
<flux:input
|
||
wire:model.live.debounce.300ms="search"
|
||
placeholder="{{ __('Titel oder Inhalt suchen…') }}"
|
||
icon="magnifying-glass"
|
||
/>
|
||
<flux:select wire:model.live="portalFilter">
|
||
<option value="all">{{ __('Alle Portale') }}</option>
|
||
@foreach ($portalOptions as $option)
|
||
<option value="{{ $option->value }}">{{ $option->label() }}</option>
|
||
@endforeach
|
||
</flux:select>
|
||
<flux:select wire:model.live="statusFilter">
|
||
<option value="all">{{ __('Alle Status') }}</option>
|
||
<option value="active">{{ __('Aktiv') }}</option>
|
||
<option value="inactive">{{ __('Inaktiv') }}</option>
|
||
<option value="global">{{ __('Global') }}</option>
|
||
</flux:select>
|
||
</div>
|
||
</article>
|
||
|
||
{{-- ============== TABELLE ============== --}}
|
||
<article class="panel overflow-hidden">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Alle Footer-Codes') }}</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __(':count Einträge', ['count' => $codes->total()]) }}
|
||
</span>
|
||
</div>
|
||
<flux:table>
|
||
<flux:table.columns>
|
||
<flux:table.column>{{ __('Titel') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Portal') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Sprache') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Kategorien') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Priorität') }}</flux:table.column>
|
||
<flux:table.column>{{ __('Status') }}</flux:table.column>
|
||
<flux:table.column align="end">{{ __('Aktionen') }}</flux:table.column>
|
||
</flux:table.columns>
|
||
|
||
<flux:table.rows>
|
||
@forelse ($codes as $code)
|
||
<flux:table.row :key="'fc-'.$code->id">
|
||
<flux:table.cell>
|
||
<div class="flex items-center gap-2">
|
||
@if ($code->is_global)
|
||
<span class="badge hub dot">{{ __('Global') }}</span>
|
||
@endif
|
||
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $code->title }}</span>
|
||
</div>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<span class="badge hub">{{ $code->portal->label() }}</span>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<span class="text-[12px] text-[color:var(--color-ink-2)] font-mono">
|
||
{{ $code->language ? strtoupper($code->language) : '–' }}
|
||
</span>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if ($code->is_global)
|
||
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('alle') }}</span>
|
||
@else
|
||
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->categories_count }}</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
<span class="text-[12.5px] text-[color:var(--color-ink)] tabular-nums">{{ $code->priority }}</span>
|
||
</flux:table.cell>
|
||
<flux:table.cell>
|
||
@if ($code->is_active)
|
||
<span class="badge ok dot">{{ __('Aktiv') }}</span>
|
||
@else
|
||
<span class="badge dot">{{ __('Inaktiv') }}</span>
|
||
@endif
|
||
</flux:table.cell>
|
||
<flux:table.cell align="end">
|
||
<div class="flex items-center justify-end gap-1">
|
||
<flux:button
|
||
size="xs"
|
||
variant="ghost"
|
||
:icon="$code->is_active ? 'pause' : 'play'"
|
||
wire:click="toggleActive({{ $code->id }})"
|
||
:title="$code->is_active ? __('Deaktivieren') : __('Aktivieren')"
|
||
/>
|
||
<flux:button
|
||
size="xs"
|
||
variant="ghost"
|
||
icon="pencil"
|
||
:href="route('admin.footer-codes.edit', $code->id)"
|
||
wire:navigate
|
||
:title="__('Bearbeiten')"
|
||
/>
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@empty
|
||
<flux:table.row>
|
||
<flux:table.cell :colspan="7">
|
||
<div class="flex flex-col items-center justify-center px-4 py-10 text-center">
|
||
<div class="w-14 h-14 rounded-[6px] flex items-center justify-center mb-3
|
||
bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
|
||
<flux:icon.code-bracket-square class="size-6" />
|
||
</div>
|
||
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
|
||
{{ __('Keine Footer-Codes gefunden.') }}
|
||
</div>
|
||
</div>
|
||
</flux:table.cell>
|
||
</flux:table.row>
|
||
@endforelse
|
||
</flux:table.rows>
|
||
</flux:table>
|
||
|
||
<div class="border-t border-[color:var(--color-bg-rule)] p-4">
|
||
{{ $codes->links('components.portal.pagination') }}
|
||
</div>
|
||
</article>
|
||
</div>
|