From d6461f5c5e45463dbd80dcfee352faa21ecec86a Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 3 Jul 2026 10:29:32 +0000 Subject: [PATCH] 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 --- app/Models/WithdrawalRequest.php | 5 + .../factories/WithdrawalRequestFactory.php | 37 +++++ .../admin/legal-requests/index.blade.php | 144 +++++++++++++++++- .../Admin/WithdrawalRequestsAdminTest.php | 75 +++++++++ 4 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 database/factories/WithdrawalRequestFactory.php create mode 100644 tests/Feature/Admin/WithdrawalRequestsAdminTest.php diff --git a/app/Models/WithdrawalRequest.php b/app/Models/WithdrawalRequest.php index c97169f6..1f4ee206 100644 --- a/app/Models/WithdrawalRequest.php +++ b/app/Models/WithdrawalRequest.php @@ -2,7 +2,9 @@ namespace App\Models; +use Database\Factories\WithdrawalRequestFactory; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; /** @@ -11,6 +13,9 @@ use Illuminate\Database\Eloquent\Model; */ class WithdrawalRequest extends Model { + /** @use HasFactory */ + use HasFactory; + protected $fillable = [ 'name', 'email', diff --git a/database/factories/WithdrawalRequestFactory.php b/database/factories/WithdrawalRequestFactory.php new file mode 100644 index 00000000..ab17e663 --- /dev/null +++ b/database/factories/WithdrawalRequestFactory.php @@ -0,0 +1,37 @@ + + */ +class WithdrawalRequestFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->safeEmail(), + 'order_reference' => fake()->optional()->bothify('STR-2026-####'), + 'host' => 'pressekonto.test', + 'requester_ip' => fake()->ipv4(), + 'status' => 'open', + ]; + } + + public function resolved(): static + { + return $this->state(fn (): array => [ + 'status' => 'resolved', + 'resolved_at' => now(), + ]); + } +} diff --git a/resources/views/livewire/admin/legal-requests/index.blade.php b/resources/views/livewire/admin/legal-requests/index.blade.php index f2381209..83fd7d18 100644 --- a/resources/views/livewire/admin/legal-requests/index.blade.php +++ b/resources/views/livewire/admin/legal-requests/index.blade.php @@ -3,6 +3,7 @@ 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; @@ -19,6 +20,9 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten #[Url(as: 'type', except: 'all')] public string $typeFilter = 'all'; + #[Url(as: 'widerrufe', except: 'open')] + public string $withdrawalStatusFilter = 'open'; + public function updatedStatusFilter(): void { $this->resetPage(); @@ -29,6 +33,30 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten $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() @@ -39,11 +67,19 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten ->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(), ]; } @@ -64,11 +100,11 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten :badge="__('Admin Backend')" :eyebrow="__('Administration · Recht & Compliance')" :title="__('Recht & Compliance')" - :description="__('DSGVO-Anonymisierung, Persönlichkeitsrechte und Meldungen zu Pressemitteilungen – manuelle Bearbeitung, keine KI.')" + :description="__('DSGVO-Anonymisierung, Persönlichkeitsrechte und Meldungen zu Pressemitteilungen sowie Online-Widerrufe über den Widerrufsbutton – manuelle Bearbeitung, keine KI.')" /> {{-- ============== KPI-Reihe ============== --}} -
+
{{ __('zu bearbeiten') }} {{ __('offen + in Bearbeitung') }} @@ -77,6 +113,10 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten {{ __('alle Anfragen') }} {{ __('inkl. erledigt') }} + + {{ __('Online-Widerruf') }} + {{ __('gesetzliche Frist beachten') }} +
{{-- ============== FILTER ============== --}} @@ -174,4 +214,104 @@ new #[Layout('components.layouts.app'), Title('Recht & Compliance')] class exten + + {{-- ============== WIDERRUFE (Online-Widerruf, gesetzlicher Widerrufsbutton) ============== --}} +
+
+ {{ __('Widerrufe') }} +
+ + + + + + + {{ __(':count Einträge', ['count' => $withdrawals->total()]) }} + +
+
+ + + {{ __('Vorgang') }} + {{ __('Name') }} + {{ __('E-Mail') }} + {{ __('Bestell-/Rechnungsnr.') }} + {{ __('Eingegangen') }} + {{ __('Status') }} + {{ __('Aktion') }} + + + + @forelse ($withdrawals as $withdrawal) + + + + W-{{ $withdrawal->id }} + + + + + {{ $withdrawal->name }} + + + + + {{ $withdrawal->email }} + + + + + {{ $withdrawal->order_reference ?: '—' }} + + + + + {{ $withdrawal->created_at?->format('d.m.Y H:i') ?? '-' }} + + + + @if ($withdrawal->status === 'open') + {{ __('Offen') }} + @else + {{ __('Erledigt') }} + @endif + + + @if ($withdrawal->status === 'open') + + {{ __('Erledigt') }} + + @else + + {{ __('Wieder öffnen') }} + + @endif + + + @empty + + +
+
+ +
+
+ {{ __('Keine Widerrufe in dieser Ansicht') }} +
+

