presseportale/resources/views/livewire/admin/reports/activity.blade.php
Kevin Adametz 31da9ea8b6
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

164 lines
6.7 KiB
PHP

<?php
use App\Enums\ActivityType;
use App\Models\ActivityEvent;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Verlauf')] class extends Component
{
use WithPagination;
public string $typeFilter = '';
public string $from = '';
public string $to = '';
public string $search = '';
public function resetFilters(): void
{
$this->typeFilter = '';
$this->from = '';
$this->to = '';
$this->search = '';
$this->resetPage();
}
public function updated(string $property): void
{
if (in_array($property, ['typeFilter', 'from', 'to', 'search'], true)) {
$this->resetPage();
}
}
public function with(): array
{
$events = ActivityEvent::query()
->with('user')
->when($this->typeFilter !== '', fn ($query) => $query->where('type', $this->typeFilter))
->when($this->from !== '', fn ($query) => $query->where('occurred_at', '>=', $this->from))
->when($this->to !== '', fn ($query) => $query->where('occurred_at', '<=', $this->to))
->when($this->search !== '', function ($query) {
$query->where(function ($inner) {
$inner->where('description', 'like', "%{$this->search}%")
->orWhere('actor_label', 'like', "%{$this->search}%");
});
})
->orderByDesc('occurred_at')
->orderByDesc('id')
->paginate(50);
return [
'events' => $events,
'types' => ActivityType::cases(),
];
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Reports')"
:title="__('Verlauf')"
:description="__('Chronologisches Protokoll der Systemereignisse: Registrierungen, Anmeldungen, Pressemitteilungen, Zahlungen, Formular-Eingänge, versendete E-Mails und Mail-Abmeldungen.')"
/>
{{-- ============== FILTER-PANEL ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter') }}</span>
<flux:button size="sm" variant="filled" icon="arrow-path" wire:click="resetFilters">
{{ __('Filter zurücksetzen') }}
</flux:button>
</div>
<div class="p-5">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<flux:field>
<flux:label>{{ __('Ereignis') }}</flux:label>
<flux:select wire:model.live="typeFilter">
<option value="">{{ __('Alle') }}</option>
@foreach ($types as $type)
<option value="{{ $type->value }}">{{ $type->label() }}</option>
@endforeach
</flux:select>
</flux:field>
<flux:field>
<flux:label>{{ __('Von') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="from" />
</flux:field>
<flux:field>
<flux:label>{{ __('Bis') }}</flux:label>
<flux:input type="datetime-local" wire:model.live.debounce.500ms="to" />
</flux:field>
<flux:field>
<flux:label>{{ __('Suche') }}</flux:label>
<flux:input wire:model.live.debounce.500ms="search" :placeholder="__('Name, E-Mail oder Beschreibung')" />
</flux:field>
</div>
</div>
</article>
{{-- ============== VERLAUF ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Ereignisse') }}</span>
<span class="badge hub">{{ number_format($events->total(), 0, ',', '.') }} {{ __('Einträge') }}</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Zeitpunkt') }}</flux:table.column>
<flux:table.column>{{ __('Ereignis') }}</flux:table.column>
<flux:table.column>{{ __('Benutzer') }}</flux:table.column>
<flux:table.column>{{ __('Beschreibung') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($events as $event)
<flux:table.row :key="$event->id">
<flux:table.cell class="whitespace-nowrap">
<span class="text-[12px] text-[color:var(--color-ink-2)] tabular-nums">
{{ $event->occurred_at->format('d.m.Y H:i:s') }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:badge size="sm" :color="$event->type->badgeColor()">{{ $event->type->label() }}</flux:badge>
</flux:table.cell>
<flux:table.cell class="whitespace-nowrap">
@if ($event->user)
<a href="{{ route('admin.users.show', $event->user->id) }}" wire:navigate
class="text-[12.5px] text-[color:var(--color-ink)] hover:underline">
{{ $event->user->name }}
</a>
@else
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">{{ $event->actor_label ?: '—' }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-2)]">{{ $event->description }}</span>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="4">
<div class="py-8 text-center text-[12px] text-[color:var(--color-ink-3)]">
{{ __('Keine Ereignisse gefunden. Der Verlauf wird ab jetzt fortlaufend befüllt.') }}
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
@if ($events->hasPages())
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)]">
{{ $events->links() }}
</div>
@endif
</article>
</div>