admin: Aktivitäts-Verlauf unter Reports ▸ Verlauf
Chronologisches Protokoll der Systemereignisse als neue Admin-Seite (/admin/reports/activity) mit Filter nach Ereignistyp, Zeitraum und Suche. Erfasst ab jetzt fortlaufend: - Registrierungen inkl. Weg (Formular, Google, Magic-Link) - Anmeldungen inkl. Weg (Passwort, Google, Magic-Link, Impersonation) - PM-Statuswechsel mit Ergebnis/Begründung (zentral im Status-Log-Hook; KI-/Scheduler-Wechsel erscheinen als „System") - Zahlungseingänge (Rechnungsspiegelung) und Credit-Aufladungen - Formular-Eingänge: Kontakt, Rechtsanfrage, Widerruf, Kündigung, Newsletter-Anmeldung (Observer auf den Modellen) Schreiben ist Best-Effort über den ActivityLog-Service — ein fehlgeschlagener Verlaufseintrag bricht nie den fachlichen Ablauf. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a574898ca9
commit
3a11963990
19 changed files with 720 additions and 4 deletions
164
resources/views/livewire/admin/reports/activity.blade.php
Normal file
164
resources/views/livewire/admin/reports/activity.blade.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Models\ActivityEvent;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\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 und Formular-Eingänge.')"
|
||||
/>
|
||||
|
||||
{{-- ============== 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue