Backoffice-Statistik: Neue-Kundenabos-Ranking & Führungskräfte-Abos

Zwei neue Listen unter der Team-Übersicht: Ranking der Berater nach neuen
Kundenabos im Monat (inkl. Gesamtbestand und Linie) sowie eigene und
Kundenabos aller Führungskräfte ab Stufe Vertriebsleiter mit Gesamtsumme.
Beides fließt mit in die Monats-Snapshots ein.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-07-03 16:24:01 +00:00
parent e7b40bc513
commit ce07bee589
3 changed files with 417 additions and 1 deletions

View file

@ -4,6 +4,7 @@ namespace App\Services\Backoffice;
use App\Models\BackofficeStatisticsSnapshot;
use App\Models\UserAbo;
use App\Models\UserLevel;
use App\Models\UserSalesVolume;
use App\User;
use Carbon\Carbon;
@ -14,6 +15,8 @@ class BackofficeDashboardService
{
private const MAX_DEPTH_SAFETY_LIMIT = 100;
private const LEADER_LEVEL_NAME = 'Vertriebsleiter';
/**
* @return array<string, string>
*/
@ -94,6 +97,8 @@ class BackofficeDashboardService
'metric_labels' => $this->metricLabels(),
'lines' => $lines,
'totals' => $totals,
'new_customer_abos' => $this->newCustomerAboRanking($lineBuckets, $month, $year),
'leader_abos' => $this->leaderAboOverview($lineBuckets),
];
}
@ -128,7 +133,7 @@ class BackofficeDashboardService
for ($line = 1; $line <= self::MAX_DEPTH_SAFETY_LIMIT && $currentSponsorIds !== []; $line++) {
$users = User::query()
->with('account')
->with(['account', 'user_level'])
->whereIn('m_sponsor', $currentSponsorIds)
->whereNotIn('id', $visitedUserIds)
->whereColumn('id', '!=', 'm_sponsor')
@ -395,6 +400,156 @@ class BackofficeDashboardService
->count();
}
/**
* Ranking: Wer im Team hat im Monat wie viele neue Kundenabos abgeschlossen?
* Sortiert nach Anzahl absteigend (nicht nach Linie), Linie wird je Berater mitgeliefert,
* dazu der aktuelle Gesamtbestand an Kundenabos des Beraters.
* Zählweise identisch zu team_customer_abos_new {@see newAboCount()}.
*
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
* @return array<int, array{user_id: int, name: string, career_level: string, line: int, abo_count: int, total_abo_count: int}>
*/
private function newCustomerAboRanking(array $lineBuckets, int $month, int $year): array
{
$usersByLine = $this->usersWithLine($lineBuckets);
if ($usersByLine === []) {
return [];
}
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
$counts = $this->activeAboQuery()
->whereIn('member_id', array_keys($usersByLine))
->where('is_for', 'ot')
->whereBetween('start_date', [$startDate, $endDate])
->selectRaw('member_id, COUNT(*) as abo_count')
->groupBy('member_id')
->pluck('abo_count', 'member_id');
$totalCounts = $this->customerAboCounts($counts->keys()->all());
return collect($usersByLine)
->filter(fn (array $entry, int $userId) => $counts->has($userId))
->map(fn (array $entry, int $userId) => [
'user_id' => $userId,
'name' => $this->userName($entry['user']),
'career_level' => $this->careerLevel($entry['user']),
'line' => $entry['line'],
'abo_count' => (int) $counts->get($userId),
'total_abo_count' => (int) $totalCounts->get($userId, 0),
])
->sortByDesc('abo_count')
->values()
->all();
}
/**
* Abos der Führungskräfte (ab Stufe Vertriebsleiter) im Team: Gesamtzahlen plus
* Liste aller Führungskräfte inklusive Linie, eigener Abos und Kundenabos.
*
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
* @return array{level_name: string, total: int, total_own: int, total_customer: int, rows: array<int, array{user_id: int, name: string, career_level: string, line: int, own_abo_count: int, customer_abo_count: int}>}
*/
private function leaderAboOverview(array $lineBuckets): array
{
$leaderMinPos = UserLevel::query()
->where('name', self::LEADER_LEVEL_NAME)
->value('pos');
$leadersByLine = $leaderMinPos === null ? [] : collect($this->usersWithLine($lineBuckets))
->filter(fn (array $entry) => ($entry['user']->user_level?->pos ?? 0) >= $leaderMinPos)
->all();
if ($leadersByLine === []) {
return ['level_name' => self::LEADER_LEVEL_NAME, 'total' => 0, 'total_own' => 0, 'total_customer' => 0, 'rows' => []];
}
$leaderIds = array_keys($leadersByLine);
$ownCounts = $this->activeAboQuery()
->whereIn('user_id', $leaderIds)
->where('is_for', 'me')
->selectRaw('user_id, COUNT(*) as abo_count')
->groupBy('user_id')
->pluck('abo_count', 'user_id');
$customerCounts = $this->customerAboCounts($leaderIds);
$rows = collect($leadersByLine)
->map(fn (array $entry, int $userId) => [
'user_id' => $userId,
'name' => $this->userName($entry['user']),
'career_level' => $this->careerLevel($entry['user']),
'line' => $entry['line'],
'own_abo_count' => (int) $ownCounts->get($userId, 0),
'customer_abo_count' => (int) $customerCounts->get($userId, 0),
])
->sortByDesc(fn (array $row) => $row['own_abo_count'] + $row['customer_abo_count'])
->values();
$totalOwn = (int) $rows->sum('own_abo_count');
$totalCustomer = (int) $rows->sum('customer_abo_count');
return [
'level_name' => self::LEADER_LEVEL_NAME,
'total' => $totalOwn + $totalCustomer,
'total_own' => $totalOwn,
'total_customer' => $totalCustomer,
'rows' => $rows->all(),
];
}
/**
* Aktueller Bestand aktiver Kundenabos je Berater (member_id).
*
* @param int[] $memberIds
* @return \Illuminate\Support\Collection<int, int>
*/
private function customerAboCounts(array $memberIds): Collection
{
if ($memberIds === []) {
return collect();
}
return $this->activeAboQuery()
->whereIn('member_id', $memberIds)
->where('is_for', 'ot')
->selectRaw('member_id, COUNT(*) as abo_count')
->groupBy('member_id')
->pluck('abo_count', 'member_id');
}
/**
* @param array<int, \Illuminate\Support\Collection<int, \App\User>> $lineBuckets
* @return array<int, array{user: \App\User, line: int}>
*/
private function usersWithLine(array $lineBuckets): array
{
$usersByLine = [];
foreach ($lineBuckets as $line => $users) {
foreach ($users as $user) {
$usersByLine[(int) $user->id] = ['user' => $user, 'line' => $line];
}
}
return $usersByLine;
}
private function userName(User $user): string
{
$name = trim(($user->account?->first_name ?? '').' '.($user->account?->last_name ?? ''));
return $name !== '' ? $name : ($user->email ?: '#'.$user->id);
}
private function careerLevel(User $user): string
{
return $user->user_level?->name ?: ($user->m_level ? 'Level '.$user->m_level : '-');
}
/**
* Anzahl der Berater mit >= 1000 Punkten Kundenumsatz (KP + Shop) im Monat.
*