Berater uebertragen: kompletten Berater- und Kundenstamm an einen anderen Berater

Ticket dev/_tickets/2026-09-08.md #1: neuer Punkt "Berater uebertragen" im
Superadmin (/admin/user/transfer). Vorschau zeigt Firstline, Downline,
geparkte Sponsor-Bezuege, Kunden, Kundenabos und Portal-Logins; nach
Bestaetigung wird alles in einer Transaktion umgehaengt und wie die
User-Bereinigung protokolliert (user_clean_up_logs,
shopping_user_member_logs, SysLog). Bestellungen, Umsaetze und
Provisionsdaten bleiben beim bisherigen Berater. Ziel unterhalb der
Firstline der Quelle wird abgelehnt (Kreis im Baum).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-23 15:43:58 +00:00
parent 60f9985313
commit ef7e333bd4
17 changed files with 902 additions and 0 deletions

View file

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\ConsultantTransferException;
use App\Http\Requests\UserTransfer\ExecuteConsultantTransferRequest;
use App\Http\Requests\UserTransfer\PreviewConsultantTransferRequest;
use App\Services\ConsultantTransferService;
use App\User;
use Auth;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
/**
* Superadmin-Seite "Berater übertragen": kompletter Berater- und Kundenstamm
* eines Beraters auf einen anderen Berater (dev/_tickets/2026-09-08.md, Ticket #1).
*/
class AdminUserTransferController extends Controller
{
public function __construct(protected ConsultantTransferService $transferService)
{
$this->middleware('superadmin');
}
public function index(): View
{
return view('admin.user.transfer.index', $this->viewData());
}
public function preview(PreviewConsultantTransferRequest $request): View|RedirectResponse
{
$source = User::findOrFail($request->integer('source_user_id'));
$target = User::findOrFail($request->integer('target_user_id'));
try {
$this->transferService->assertTransferable($source, $target);
} catch (ConsultantTransferException $e) {
return redirect(route('admin_user_transfer'))->withInput()->with('alert-error', $e->getMessage());
}
return view('admin.user.transfer.index', $this->viewData($source, $target, $this->transferService->preview($source)));
}
public function execute(ExecuteConsultantTransferRequest $request): RedirectResponse
{
$source = User::findOrFail($request->integer('source_user_id'));
$target = User::findOrFail($request->integer('target_user_id'));
try {
$summary = $this->transferService->transfer($source, $target, Auth::id());
} catch (ConsultantTransferException $e) {
return redirect(route('admin_user_transfer'))->withInput()->with('alert-error', $e->getMessage());
}
\Session()->flash('alert-success', __('msg.transfer_success', [
'firstline' => $summary['firstline'],
'customers' => $summary['customers'],
'abos' => $summary['abos'],
'target' => $target->email,
]));
return redirect(route('admin_user_transfer'));
}
/**
* @return array<string, mixed>
*/
protected function viewData(?User $source = null, ?User $target = null, ?array $preview = null): array
{
return [
'source' => $source,
'target' => $target,
'preview' => $preview,
'sourceOptions' => $this->userOptions(User::query()),
'targetOptions' => $this->userOptions(
User::where('active', true)->where('blocked', false)->where('payment_account', '>=', now())
),
];
}
/**
* Optionen für die Berater-Auswahl: "Vorname Nachname | E-Mail #M-Account".
*
* @param Builder<User> $query
* @return array<int|string, string>
*/
protected function userOptions($query): array
{
$options = ['' => __('please select')];
foreach ($query->with('account')->orderBy('email')->get() as $user) {
$options[$user->id] = $this->userLabel($user);
}
return $options;
}
public static function userLabel(User $user): string
{
$name = $user->account ? trim($user->account->first_name.' '.$user->account->last_name) : '';
$mAccount = $user->account && $user->account->m_account ? ' #'.$user->account->m_account : '';
return ($name !== '' ? $name.' | ' : '').$user->email.$mAccount;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\UserTransfer;
/**
* Ausführung der Übertragung: zusätzlich zur Auswahl ist die bewusste
* Bestätigung per Checkbox Pflicht — die Umhängung betrifft den ganzen Baum.
*/
class ExecuteConsultantTransferRequest extends PreviewConsultantTransferRequest
{
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return array_merge(parent::rules(), [
'confirm_transfer' => ['accepted'],
]);
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return array_merge(parent::messages(), [
'confirm_transfer.accepted' => __('msg.transfer_confirm_checkbox_missing'),
]);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Requests\UserTransfer;
use Illuminate\Foundation\Http\FormRequest;
/**
* Auswahl von Quell- und Zielberater für die Übertragung eines Berater-Stamms
* (dev/_tickets/2026-09-08.md, Ticket #1).
*/
class PreviewConsultantTransferRequest extends FormRequest
{
public function authorize(): bool
{
// Die Route liegt hinter der superadmin-Middleware.
return true;
}
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'source_user_id' => ['required', 'integer', 'exists:users,id'],
'target_user_id' => ['required', 'integer', 'exists:users,id', 'different:source_user_id'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'source_user_id.required' => __('msg.transfer_error_source_required'),
'target_user_id.required' => __('msg.transfer_error_target_required'),
'target_user_id.different' => __('msg.transfer_error_same_user'),
];
}
}