presseportale/resources/views/livewire/admin/legal-requests/index.blade.php
Kevin Adametz d6461f5c5e Admin: Widerrufe laufen in der Recht-&-Compliance-Queue auf
Eigene Widerrufe-Sektion unter der Anfragen-Tabelle in
/admin/legal-requests: Vorgangsnummer, Zuordnungsdaten, Statusfilter
(offen/alle/erledigt, eigener Paginator), Erledigt/Wieder-oeffnen-Aktion
mit resolved_at; KPI-Karte "Offene Widerrufe". Dazu
WithdrawalRequest-Factory (+resolved-State) fuer die Tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:29:32 +00:00

317 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\LegalRequestStatus;
use App\Enums\LegalRequestType;
use App\Models\LegalRequest;
use App\Models\WithdrawalRequest;
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('Recht & Compliance')] class extends Component
{
use WithPagination;
#[Url(as: 'status', except: 'open')]
public string $statusFilter = 'open';
#[Url(as: 'type', except: 'all')]
public string $typeFilter = 'all';
#[Url(as: 'widerrufe', except: 'open')]
public string $withdrawalStatusFilter = 'open';
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function updatedTypeFilter(): void
{
$this->resetPage();
}
public function updatedWithdrawalStatusFilter(): void
{
$this->resetPage('withdrawals');
}
/**
* Widerruf als erledigt markieren (Rückabwicklung angestoßen/abgeschlossen).
*/
public function resolveWithdrawal(int $id): void
{
WithdrawalRequest::query()->findOrFail($id)->update([
'status' => 'resolved',
'resolved_at' => now(),
]);
}
public function reopenWithdrawal(int $id): void
{
WithdrawalRequest::query()->findOrFail($id)->update([
'status' => 'open',
'resolved_at' => null,
]);
}
public function with(): array
{
$requests = LegalRequest::query()
->with('pressRelease:id,title,slug')
->when($this->statusFilter === 'open', fn ($query) => $query->open())
->when(! in_array($this->statusFilter, ['open', 'all'], true), fn ($query) => $query->where('status', $this->statusFilter))
->when($this->typeFilter !== 'all', fn ($query) => $query->where('type', $this->typeFilter))
->latest()
->paginate(50);
$withdrawals = WithdrawalRequest::query()
->when($this->withdrawalStatusFilter === 'open', fn ($query) => $query->open())
->when($this->withdrawalStatusFilter === 'resolved', fn ($query) => $query->where('status', 'resolved'))
->latest()
->paginate(25, pageName: 'withdrawals');
return [
'requests' => $requests,
'typeOptions' => LegalRequestType::cases(),
'openCount' => LegalRequest::query()->open()->count(),
'totalCount' => LegalRequest::query()->count(),
'withdrawals' => $withdrawals,
'openWithdrawalCount' => WithdrawalRequest::query()->open()->count(),
];
}
public function statusBadgeClass(LegalRequestStatus $status): string
{
return match ($status) {
LegalRequestStatus::Open => 'warn',
LegalRequestStatus::InProgress => 'hub',
LegalRequestStatus::Resolved => 'ok',
LegalRequestStatus::Rejected => 'err',
};
}
}; ?>
<div class="space-y-8">
{{-- ============== PAGE HEADER ============== --}}
<x-portal.page-header
:badge="__('Admin Backend')"
:eyebrow="__('Administration · Recht & Compliance')"
:title="__('Recht & Compliance')"
:description="__('DSGVO-Anonymisierung, Persönlichkeitsrechte und Meldungen zu Pressemitteilungen sowie Online-Widerrufe über den Widerrufsbutton – manuelle Bearbeitung, keine KI.')"
/>
{{-- ============== KPI-Reihe ============== --}}
<section class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<x-portal.stat-card variant="primary" :label="__('Offen')" :value="number_format($openCount)">
<x-slot:meta>{{ __('zu bearbeiten') }}</x-slot:meta>
<x-slot:trend>{{ __('offen + in Bearbeitung') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="muted" :label="__('Gesamt')" :value="number_format($totalCount)">
<x-slot:meta>{{ __('alle Anfragen') }}</x-slot:meta>
<x-slot:trend>{{ __('inkl. erledigt') }}</x-slot:trend>
</x-portal.stat-card>
<x-portal.stat-card variant="warn" :label="__('Offene Widerrufe')" :value="number_format($openWithdrawalCount)">
<x-slot:meta>{{ __('Online-Widerruf') }}</x-slot:meta>
<x-slot:trend>{{ __('gesetzliche Frist beachten') }}</x-slot:trend>
</x-portal.stat-card>
</section>
{{-- ============== FILTER ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Filter') }}</span>
</div>
<div class="p-5 flex flex-col gap-4 sm:flex-row">
<flux:select wire:model.live="statusFilter" class="w-full sm:w-56">
<option value="open">{{ __('Offen & in Bearbeitung') }}</option>
<option value="all">{{ __('Alle Status') }}</option>
<option value="resolved">{{ __('Erledigt') }}</option>
<option value="rejected">{{ __('Abgelehnt') }}</option>
</flux:select>
<flux:select wire:model.live="typeFilter" class="w-full sm:w-56">
<option value="all">{{ __('Alle Typen') }}</option>
@foreach ($typeOptions as $typeOption)
<option value="{{ $typeOption->value }}">{{ $typeOption->label() }}</option>
@endforeach
</flux:select>
</div>
</article>
{{-- ============== TABELLE ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Anfragen') }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $requests->total()]) }}
</span>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Typ') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Pressemitteilung') }}</flux:table.column>
<flux:table.column>{{ __('Absender') }}</flux:table.column>
<flux:table.column>{{ __('Eingegangen') }}</flux:table.column>
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($requests as $request)
<flux:table.row :key="$request->id">
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $request->type->label() }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="badge dot {{ $this->statusBadgeClass($request->status) }}">
{{ $request->status->label() }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink)]">
{{ \Illuminate\Support\Str::limit($request->pressRelease?->title ?? __('PM gelöscht'), 60) }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">
{{ $request->requester_email ?: __('anonym') }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $request->created_at?->format('d.m.Y H:i') ?? '-' }}
</span>
</flux:table.cell>
<flux:table.cell>
<flux:button size="sm" variant="filled" icon="eye"
href="{{ route('admin.legal-requests.show', $request->id) }}" wire:navigate>
{{ __('Öffnen') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@empty
<flux:table.row>
<flux:table.cell colspan="6">
<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.scale class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Anfragen in dieser Ansicht') }}
</div>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<x-portal.table-pagination :paginator="$requests" />
</article>
{{-- ============== WIDERRUFE (Online-Widerruf, gesetzlicher Widerrufsbutton) ============== --}}
<article class="panel overflow-hidden">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Widerrufe') }}</span>
<div class="flex items-center gap-3">
<flux:select wire:model.live="withdrawalStatusFilter" size="sm" class="w-44">
<option value="open">{{ __('Offen') }}</option>
<option value="all">{{ __('Alle') }}</option>
<option value="resolved">{{ __('Erledigt') }}</option>
</flux:select>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __(':count Einträge', ['count' => $withdrawals->total()]) }}
</span>
</div>
</div>
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Vorgang') }}</flux:table.column>
<flux:table.column>{{ __('Name') }}</flux:table.column>
<flux:table.column>{{ __('E-Mail') }}</flux:table.column>
<flux:table.column>{{ __('Bestell-/Rechnungsnr.') }}</flux:table.column>
<flux:table.column>{{ __('Eingegangen') }}</flux:table.column>
<flux:table.column>{{ __('Status') }}</flux:table.column>
<flux:table.column>{{ __('Aktion') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@forelse ($withdrawals as $withdrawal)
<flux:table.row :key="'withdrawal-'.$withdrawal->id">
<flux:table.cell>
<span class="text-[12.5px] font-mono font-semibold text-[color:var(--color-ink)]">
W-{{ $withdrawal->id }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $withdrawal->name }}
</span>
</flux:table.cell>
<flux:table.cell>
<a href="mailto:{{ $withdrawal->email }}"
class="text-[12.5px] text-[color:var(--color-hub)] hover:underline">
{{ $withdrawal->email }}
</a>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12.5px] text-[color:var(--color-ink-2)]">
{{ $withdrawal->order_reference ?: '—' }}
</span>
</flux:table.cell>
<flux:table.cell>
<span class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $withdrawal->created_at?->format('d.m.Y H:i') ?? '-' }}
</span>
</flux:table.cell>
<flux:table.cell>
@if ($withdrawal->status === 'open')
<span class="badge warn dot">{{ __('Offen') }}</span>
@else
<span class="badge ok dot">{{ __('Erledigt') }}</span>
@endif
</flux:table.cell>
<flux:table.cell>
@if ($withdrawal->status === 'open')
<flux:button size="sm" variant="filled" icon="check"
wire:click="resolveWithdrawal({{ $withdrawal->id }})">
{{ __('Erledigt') }}
</flux:button>
@else
<flux:button size="sm" variant="filled" icon="arrow-uturn-left"
wire:click="reopenWithdrawal({{ $withdrawal->id }})">
{{ __('Wieder öffnen') }}
</flux:button>
@endif
</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.arrow-uturn-left class="size-6" />
</div>
<div class="text-[14px] font-semibold text-[color:var(--color-ink)] mb-1">
{{ __('Keine Widerrufe in dieser Ansicht') }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0 max-w-[420px]">
{{ __('Online-Widerrufe über den „Vertrag widerrufen"-Button laufen hier auf. Die Eingangsbestätigung an den Verbraucher wird automatisch versendet.') }}
</p>
</div>
</flux:table.cell>
</flux:table.row>
@endforelse
</flux:table.rows>
</flux:table>
<x-portal.table-pagination :paginator="$withdrawals" />
</article>
</div>