+ {{ __('Online-Widerrufe über den „Vertrag widerrufen"-Button laufen hier auf. Die Eingangsbestätigung an den Verbraucher wird automatisch versendet.') }} +

+
+
+
+ @endforelse +
+
+ + +
diff --git a/tests/Feature/Admin/WithdrawalRequestsAdminTest.php b/tests/Feature/Admin/WithdrawalRequestsAdminTest.php new file mode 100644 index 00000000..25f44442 --- /dev/null +++ b/tests/Feature/Admin/WithdrawalRequestsAdminTest.php @@ -0,0 +1,75 @@ +seed(RolesAndPermissionsSeeder::class); +}); + +function withdrawalAdmin(): User +{ + $admin = User::factory()->create(['is_active' => true]); + $admin->assignRole('admin'); + + return $admin; +} + +test('the legal queue lists open withdrawal requests', function () { + $withdrawal = WithdrawalRequest::factory()->create([ + 'name' => 'Paula Presse', + 'email' => 'paula@example.test', + 'order_reference' => 'STR-2026-0042', + ]); + + Volt::actingAs(withdrawalAdmin()) + ->test('admin.legal-requests.index') + ->assertOk() + ->assertSee('Widerrufe') + ->assertSee('W-'.$withdrawal->id) + ->assertSee('Paula Presse') + ->assertSee('paula@example.test') + ->assertSee('STR-2026-0042'); +}); + +test('an admin can resolve a withdrawal request', function () { + $withdrawal = WithdrawalRequest::factory()->create(); + + Volt::actingAs(withdrawalAdmin()) + ->test('admin.legal-requests.index') + ->call('resolveWithdrawal', $withdrawal->id) + ->assertOk(); + + $withdrawal->refresh(); + + expect($withdrawal->status)->toBe('resolved'); + expect($withdrawal->resolved_at)->not->toBeNull(); +}); + +test('an admin can reopen a resolved withdrawal request', function () { + $withdrawal = WithdrawalRequest::factory()->resolved()->create(); + + Volt::actingAs(withdrawalAdmin()) + ->test('admin.legal-requests.index') + ->set('withdrawalStatusFilter', 'all') + ->call('reopenWithdrawal', $withdrawal->id) + ->assertOk(); + + $withdrawal->refresh(); + + expect($withdrawal->status)->toBe('open'); + expect($withdrawal->resolved_at)->toBeNull(); +}); + +test('resolved withdrawals are hidden from the default open view', function () { + WithdrawalRequest::factory()->resolved()->create(['name' => 'Erledigte Person']); + + Volt::actingAs(withdrawalAdmin()) + ->test('admin.legal-requests.index') + ->assertOk() + ->assertDontSee('Erledigte Person'); +});