- E-Mail-Aenderung durch Berater wird erst nach Bestaetigung durch den Kunden aktiv (Link an die neue Adresse, Portal kunde/email-bestaetigen); danach ziehen alle Eintraege des Kunden und seine Abos mit. Admins aendern direkt. - Kundenhoheit: E-Mail eines Beraterkontos oder eines Kunden eines anderen Beraters wird abgelehnt (beim Speichern und bei der Bestaetigung). - Fix CustomerPriority::entryExists: aelteste gepruefte Zuordnung gilt, Eintraege in Pruefung (is_like) dienen nicht mehr als Vorlage. - "Berater zuweisen" voreingestellt auf alle Bestellungen des Kunden. - Neue Admin-Seite Kundenhoheit: offene Pruefungen, Kunden bei mehreren Beratern, abgelehnte/offene E-Mail-Aenderungen; Zaehler im Menue. - Neue Tabelle shopping_user_email_changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
10 KiB
PHP
181 lines
10 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\ShoppingUser;
|
||
use App\Models\ShoppingUserEmailChange;
|
||
use App\Services\CustomerEmailChangeService;
|
||
use App\Services\CustomerPriority;
|
||
use App\Services\CustomerPriorityOverviewService;
|
||
use App\User;
|
||
use Illuminate\Contracts\View\View;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\RedirectResponse;
|
||
|
||
/**
|
||
* Admin overview "Kundenhoheit": everything where the customer sovereignty still needs a
|
||
* decision, so the work does not depend on info mails that get lost.
|
||
*/
|
||
class CustomerPriorityController extends Controller
|
||
{
|
||
public const TABS = ['reviews', 'conflicts', 'email-changes'];
|
||
|
||
public function __construct(private CustomerPriorityOverviewService $overview)
|
||
{
|
||
$this->middleware('admin');
|
||
}
|
||
|
||
public function index(string $tab = 'reviews'): View
|
||
{
|
||
abort_unless(in_array($tab, self::TABS, true), 404);
|
||
|
||
return view('admin.customer_priority.index', [
|
||
'tab' => $tab,
|
||
'counts' => $this->overview->counts(),
|
||
]);
|
||
}
|
||
|
||
public function reviewsDatatable(): JsonResponse
|
||
{
|
||
$back = route('admin_customer_priority', ['reviews']);
|
||
|
||
return \DataTables::eloquent($this->overview->reviewsQuery())
|
||
->addColumn('created', fn (ShoppingUser $customer): string => $customer->created_at->format('d.m.Y H:i')
|
||
.'<br><small class="text-muted">'.$customer->created_at->diffForHumans().'</small>')
|
||
->addColumn('customer', fn (ShoppingUser $customer): string => $this->customerCell($customer))
|
||
->addColumn('member', fn (ShoppingUser $customer): string => $this->memberCell($customer->member))
|
||
->addColumn('shop', fn (ShoppingUser $customer): string => $this->memberCell($customer->shopping_order?->user_shop?->user))
|
||
->addColumn('matches', fn (ShoppingUser $customer): string => (string) count($customer->getNotice('like') ?: []))
|
||
->addColumn('action', function (ShoppingUser $customer) use ($back): string {
|
||
return '<button type="button" class="btn btn-sm btn-primary mb-1" data-toggle="modal" data-target="#modals-load-content"'
|
||
.' data-id="'.$customer->id.'" data-action="shopping-user-is-like-member" data-back="'.e($back).'"'
|
||
.' data-modal="modal-xl" data-route="'.route('modal_load').'"><i class="fa fa-edit"></i> Prüfen & zuordnen</button> '
|
||
.'<form method="POST" action="'.route('admin_customer_priority_keep', [$customer->id]).'" class="d-inline"'
|
||
.' onsubmit="return confirm(\'Prüffall schließen? Berater und Kundennummer bleiben unverändert.\')">'
|
||
.csrf_field()
|
||
.'<button type="submit" class="btn btn-sm btn-outline-secondary mb-1">Zuordnung beibehalten</button></form>';
|
||
})
|
||
->filterColumn('customer', function ($query, $keyword): void {
|
||
$query->where(function ($query) use ($keyword) {
|
||
$query->where('billing_email', 'like', '%'.$keyword.'%')
|
||
->orWhere('billing_lastname', 'like', '%'.$keyword.'%')
|
||
->orWhere('billing_firstname', 'like', '%'.$keyword.'%')
|
||
->orWhere('billing_zipcode', 'like', '%'.$keyword.'%');
|
||
});
|
||
})
|
||
->orderColumn('created', 'shopping_users.created_at $1')
|
||
->rawColumns(['created', 'customer', 'member', 'shop', 'action'])
|
||
->make(true);
|
||
}
|
||
|
||
public function conflictsDatatable(): JsonResponse
|
||
{
|
||
$back = route('admin_customer_priority', ['conflicts']);
|
||
|
||
return \DataTables::of($this->overview->conflicts())
|
||
->addColumn('customer', fn (array $row): string => '<strong>'.e($row['name']).'</strong><br>'.e($row['email']))
|
||
->addColumn('groups', function (array $row) use ($back): string {
|
||
$html = '';
|
||
foreach ($row['groups'] as $group) {
|
||
$html .= '<div class="mb-2">'
|
||
.($group['oldest'] ? '<span class="badge badge-success">älteste Zuordnung</span> ' : '<span class="badge badge-warning">weitere Zuordnung</span> ')
|
||
.$this->memberCell($group['member'])
|
||
.' <span class="text-muted">· Kunden-Nr. '.e($group['number']).' · '.$group['entries'].' Einträge · zuletzt '.e(date('d.m.Y', strtotime($group['last_at']))).'</span> '
|
||
.'<a href="'.route('admin_customer_detail', [$group['latest_id']]).'" class="btn btn-xs btn-outline-primary"><i class="fa fa-eye"></i></a> '
|
||
.'<button type="button" class="btn btn-xs btn-outline-secondary" data-toggle="modal" data-target="#modals-load-content"'
|
||
.' data-id="'.$group['latest_id'].'" data-action="shopping-user-change-member" data-view="customer" data-back="'.e($back).'"'
|
||
.' data-route="'.route('modal_load').'"><i class="fa fa-edit"></i> Berater zuweisen</button>'
|
||
.'</div>';
|
||
}
|
||
|
||
return $html;
|
||
})
|
||
->addColumn('last', fn (array $row): string => date('d.m.Y', strtotime($row['last_at'])))
|
||
->filter(function ($instance): void {
|
||
$keyword = mb_strtolower((string) request('search.value'));
|
||
if ($keyword !== '') {
|
||
$instance->collection = $instance->collection->filter(
|
||
fn (array $row) => str_contains($row['email'], $keyword) || str_contains(mb_strtolower($row['name']), $keyword)
|
||
);
|
||
}
|
||
})
|
||
->rawColumns(['customer', 'groups'])
|
||
->make(true);
|
||
}
|
||
|
||
public function emailChangesDatatable(): JsonResponse
|
||
{
|
||
return \DataTables::eloquent($this->overview->emailChangesQuery())
|
||
->addColumn('created', fn (ShoppingUserEmailChange $change): string => $change->created_at->format('d.m.Y H:i'))
|
||
->addColumn('customer', fn (ShoppingUserEmailChange $change): string => $change->shopping_user
|
||
? $this->customerCell($change->shopping_user)
|
||
: '<span class="text-muted">Kunde #'.$change->shopping_user_id.' gelöscht</span>')
|
||
->addColumn('change', fn (ShoppingUserEmailChange $change): string => e($change->old_email).' <i class="fa fa-arrow-right"></i> <strong>'.e($change->new_email).'</strong>')
|
||
->addColumn('requested_by', fn (ShoppingUserEmailChange $change): string => $this->memberCell($change->requested_by))
|
||
->addColumn('status', function (ShoppingUserEmailChange $change): string {
|
||
if ($change->blocked_at) {
|
||
return '<span class="badge badge-danger">abgelehnt (Kundenhoheit)</span><br><small class="text-muted">Adresse gehört einem anderen Berater oder Beraterkonto</small>';
|
||
}
|
||
|
||
return '<span class="badge badge-info">wartet auf Bestätigung</span><br><small class="text-muted">Link gültig bis '.$change->expires_at->format('d.m.Y H:i').'</small>';
|
||
})
|
||
->addColumn('action', function (ShoppingUserEmailChange $change): string {
|
||
if ($change->blocked_at) {
|
||
return '<form method="POST" action="'.route('admin_customer_priority_email_resolve', [$change->id]).'" class="d-inline">'
|
||
.csrf_field().'<button type="submit" class="btn btn-sm btn-outline-success"><i class="fa fa-check"></i> Erledigt</button></form>';
|
||
}
|
||
|
||
return '<form method="POST" action="'.route('admin_customer_priority_email_discard', [$change->id]).'" class="d-inline"'
|
||
.' onsubmit="return confirm(\'Anfrage verwerfen? Der Link in der Bestätigungsmail funktioniert dann nicht mehr.\')">'
|
||
.csrf_field().'<button type="submit" class="btn btn-sm btn-outline-danger">Verwerfen</button></form>';
|
||
})
|
||
->orderColumn('created', 'shopping_user_email_changes.created_at $1')
|
||
->rawColumns(['customer', 'change', 'requested_by', 'status', 'action'])
|
||
->make(true);
|
||
}
|
||
|
||
public function keepAssignment(int $id): RedirectResponse
|
||
{
|
||
$customer = ShoppingUser::where('is_like', true)->findOrFail($id);
|
||
CustomerPriority::keepAssignment($customer);
|
||
\Session()->flash('alert-success', 'Prüffall geschlossen, Zuordnung beibehalten.');
|
||
|
||
return redirect(route('admin_customer_priority', ['reviews']));
|
||
}
|
||
|
||
public function resolveEmailChange(int $id, CustomerEmailChangeService $service): RedirectResponse
|
||
{
|
||
$service->resolveBlocked(ShoppingUserEmailChange::blockedOpen()->findOrFail($id), \Auth::id());
|
||
\Session()->flash('alert-success', 'Als erledigt markiert.');
|
||
|
||
return redirect(route('admin_customer_priority', ['email-changes']));
|
||
}
|
||
|
||
public function discardEmailChange(int $id, CustomerEmailChangeService $service): RedirectResponse
|
||
{
|
||
$service->discard(ShoppingUserEmailChange::pending()->findOrFail($id));
|
||
\Session()->flash('alert-success', 'Anfrage verworfen.');
|
||
|
||
return redirect(route('admin_customer_priority', ['email-changes']));
|
||
}
|
||
|
||
private function customerCell(ShoppingUser $customer): string
|
||
{
|
||
return '<a href="'.route('admin_customer_detail', [$customer->id]).'"><strong>'
|
||
.e(trim($customer->billing_firstname.' '.$customer->billing_lastname)).'</strong></a>'
|
||
.($customer->number ? ' <span class="text-muted">#'.e($customer->number).'</span>' : '')
|
||
.'<br>'.e($customer->billing_email)
|
||
.'<br><small class="text-muted">'.e($customer->billing_zipcode.' '.$customer->billing_city).'</small>';
|
||
}
|
||
|
||
private function memberCell(?User $member): string
|
||
{
|
||
if (! $member) {
|
||
return '<span class="text-muted">–</span>';
|
||
}
|
||
|
||
return '<a href="'.route('admin_lead_edit', [$member->id]).'">'.e($member->getFullName(false) ?: $member->email).'</a>'
|
||
.' <span class="text-muted">#'.e($member->number).'</span>';
|
||
}
|
||
}
|