Kunden loeschen: Modal mit Komplett- oder DSGVO-Loeschung ueber alle Dubletten

Ticket dev/_tickets/2026-09-08.md #2:
- "Kunde loeschen" oeffnet ein Modal statt direkt per GET zu loeschen. Es
  zeigt alle Eintraege desselben Kunden (Kundennummer oder E-Mail, ohne
  Platzhalter-Adressen) mit Anzahl Bestellungen.
- Ohne Bestellungen: Komplett-Loeschung (anonymisiert und soft-geloescht,
  Portal-Logins und Stripe-Kunden entfernt, Zahlungs-Tokens inaktiver Abos
  geleert). Mit Bestellungen nur DSGVO-Loeschung (Superadmin); die arbeitet
  jetzt ebenfalls auf der E-Mail-Gruppe und entfernt Portal-Logins.
- Bei aktivem Abo oder Berater-Datensatz wird abgelehnt.
- Die alte "Loeschung" (CustomerRepository::deleteCustomer) entfaellt.
- Fehlermeldungen nutzen alert-error; alert-danger zeigte kein Layout an.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-23 15:43:35 +00:00
parent 728dde4c76
commit 90c2188ffa
12 changed files with 631 additions and 88 deletions

View file

@ -48,6 +48,10 @@ return [
'gdpr_error_is_admin' => 'DSGVO-Löschung nicht möglich: Der Account hat Admin-Rechte. Bitte entziehe zuerst die Admin-Rechte.',
'gdpr_error_active_abo' => 'DSGVO-Löschung nicht möglich: Es besteht noch ein aktives Abo. Bitte kündige das Abo zuerst.',
'gdpr_error_customer_is_member' => 'DSGVO-Löschung nicht möglich: Dieser Kundendatensatz gehört zu einem Berater-Account. Bitte nutze die DSGVO-Löschung in der User-Verwaltung.',
'delete_confirm_checkbox_missing' => 'Bitte bestätige die Löschung über die Checkbox.',
'gdpr_purge_customer_button' => 'Kunde komplett löschen',
'gdpr_purge_customer_success' => 'Kunde wurde komplett gelöscht: :count Kundeneintrag/-einträge entfernt.',
'gdpr_error_customer_has_orders' => 'Komplett löschen nicht möglich: Der Kunde hat bereits Bestellungen. Es ist nur eine DSGVO-Löschung möglich.',
];
/*
{{ __('msg.') }}

View file

@ -50,4 +50,8 @@ return [
'gdpr_error_is_admin' => 'GDPR deletion not possible: The account has admin rights. Please remove the admin rights first.',
'gdpr_error_active_abo' => 'GDPR deletion not possible: There is still an active subscription. Please cancel the subscription first.',
'gdpr_error_customer_is_member' => 'GDPR deletion not possible: This customer record belongs to a consultant account. Please use the GDPR deletion in the user administration.',
'delete_confirm_checkbox_missing' => 'Please confirm the deletion via the checkbox.',
'gdpr_purge_customer_button' => 'Delete customer completely',
'gdpr_purge_customer_success' => 'Customer deleted completely: :count customer record(s) removed.',
'gdpr_error_customer_has_orders' => 'Complete deletion not possible: The customer already has orders. Only a GDPR deletion is possible.',
];

View file

@ -50,4 +50,8 @@ return [
'gdpr_error_is_admin' => 'Eliminación RGPD no posible: La cuenta tiene derechos de administrador. Por favor retira primero los derechos de administrador.',
'gdpr_error_active_abo' => 'Eliminación RGPD no posible: Todavía existe una suscripción activa. Por favor cancela primero la suscripción.',
'gdpr_error_customer_is_member' => 'Eliminación RGPD no posible: Este registro de cliente pertenece a una cuenta de asesor. Por favor utiliza la eliminación RGPD en la administración de usuarios.',
'delete_confirm_checkbox_missing' => 'Confirma la eliminación mediante la casilla.',
'gdpr_purge_customer_button' => 'Eliminar cliente por completo',
'gdpr_purge_customer_success' => 'Cliente eliminado por completo: :count registro(s) de cliente eliminado(s).',
'gdpr_error_customer_has_orders' => 'Eliminación completa no posible: el cliente ya tiene pedidos. Solo es posible una eliminación RGPD.',
];

View file

@ -48,4 +48,8 @@ return [
'gdpr_error_is_admin' => "Suppression RGPD impossible : Le compte dispose de droits d'administrateur. Veuillez d'abord retirer les droits d'administrateur.",
'gdpr_error_active_abo' => "Suppression RGPD impossible : Un abonnement actif existe encore. Veuillez d'abord résilier l'abonnement.",
'gdpr_error_customer_is_member' => "Suppression RGPD impossible : Cette fiche client appartient à un compte de conseiller. Veuillez utiliser la suppression RGPD dans l'administration des utilisateurs.",
'delete_confirm_checkbox_missing' => 'Veuillez confirmer la suppression via la case à cocher.',
'gdpr_purge_customer_button' => 'Supprimer complètement le client',
'gdpr_purge_customer_success' => 'Client supprimé complètement : :count enregistrement(s) client supprimé(s).',
'gdpr_error_customer_has_orders' => 'Suppression complète impossible : le client a déjà des commandes. Seule une suppression RGPD est possible.',
];

View file

@ -21,7 +21,7 @@
</div>
<div class="col-md-3 mb-3">
<a class="btn btn-sm btn-secondary mt-2" href="{{route('admin_customer_edit', [$shopping_user->id])}}">{{ __('customer.edit_customer_data') }}</a>
<a class="btn btn-sm btn-danger mt-2" href="{{route('admin_customer_delete', [$shopping_user->id])}}" onclick="return confirm('{{ __('customer.delete_customer_confirm') }}')">{{ __('customer.delete_customer') }}</a>
<button type="button" class="btn btn-sm btn-danger mt-2" data-toggle="modal" data-target="#modals-customer-delete">{{ __('customer.delete_customer') }}</button>
</div>
</div>
@else

View file

@ -9,40 +9,102 @@
@include('admin.customer._detail')
<a href="{{route('admin_customers')}}" class="btn btn-sm btn-default mt-2 float-right">{{ __('back') }}</a>
@if (Auth::user()->isSuperAdmin() && !$shopping_user->auth_user_id)
<button type="button" class="btn btn-sm btn-danger mt-2" data-toggle="modal"
data-target="#modals-customer-gdpr-delete">{{ __('msg.gdpr_delete_customer_button') }}</button>
{{-- Kunden löschen: ein Modal für beide Wege (dev/_tickets/2026-09-08.md, Ticket #2) --}}
@php($canGdprDelete = Auth::user()->isSuperAdmin())
<div class="modal fade" id="modals-customer-delete">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ __('customer.delete_customer') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</div>
<div class="modal-body">
@if ($deletion['is_member'])
<div class="alert alert-warning mb-0">{{ __('msg.gdpr_error_customer_is_member') }}</div>
@else
<p>
Der Kunde ist <b>{{ $deletion['rows']->count() }}-mal</b> angelegt
@if ($deletion['email'])
(E-Mail <b>{{ $deletion['email'] }}</b>, Kundennummer {{ $shopping_user->number ?: '-' }}).
@else
(Kundennummer {{ $shopping_user->number ?: '-' }}; keine gültige E-Mail zur Duplikat-Suche).
@endif
Alle folgenden Einträge werden gemeinsam behandelt:
</p>
<div class="table-responsive">
<table class="table table-sm table-bordered" id="customer-delete-rows">
<thead>
<tr>
<th>ID</th>
<th>Kd-Nr.</th>
<th>Name</th>
<th>Berater</th>
<th>Bestellungen</th>
<th>Angelegt</th>
</tr>
</thead>
<tbody>
@foreach ($deletion['rows'] as $row)
<tr class="{{ $row->id === $shopping_user->id ? 'font-weight-bold' : '' }}">
<td>{{ $row->id }}</td>
<td>{{ $row->number ?: '-' }}</td>
<td>{{ trim($row->billing_firstname.' '.$row->billing_lastname) }}
@if ($row->deleted_at) <span class="badge badge-secondary">gelöscht</span> @endif
</td>
<td>{{ $row->member->email ?? '-' }}</td>
<td>{{ $deletion['orders_by_row'][$row->id] ?? 0 }}</td>
<td>{{ $row->created_at ? $row->created_at->format('d.m.Y') : '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Modal DSGVO-Löschung -->
<div class="modal fade" id="modals-customer-gdpr-delete">
<div class="modal-dialog">
<form class="modal-content" action="{{ route('admin_customer_gdpr_delete', [$shopping_user->id]) }}"
method="post">
@csrf
<div class="modal-header">
<h5 class="modal-title">{{ __('msg.gdpr_delete_customer_button') }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
</div>
<div class="modal-body">
<p>Alle personenbezogenen Daten dieses Kunden werden <b>unwiderruflich anonymisiert</b>
(alle Einträge mit der Kundennummer {{ $shopping_user->number }}).
Bei Bestellungen mit Rechnung bleiben Name und Rechnungsadresse wegen der
gesetzlichen Aufbewahrungspflichten erhalten; alle Kontaktdaten
(E-Mail, Telefon, Notizen, Newsletter) werden entfernt.
Aktive Abos müssen vorher gekündigt werden.</p>
<label class="custom-control custom-checkbox m-0">
<input type="checkbox" class="custom-control-input" name="gdpr_delete_customer" value="1">
<span class="custom-control-label">DSGVO-Löschung unwiderruflich ausführen?</span>
</label>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ __('close') }}</button>
<button type="submit"
class="btn btn-danger">{{ __('msg.gdpr_delete_customer_button') }}</button>
</div>
</form>
@if ($deletion['orders_count'] > 0)
<div class="alert alert-warning">
Der Kunde hat schon <b>{{ $deletion['orders_count'] }} Bestellung(en)</b>. Es ist nur <b>DSGVO löschen</b> möglich:
Alle personenbezogenen Daten werden unwiderruflich anonymisiert. Bei Einträgen mit Bestellung bleiben Name und
Rechnungsadresse wegen der gesetzlichen Aufbewahrungspflichten erhalten; alle Kontaktdaten (E-Mail, Telefon,
Notizen, Newsletter) sowie die Portal-Logins werden entfernt.
@if ($deletion['active_abos'] > 0)
<br><b>{{ __('msg.gdpr_error_active_abo') }}</b>
@endif
</div>
@if ($canGdprDelete)
<form action="{{ route('admin_customer_gdpr_delete', [$shopping_user->id]) }}" method="post" id="form-customer-gdpr-delete">
@csrf
<label class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" name="gdpr_delete_customer" value="1">
<span class="custom-control-label">DSGVO-Löschung unwiderruflich ausführen?</span>
</label>
<button type="submit" class="btn btn-danger" {{ $deletion['active_abos'] > 0 ? 'disabled' : '' }}>{{ __('msg.gdpr_delete_customer_button') }}</button>
</form>
@else
<p class="text-muted mb-0">Die DSGVO-Löschung kann nur von einem Superadmin ausgeführt werden.</p>
@endif
@elseif ($deletion['active_abos'] > 0)
<div class="alert alert-warning mb-0">{{ __('msg.gdpr_error_active_abo') }}</div>
@else
<div class="alert alert-info">
Der Kunde hat <b>keine Bestellungen</b> und kann <b>komplett gelöscht</b> werden: Alle oben aufgeführten Einträge
werden vollständig anonymisiert und entfernt, inklusive Portal-Login und inaktiver Abos.
</div>
<form action="{{ route('admin_customer_delete', [$shopping_user->id]) }}" method="post" id="form-customer-delete">
@csrf
<label class="custom-control custom-checkbox mb-3">
<input type="checkbox" class="custom-control-input" name="delete_customer" value="1">
<span class="custom-control-label">Kunden mit allen {{ $deletion['rows']->count() }} Einträgen unwiderruflich löschen?</span>
</label>
<button type="submit" class="btn btn-danger">{{ __('msg.gdpr_purge_customer_button') }}</button>
</form>
@endif
@endif
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ __('close') }}</button>
</div>
</div>
</div>
@endif
</div>
@endsection