Punkte/Umsatz korrekt aus per-Zeile points/total_net je status berechnen statt SUM der kumulativen month_*-Snapshots (zaehlte massiv mehrfach). Zentralisiert in SalesVolumePoints, genutzt von Dashboard- & Drilldown-Service. Detail-Tabelle neu gegliedert: Eigenpunkte | Shop | Abo | Sonstige | Kundenpunkte (=Summe) | Gesamt, mit gruener Hervorhebung der Basis-/Summenspalten. Neue VIP-Seite "Meine Statistik" (Untermenue neben Team-Statistik): KPI-Karten + nachvollziehbare Detailtabellen, gruppiert nach eigenen Bestellungen, Shop-Bestellungen und Abos (PersonalStatisticsService). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
342 lines
14 KiB
PHP
342 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Backoffice;
|
|
|
|
use App\Models\UserAbo;
|
|
use App\Models\UserSalesVolume;
|
|
use App\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
class BackofficeDrilldownService
|
|
{
|
|
public function __construct(private BackofficeDashboardService $dashboardService) {}
|
|
|
|
/**
|
|
* @return array{metric: string, metric_label: string, line: int, line_label: string, month: int, year: int, rows: array<int, array<string, mixed>>, summary: array<string, mixed>}
|
|
*/
|
|
public function details(User $viewer, int $line, string $metric, int $month, int $year): array
|
|
{
|
|
$metricLabels = $this->dashboardService->metricLabels();
|
|
|
|
if (! array_key_exists($metric, $metricLabels)) {
|
|
abort(404);
|
|
}
|
|
|
|
$userIds = $this->dashboardService->lineUserIds($viewer->id, $line);
|
|
$rows = match ($metric) {
|
|
'consultants' => $this->consultantRows($userIds),
|
|
'new_partners' => $this->newPartnerRows($userIds, $month, $year),
|
|
'team_partner_abos' => $this->partnerAboRows($userIds, $month, $year),
|
|
'team_customer_abos' => $this->customerAboRows($userIds, $month, $year),
|
|
'own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000' => $this->pointsRows($userIds, $month, $year, $metric),
|
|
default => [],
|
|
};
|
|
|
|
return [
|
|
'metric' => $metric,
|
|
'metric_label' => $metricLabels[$metric],
|
|
'line' => $line,
|
|
'line_label' => $line > 0 ? 'Linie '.$line : 'Alle Linien',
|
|
'month' => $month,
|
|
'year' => $year,
|
|
'rows' => $rows,
|
|
'summary' => $this->summary($rows),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $rows
|
|
* @return array{count: int, abo_count: int, points: float, own_points: float, external_points: float, customer_abo_points: float, customer_single_order_points: float, customer_other_points: float, total_points: float, deliveries: int}
|
|
*/
|
|
private function summary(array $rows): array
|
|
{
|
|
return [
|
|
'count' => count($rows),
|
|
'abo_count' => (int) collect($rows)->sum('abo_count'),
|
|
'points' => (float) collect($rows)->sum('points'),
|
|
'own_points' => (float) collect($rows)->sum('own_points'),
|
|
'external_points' => (float) collect($rows)->sum('external_points'),
|
|
'customer_abo_points' => (float) collect($rows)->sum('customer_abo_points'),
|
|
'customer_single_order_points' => (float) collect($rows)->sum('customer_single_order_points'),
|
|
'customer_other_points' => (float) collect($rows)->sum('customer_other_points'),
|
|
'total_points' => (float) collect($rows)->sum('total_points'),
|
|
'deliveries' => (int) collect($rows)->sum('deliveries'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function consultantRows(array $userIds): array
|
|
{
|
|
return User::query()
|
|
->with(['account', 'user_level'])
|
|
->whereIn('id', $userIds)
|
|
->whereNotNull('m_level')
|
|
->whereNotNull('payment_account')
|
|
->orderBy('id')
|
|
->get()
|
|
->map(fn (User $user) => [
|
|
'type' => 'user',
|
|
'user_id' => $user->id,
|
|
'name' => $this->userName($user),
|
|
'email' => $user->email,
|
|
'career_level' => $this->careerLevel($user),
|
|
'is_account_active' => Carbon::parse($user->payment_account)->isFuture(),
|
|
'account_status' => Carbon::parse($user->payment_account)->isFuture() ? 'Aktiv' : 'Abgelaufen',
|
|
'active_date' => $this->formatDate($user->active_date),
|
|
'payment_account' => $this->formatDate($user->payment_account),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function newPartnerRows(array $userIds, int $month, int $year): array
|
|
{
|
|
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
|
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
|
|
|
return User::query()
|
|
->with(['account', 'user_level'])
|
|
->whereIn('id', $userIds)
|
|
->whereNotNull('m_level')
|
|
->whereNotNull('payment_account')
|
|
->whereBetween('active_date', [$startDate, $endDate])
|
|
->orderBy('active_date')
|
|
->get()
|
|
->filter(fn (User $user) => Carbon::parse($user->payment_account)->isFuture())
|
|
->map(fn (User $user) => [
|
|
'type' => 'user',
|
|
'user_id' => $user->id,
|
|
'name' => $this->userName($user),
|
|
'email' => $user->email,
|
|
'career_level' => $this->careerLevel($user),
|
|
'active_date' => $this->formatDate($user->active_date),
|
|
'payment_account' => $this->formatDate($user->payment_account),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function partnerAboRows(array $userIds, int $month, int $year): array
|
|
{
|
|
return $this->activeAboQuery()
|
|
->with(['user.account', 'user.user_level', 'user_abo_items.product', 'user_abo_orders.shopping_order.shopping_payments.payment_transactions'])
|
|
->whereIn('user_id', $userIds)
|
|
->where('is_for', 'me')
|
|
->orderBy('next_date')
|
|
->get()
|
|
->map(fn (UserAbo $abo) => [
|
|
'type' => 'abo',
|
|
'abo_id' => $abo->id,
|
|
'user_id' => $abo->user_id,
|
|
'name' => $abo->user ? $this->userName($abo->user) : '#'.$abo->user_id,
|
|
'email' => $abo->user?->email,
|
|
'career_level' => $abo->user ? $this->careerLevel($abo->user) : '-',
|
|
'points' => $abo->getTotalPoints(),
|
|
'start_date' => $this->formatDate($abo->getRawOriginal('start_date')),
|
|
'is_new_this_month' => $this->isAboNewInMonth($abo, $month, $year),
|
|
'next_date' => $this->formatDate($abo->next_date),
|
|
'deliveries' => $abo->getCountOrders(),
|
|
'status' => $abo->status,
|
|
'status_label' => $abo->getStatusType(),
|
|
'status_badge' => $abo->getStatusFormated(),
|
|
'status_reason' => $this->aboStatusReason($abo),
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* Teamkundenabos pro Teampartner (member_id) gruppiert: je Teampartner die
|
|
* Anzahl der Kundenabos, die Gesamt-Volumenpunkte sowie die einzelnen Abos
|
|
* mit Punkten je Abo. Kundennamen bleiben aus Datenschutzgründen außen vor.
|
|
*
|
|
* @param int[] $userIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function customerAboRows(array $userIds, int $month, int $year): array
|
|
{
|
|
return $this->activeAboQuery()
|
|
->with(['member.account', 'member.user_level', 'user_abo_items.product'])
|
|
->whereIn('member_id', $userIds)
|
|
->where('is_for', 'ot')
|
|
->orderBy('member_id')
|
|
->orderBy('next_date')
|
|
->get()
|
|
->groupBy(fn (UserAbo $abo) => (int) $abo->member_id)
|
|
->map(function ($abos) use ($month, $year) {
|
|
$consultant = $abos->first()->member;
|
|
$memberId = (int) $abos->first()->member_id;
|
|
|
|
$aboItems = $abos->map(fn (UserAbo $abo) => [
|
|
'abo_id' => $abo->id,
|
|
'points' => (float) $abo->getTotalPoints(),
|
|
'start_date' => $this->formatDate($abo->getRawOriginal('start_date')),
|
|
'next_date' => $this->formatDate($abo->next_date),
|
|
'is_new_this_month' => $this->isAboNewInMonth($abo, $month, $year),
|
|
])->values()->all();
|
|
|
|
return [
|
|
'type' => 'customer_abo_group',
|
|
'consultant_id' => $memberId,
|
|
'name' => $consultant ? $this->userName($consultant) : '#'.$memberId,
|
|
'email' => $consultant?->email,
|
|
'career_level' => $consultant ? $this->careerLevel($consultant) : '-',
|
|
'abo_count' => $abos->count(),
|
|
'points' => (float) $abos->sum(fn (UserAbo $abo) => $abo->getTotalPoints()),
|
|
'abos' => $aboItems,
|
|
];
|
|
})
|
|
->sortByDesc('points')
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param int[] $userIds
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function pointsRows(array $userIds, int $month, int $year, string $metric): array
|
|
{
|
|
$rows = UserSalesVolume::query()
|
|
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
|
->whereIn('user_id', $userIds)
|
|
->where('month', $month)
|
|
->where('year', $year)
|
|
->select('user_sales_volumes.user_id')
|
|
->selectRaw(SalesVolumePoints::ownPoints().' as own_points')
|
|
->selectRaw(SalesVolumePoints::externalPoints().' as external_points')
|
|
->selectRaw(SalesVolumePoints::customerAboPoints().' as customer_abo_points')
|
|
->selectRaw(SalesVolumePoints::customerSingleOrderPoints().' as customer_single_order_points')
|
|
->selectRaw(SalesVolumePoints::customerOtherPoints().' as customer_other_points')
|
|
->selectRaw(SalesVolumePoints::totalPoints().' as total_points')
|
|
->groupBy('user_sales_volumes.user_id');
|
|
|
|
if ($metric === 'own_points') {
|
|
$rows->havingRaw(SalesVolumePoints::ownPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'external_points') {
|
|
$rows->havingRaw(SalesVolumePoints::externalPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'customer_abo_points') {
|
|
$rows->havingRaw(SalesVolumePoints::customerAboPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'customer_single_order_points') {
|
|
$rows->havingRaw(SalesVolumePoints::customerSingleOrderPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'customer_other_points') {
|
|
$rows->havingRaw(SalesVolumePoints::customerOtherPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'total_points') {
|
|
$rows->havingRaw(SalesVolumePoints::totalPoints().' > 0');
|
|
}
|
|
|
|
if ($metric === 'shop_1000') {
|
|
$rows->havingRaw(SalesVolumePoints::totalPoints().' >= 1000');
|
|
}
|
|
|
|
$salesRows = $rows->orderByDesc('total_points')->get();
|
|
$users = User::query()->with(['account', 'user_level'])->whereIn('id', $salesRows->pluck('user_id'))->get()->keyBy('id');
|
|
|
|
return $salesRows
|
|
->map(fn (UserSalesVolume $row) => [
|
|
'type' => 'points',
|
|
'user_id' => $row->user_id,
|
|
'name' => $users->has($row->user_id) ? $this->userName($users->get($row->user_id)) : '#'.$row->user_id,
|
|
'email' => $users->get($row->user_id)?->email,
|
|
'career_level' => $users->has($row->user_id) ? $this->careerLevel($users->get($row->user_id)) : '-',
|
|
'own_points' => (float) $row->own_points,
|
|
'external_points' => (float) $row->external_points,
|
|
'customer_abo_points' => (float) $row->customer_abo_points,
|
|
'customer_single_order_points' => (float) $row->customer_single_order_points,
|
|
'customer_other_points' => (float) $row->customer_other_points,
|
|
'total_points' => (float) $row->total_points,
|
|
])
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function activeAboQuery(): Builder
|
|
{
|
|
return UserAbo::query()
|
|
->where('active', true)
|
|
->whereNotIn('status', [4, 5, 6]);
|
|
}
|
|
|
|
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 : '-');
|
|
}
|
|
|
|
private function aboStatusReason(UserAbo $abo): ?string
|
|
{
|
|
if ((int) $abo->status === 2) {
|
|
return null;
|
|
}
|
|
|
|
$transaction = $abo->user_abo_orders
|
|
->sortByDesc('created_at')
|
|
->pluck('shopping_order')
|
|
->filter()
|
|
->map(fn ($order) => $order->getLastShoppingPaymentTransaction())
|
|
->filter()
|
|
->first();
|
|
|
|
if (! $transaction) {
|
|
return null;
|
|
}
|
|
|
|
$message = $transaction->errormessage ?: $transaction->customermessage;
|
|
|
|
if (! $message) {
|
|
return null;
|
|
}
|
|
|
|
return $transaction->errorcode ? '['.$transaction->errorcode.'] '.$message : $message;
|
|
}
|
|
|
|
private function isAboNewInMonth(UserAbo $abo, int $month, int $year): bool
|
|
{
|
|
$startDate = $abo->getRawOriginal('start_date');
|
|
|
|
if (! $startDate) {
|
|
return false;
|
|
}
|
|
|
|
$date = Carbon::parse($startDate);
|
|
|
|
return (int) $date->month === $month && (int) $date->year === $year;
|
|
}
|
|
|
|
private function formatDate(mixed $date): ?string
|
|
{
|
|
if ($date === null || $date === '') {
|
|
return null;
|
|
}
|
|
|
|
return Carbon::parse($date)->format('d.m.Y');
|
|
}
|
|
}
|