User Statistik
This commit is contained in:
parent
70240d2b6a
commit
53bdba33cd
24 changed files with 2633 additions and 9 deletions
127
app/Console/Commands/BackofficeStoreStatisticsSnapshots.php
Normal file
127
app/Console/Commands/BackofficeStoreStatisticsSnapshots.php
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BackofficeStatisticsSnapshot;
|
||||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class BackofficeStoreStatisticsSnapshots extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'backoffice:store-statistics-snapshots
|
||||
{--user= : Nur einen bestimmten User berechnen (user_id)}
|
||||
{--month= : Nur einen bestimmten Monat berechnen}
|
||||
{--year= : Nur ein bestimmtes Jahr berechnen}
|
||||
{--force : Bereits vorhandene Snapshots überschreiben}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Speichert Backoffice-Statistik-Snapshots fuer abgeschlossene Monate';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(BackofficeDashboardService $dashboardService): int
|
||||
{
|
||||
$months = $this->monthsToStore();
|
||||
$force = (bool) $this->option('force');
|
||||
|
||||
if ($months === []) {
|
||||
$this->info('Keine abgeschlossenen Monate zum Speichern.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$userQuery = User::query()
|
||||
->where('admin', '>=', 1)
|
||||
->where('admin', '<', 4)
|
||||
->whereNull('deleted_at');
|
||||
|
||||
if ($userId = $this->option('user')) {
|
||||
$userQuery->where('id', $userId);
|
||||
}
|
||||
|
||||
$users = $userQuery->get();
|
||||
$this->info('Berechne Backoffice-Snapshots fuer '.$users->count().' User und '.count($months).' Monate...');
|
||||
|
||||
$bar = $this->output->createProgressBar($users->count());
|
||||
$bar->start();
|
||||
|
||||
$stored = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
foreach ($months as [$year, $month]) {
|
||||
$exists = BackofficeStatisticsSnapshot::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('year', $year)
|
||||
->where('month', $month)
|
||||
->exists();
|
||||
|
||||
if ($exists && ! $force) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$dashboardService->storeSnapshot($user, $month, $year);
|
||||
$stored++;
|
||||
}
|
||||
|
||||
$bar->advance();
|
||||
gc_collect_cycles();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
$this->info("Fertig. Gespeichert: {$stored}, uebersprungen: {$skipped}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{int, int}>
|
||||
*/
|
||||
private function monthsToStore(): array
|
||||
{
|
||||
$monthOption = $this->option('month');
|
||||
$yearOption = $this->option('year');
|
||||
|
||||
if ($monthOption && $yearOption) {
|
||||
$month = max(1, min(12, (int) $monthOption));
|
||||
$year = (int) $yearOption;
|
||||
|
||||
if (! $this->isClosedMonth($month, $year)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[$year, $month]];
|
||||
}
|
||||
|
||||
$months = [];
|
||||
$cursor = Carbon::create(2026, 1, 1)->startOfMonth();
|
||||
$lastClosedMonth = now()->startOfMonth()->subMonth();
|
||||
|
||||
while ($cursor->lte($lastClosedMonth)) {
|
||||
$months[] = [(int) $cursor->year, (int) $cursor->month];
|
||||
$cursor->addMonth();
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
private function isClosedMonth(int $month, int $year): bool
|
||||
{
|
||||
return Carbon::create($year, $month, 1)->endOfMonth()->lt(now()->startOfMonth());
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,9 @@ class Kernel extends ConsoleKernel
|
|||
// Abo-Chart-Snapshots: vergangene Monate einfrieren (nach allen Abo-Jobs)
|
||||
$schedule->command('abo:store-chart-snapshots')->dailyAt('04:30');
|
||||
|
||||
// Backoffice-Statistik-Snapshots: abgeschlossene Monate fuer VIP-Statistiken einfrieren
|
||||
$schedule->command('backoffice:store-statistics-snapshots')->dailyAt('04:45');
|
||||
|
||||
// Incentive: Punkteberechnung täglich nach business:store-optimized
|
||||
$schedule->command('incentive:calculate')->dailyAt('05:00');
|
||||
|
||||
|
|
|
|||
279
app/Http/Controllers/User/BackofficeStatisticsController.php
Normal file
279
app/Http/Controllers/User/BackofficeStatisticsController.php
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Backoffice\BackofficeDashboardService;
|
||||
use App\Services\Backoffice\BackofficeDrilldownService;
|
||||
use App\Services\HTMLHelper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class BackofficeStatisticsController extends Controller
|
||||
{
|
||||
private const SESSION_MONTH_KEY = 'backoffice_statistics_month';
|
||||
|
||||
private const SESSION_YEAR_KEY = 'backoffice_statistics_year';
|
||||
|
||||
public function __construct(
|
||||
private BackofficeDashboardService $dashboardService,
|
||||
private BackofficeDrilldownService $drilldownService
|
||||
) {
|
||||
$this->middleware('active.account');
|
||||
}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
if (! $request->user()?->isVIP()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$startTime = microtime(true);
|
||||
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
|
||||
$performance = [
|
||||
'duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
|
||||
'source_label' => $statistics['_meta']['source_label'] ?? 'Live',
|
||||
'calculated_at' => $statistics['_meta']['calculated_at'] ?? null,
|
||||
];
|
||||
|
||||
return view('user.backoffice.statistics.index', [
|
||||
'selectedMonth' => $selectedMonth,
|
||||
'selectedYear' => $selectedYear,
|
||||
'filterMonths' => HTMLHelper::getTransMonths(),
|
||||
'filterYears' => HTMLHelper::getYearRange(2022),
|
||||
'statistics' => $statistics,
|
||||
'performance' => $performance,
|
||||
]);
|
||||
}
|
||||
|
||||
public function details(Request $request): View
|
||||
{
|
||||
if (! $request->user()?->isVIP()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$line = (int) $request->get('line', 1);
|
||||
$metric = (string) $request->get('metric', 'consultants');
|
||||
|
||||
return view('user.backoffice.statistics.details', [
|
||||
'selectedMonth' => $selectedMonth,
|
||||
'selectedYear' => $selectedYear,
|
||||
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear),
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
if (! $request->user()?->isVIP()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$line = (int) $request->get('line', 1);
|
||||
$metric = (string) $request->get('metric', 'consultants');
|
||||
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear);
|
||||
$filename = sprintf(
|
||||
'backoffice-statistik-%s-linie-%s-%02d-%d.csv',
|
||||
$metric,
|
||||
$line === 0 ? 'alle' : $line,
|
||||
$selectedMonth,
|
||||
$selectedYear
|
||||
);
|
||||
|
||||
return response()->streamDownload(function () use ($details): void {
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, $this->csvHeaders($details['metric']), ';');
|
||||
|
||||
foreach ($details['rows'] as $row) {
|
||||
fputcsv($output, $this->csvRow($details['metric'], $row), ';');
|
||||
}
|
||||
|
||||
fputcsv($output, []);
|
||||
fputcsv($output, $this->csvSummaryRow($details), ';');
|
||||
|
||||
fclose($output);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
public function overviewExport(Request $request): StreamedResponse
|
||||
{
|
||||
if (! $request->user()?->isVIP()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
|
||||
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
|
||||
$filename = sprintf('backoffice-statistik-uebersicht-%02d-%d.csv', $selectedMonth, $selectedYear);
|
||||
|
||||
return response()->streamDownload(function () use ($statistics): void {
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
fputcsv($output, $this->overviewCsvHeaders(), ';');
|
||||
|
||||
foreach ($statistics['lines'] as $line) {
|
||||
fputcsv($output, $this->overviewCsvRow($line), ';');
|
||||
}
|
||||
|
||||
fputcsv($output, []);
|
||||
fputcsv($output, $this->overviewCsvRow($statistics['totals']), ';');
|
||||
|
||||
fclose($output);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int}
|
||||
*/
|
||||
private function selectedPeriod(Request $request): array
|
||||
{
|
||||
$selectedMonth = max(1, min(12, (int) $request->get('month', session(self::SESSION_MONTH_KEY, now()->month))));
|
||||
$selectedYear = (int) $request->get('year', session(self::SESSION_YEAR_KEY, now()->year));
|
||||
|
||||
session([
|
||||
self::SESSION_MONTH_KEY => $selectedMonth,
|
||||
self::SESSION_YEAR_KEY => $selectedYear,
|
||||
]);
|
||||
|
||||
return [$selectedMonth, $selectedYear];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function overviewCsvHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Linie',
|
||||
'Berater',
|
||||
'Neupartner',
|
||||
'Teamabos',
|
||||
'Neue Teamabos',
|
||||
'Teamkundenabos',
|
||||
'Neue Teamkundenabos',
|
||||
'Eigenpunkte',
|
||||
'Externe Punkte',
|
||||
'Kundenabo-Punkte',
|
||||
'Einzelbestellungs-Punkte',
|
||||
'Sonstige Kundenpunkte',
|
||||
'Gesamtpunkte',
|
||||
'1000 Punkte Shop',
|
||||
'Umsatz Netto',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function overviewCsvRow(array $row): array
|
||||
{
|
||||
return [
|
||||
$row['label'] ?? '',
|
||||
$row['consultants'] ?? 0,
|
||||
$row['new_partners'] ?? 0,
|
||||
$row['team_partner_abos'] ?? 0,
|
||||
$row['team_partner_abos_new'] ?? 0,
|
||||
$row['team_customer_abos'] ?? 0,
|
||||
$row['team_customer_abos_new'] ?? 0,
|
||||
$row['own_points'] ?? 0,
|
||||
$row['external_points'] ?? 0,
|
||||
$row['customer_abo_points'] ?? 0,
|
||||
$row['customer_single_order_points'] ?? 0,
|
||||
$row['customer_other_points'] ?? 0,
|
||||
$row['total_points'] ?? 0,
|
||||
$row['shop_1000'] ?? 0,
|
||||
$row['turnover_net'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function csvHeaders(string $metric): array
|
||||
{
|
||||
if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) {
|
||||
return ['Name', 'E-Mail', 'Karriere-Level', 'Berater', 'Abo-Punkte', 'Status', 'Status-Grund', 'Besteht seit', 'Naechste Ausfuehrung', 'Lieferungen'];
|
||||
}
|
||||
|
||||
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
|
||||
return ['Name', 'E-Mail', 'Karriere-Level', 'Eigenpunkte', 'Externe Punkte', 'Kundenabo-Punkte', 'Einzelbestellungs-Punkte', 'Sonstige Kundenpunkte', 'Gesamtpunkte'];
|
||||
}
|
||||
|
||||
return ['Name', 'E-Mail', 'Karriere-Level', 'Aktiv seit', 'Account gueltig bis', 'Account Status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function csvRow(string $metric, array $row): array
|
||||
{
|
||||
if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) {
|
||||
return [
|
||||
$row['name'] ?? '',
|
||||
$row['email'] ?? '',
|
||||
$row['career_level'] ?? '',
|
||||
$row['consultant_name'] ?? $row['name'] ?? '',
|
||||
$row['points'] ?? 0,
|
||||
$row['status_label'] ?? '',
|
||||
$row['status_reason'] ?? '',
|
||||
$row['start_date'] ?? '',
|
||||
$row['next_date'] ?? '',
|
||||
$row['deliveries'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
|
||||
return [
|
||||
$row['name'] ?? '',
|
||||
$row['email'] ?? '',
|
||||
$row['career_level'] ?? '',
|
||||
$row['own_points'] ?? 0,
|
||||
$row['external_points'] ?? 0,
|
||||
$row['customer_abo_points'] ?? 0,
|
||||
$row['customer_single_order_points'] ?? 0,
|
||||
$row['customer_other_points'] ?? 0,
|
||||
$row['total_points'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
$row['name'] ?? '',
|
||||
$row['email'] ?? '',
|
||||
$row['career_level'] ?? '',
|
||||
$row['active_date'] ?? '',
|
||||
$row['payment_account'] ?? '',
|
||||
$row['account_status'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
private function csvSummaryRow(array $details): array
|
||||
{
|
||||
$metric = $details['metric'];
|
||||
$summary = $details['summary'];
|
||||
|
||||
if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) {
|
||||
return ['Summe', $summary['count'].' Eintraege', '', '', $summary['points'], '', '', '', '', $summary['deliveries']];
|
||||
}
|
||||
|
||||
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
|
||||
return ['Summe', $summary['count'].' Eintraege', '', $summary['own_points'], $summary['external_points'], $summary['customer_abo_points'], $summary['customer_single_order_points'], $summary['customer_other_points'], $summary['total_points']];
|
||||
}
|
||||
|
||||
return ['Summe', $summary['count'].' Eintraege', '', '', '', ''];
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ class CheckoutController extends Controller
|
|||
'is_for' => $is_for,
|
||||
'is_abo' => $is_abo,
|
||||
'abo_interval' => $abo_interval,
|
||||
'customer_order_source_options' => ShoppingOrder::customerOrderSourceOptions(),
|
||||
'shopping_data' => $shopping_data,
|
||||
'user_shop' => Util::getUserShop(),
|
||||
'shopping_user' => $shopping_user,
|
||||
|
|
@ -225,6 +226,11 @@ class CheckoutController extends Controller
|
|||
'accepted_data_checkbox' => 'accepted',
|
||||
];
|
||||
|
||||
if (Request::get('is_from') === 'shopping') {
|
||||
$rules['customer_order_source'] = 'required|in:'.implode(',', array_keys(ShoppingOrder::customerOrderSourceOptions()));
|
||||
$rules['customer_order_source_comment'] = 'nullable|string|max:500';
|
||||
}
|
||||
|
||||
if (Request::get('same_as_billing')) {
|
||||
$rules = array_merge($rules, [
|
||||
'shipping_firstname' => 'required',
|
||||
|
|
|
|||
29
app/Models/BackofficeStatisticsSnapshot.php
Normal file
29
app/Models/BackofficeStatisticsSnapshot.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BackofficeStatisticsSnapshot extends Model
|
||||
{
|
||||
protected $table = 'backoffice_statistics_snapshots';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'year',
|
||||
'month',
|
||||
'payload',
|
||||
'calculated_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'int',
|
||||
'year' => 'int',
|
||||
'month' => 'int',
|
||||
'payload' => 'array',
|
||||
'calculated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -167,6 +167,8 @@ class ShoppingOrder extends Model
|
|||
'api_notice',
|
||||
'api_status',
|
||||
'mode',
|
||||
'customer_order_source',
|
||||
'customer_order_source_comment',
|
||||
'shipped',
|
||||
'tracking',
|
||||
];
|
||||
|
|
@ -180,6 +182,29 @@ class ShoppingOrder extends Model
|
|||
'points' => 'float',
|
||||
];
|
||||
|
||||
public const CUSTOMER_ORDER_SOURCE_OPTIONS = [
|
||||
'recommendation' => 'Empfehlung',
|
||||
'social_media' => 'Social Media',
|
||||
'search_engine' => 'Google / Suchmaschine',
|
||||
'event' => 'Event / Messe',
|
||||
'consultant_link' => 'Berater-Link',
|
||||
'returning_customer' => 'Wiederbesteller',
|
||||
'other' => 'Sonstiges',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function customerOrderSourceOptions(): array
|
||||
{
|
||||
return self::CUSTOMER_ORDER_SOURCE_OPTIONS;
|
||||
}
|
||||
|
||||
public function getCustomerOrderSourceLabel(): string
|
||||
{
|
||||
return self::CUSTOMER_ORDER_SOURCE_OPTIONS[$this->customer_order_source] ?? '';
|
||||
}
|
||||
|
||||
public static $shippedTypes = [
|
||||
0 => 'open',
|
||||
1 => 'in_process',
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class CheckoutRepository extends BaseRepository
|
|||
public function makeShoppingOrder($shopping_user, $data)
|
||||
{
|
||||
|
||||
$requestData = $data;
|
||||
$user_shop = Util::getUserShop();
|
||||
|
||||
if ($shopping_user->is_from === 'homeparty') {
|
||||
|
|
@ -103,6 +104,11 @@ class CheckoutRepository extends BaseRepository
|
|||
'txaction' => 'prev',
|
||||
'mode' => Util::getUserShoppingMode(),
|
||||
];
|
||||
|
||||
if ($shopping_user->is_from === 'shopping') {
|
||||
$data['customer_order_source'] = $requestData['customer_order_source'] ?? null;
|
||||
$data['customer_order_source_comment'] = $requestData['customer_order_source_comment'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$shopping_order = false;
|
||||
|
|
|
|||
365
app/Services/Backoffice/BackofficeDashboardService.php
Normal file
365
app/Services/Backoffice/BackofficeDashboardService.php
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Backoffice;
|
||||
|
||||
use App\Models\BackofficeStatisticsSnapshot;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BackofficeDashboardService
|
||||
{
|
||||
private const MAX_DEPTH_SAFETY_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function metricLabels(): array
|
||||
{
|
||||
return [
|
||||
'consultants' => 'Berater',
|
||||
'new_partners' => 'Neupartner',
|
||||
'team_partner_abos' => 'Teamabos',
|
||||
'team_customer_abos' => 'Teamkundenabos',
|
||||
'own_points' => 'Eigenpunkte',
|
||||
'external_points' => 'Externe Kundenpunkte',
|
||||
'customer_abo_points' => 'Kundenabo-Punkte',
|
||||
'customer_single_order_points' => 'Einzelbestellungs-Punkte',
|
||||
'customer_other_points' => 'Sonstige Kundenpunkte',
|
||||
'total_points' => 'Gesamtpunkte',
|
||||
'shop_1000' => '1000 Punkte Shop',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{month: int, year: int, metric_labels: array<string, string>, lines: array<int, array<string, mixed>>, totals: array<string, mixed>}
|
||||
*/
|
||||
public function overview(User $user, int $month, int $year): array
|
||||
{
|
||||
if ($this->isClosedMonth($month, $year)) {
|
||||
$snapshot = BackofficeStatisticsSnapshot::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->first();
|
||||
|
||||
if ($snapshot) {
|
||||
return $this->withMeta($snapshot->payload, 'snapshot', $snapshot->calculated_at?->format('d.m.Y H:i'));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->withMeta($this->buildOverview($user, $month, $year), 'live');
|
||||
}
|
||||
|
||||
public function storeSnapshot(User $user, int $month, int $year): BackofficeStatisticsSnapshot
|
||||
{
|
||||
$payload = $this->buildOverview($user, $month, $year);
|
||||
|
||||
return BackofficeStatisticsSnapshot::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
],
|
||||
[
|
||||
'payload' => $payload,
|
||||
'calculated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{month: int, year: int, metric_labels: array<string, string>, lines: array<int, array<string, mixed>>, totals: array<string, mixed>}
|
||||
*/
|
||||
private function buildOverview(User $user, int $month, int $year): array
|
||||
{
|
||||
$lineBuckets = $this->lineBuckets($user->id);
|
||||
$lines = [];
|
||||
$totals = $this->emptyLine(0, []);
|
||||
|
||||
foreach ($lineBuckets as $line => $users) {
|
||||
$row = $this->buildLineRow($line, $users, $month, $year);
|
||||
$lines[$line] = $row;
|
||||
$totals = $this->addToTotals($totals, $row);
|
||||
}
|
||||
|
||||
$totals['label'] = 'Summe';
|
||||
|
||||
return [
|
||||
'month' => $month,
|
||||
'year' => $year,
|
||||
'metric_labels' => $this->metricLabels(),
|
||||
'lines' => $lines,
|
||||
'totals' => $totals,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overview
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function withMeta(array $overview, string $source, ?string $calculatedAt = null): array
|
||||
{
|
||||
$overview['_meta'] = [
|
||||
'source' => $source,
|
||||
'source_label' => $source === 'snapshot' ? 'Snapshot' : 'Live',
|
||||
'calculated_at' => $calculatedAt,
|
||||
];
|
||||
|
||||
return $overview;
|
||||
}
|
||||
|
||||
private function isClosedMonth(int $month, int $year): bool
|
||||
{
|
||||
return Carbon::create($year, $month, 1)->endOfMonth()->lt(now()->startOfMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, \Illuminate\Support\Collection<int, \App\User>>
|
||||
*/
|
||||
public function lineBuckets(int $rootUserId): array
|
||||
{
|
||||
$lineBuckets = [];
|
||||
$currentSponsorIds = [$rootUserId];
|
||||
$visitedUserIds = [$rootUserId];
|
||||
|
||||
for ($line = 1; $line <= self::MAX_DEPTH_SAFETY_LIMIT && $currentSponsorIds !== []; $line++) {
|
||||
$users = User::query()
|
||||
->with('account')
|
||||
->whereIn('m_sponsor', $currentSponsorIds)
|
||||
->whereNotIn('id', $visitedUserIds)
|
||||
->whereColumn('id', '!=', 'm_sponsor')
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$lineBuckets[$line] = $users;
|
||||
$currentSponsorIds = $users->pluck('id')->all();
|
||||
$visitedUserIds = array_merge($visitedUserIds, $currentSponsorIds);
|
||||
}
|
||||
|
||||
return $lineBuckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function lineUserIds(int $rootUserId, int $line): array
|
||||
{
|
||||
if ($line === 0) {
|
||||
return $this->teamUserIds($rootUserId);
|
||||
}
|
||||
|
||||
if ($line < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ($this->lineBuckets($rootUserId)[$line] ?? collect())->pluck('id')->map(fn ($id) => (int) $id)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function teamUserIds(int $rootUserId): array
|
||||
{
|
||||
return collect($this->lineBuckets($rootUserId))
|
||||
->flatMap(fn (Collection $users) => $users->pluck('id'))
|
||||
->map(fn ($id) => (int) $id)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildLineRow(int $line, Collection $users, int $month, int $year): array
|
||||
{
|
||||
$userIds = $users->pluck('id')->map(fn ($id) => (int) $id)->all();
|
||||
|
||||
if ($userIds === []) {
|
||||
return $this->emptyLine($line, $userIds);
|
||||
}
|
||||
|
||||
$salesSummary = $this->salesSummary($userIds, $month, $year);
|
||||
|
||||
return [
|
||||
'line' => $line,
|
||||
'label' => 'Linie '.$line,
|
||||
'user_ids' => $userIds,
|
||||
'consultants' => $this->activeConsultants($users),
|
||||
'new_partners' => $this->newPartners($users, $month, $year),
|
||||
'team_partner_abos' => $this->activeAboQuery()->whereIn('user_id', $userIds)->where('is_for', 'me')->count(),
|
||||
'team_partner_abos_new' => $this->newAboCount($userIds, 'user_id', 'me', $month, $year),
|
||||
'team_customer_abos' => $this->activeAboQuery()->whereIn('member_id', $userIds)->where('is_for', 'ot')->count(),
|
||||
'team_customer_abos_new' => $this->newAboCount($userIds, 'member_id', 'ot', $month, $year),
|
||||
'own_points' => $salesSummary['own_points'],
|
||||
'external_points' => $salesSummary['external_points'],
|
||||
'customer_abo_points' => $salesSummary['customer_abo_points'],
|
||||
'customer_single_order_points' => $salesSummary['customer_single_order_points'],
|
||||
'customer_other_points' => $salesSummary['customer_other_points'],
|
||||
'total_points' => $salesSummary['total_points'],
|
||||
'turnover_net' => $salesSummary['turnover_net'],
|
||||
'shop_1000' => $this->shop1000Count($userIds, $month, $year),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function emptyLine(int $line, array $userIds): array
|
||||
{
|
||||
return [
|
||||
'line' => $line,
|
||||
'label' => $line > 0 ? 'Linie '.$line : 'Summe',
|
||||
'user_ids' => $userIds,
|
||||
'consultants' => 0,
|
||||
'new_partners' => 0,
|
||||
'team_partner_abos' => 0,
|
||||
'team_partner_abos_new' => 0,
|
||||
'team_customer_abos' => 0,
|
||||
'team_customer_abos_new' => 0,
|
||||
'own_points' => 0.0,
|
||||
'external_points' => 0.0,
|
||||
'customer_abo_points' => 0.0,
|
||||
'customer_single_order_points' => 0.0,
|
||||
'customer_other_points' => 0.0,
|
||||
'total_points' => 0.0,
|
||||
'turnover_net' => 0.0,
|
||||
'shop_1000' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $totals
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function addToTotals(array $totals, array $row): array
|
||||
{
|
||||
foreach (array_keys($this->metricLabels()) as $metric) {
|
||||
$totals[$metric] += $row[$metric];
|
||||
}
|
||||
|
||||
$totals['turnover_net'] += $row['turnover_net'];
|
||||
$totals['team_partner_abos_new'] += $row['team_partner_abos_new'];
|
||||
$totals['team_customer_abos_new'] += $row['team_customer_abos_new'];
|
||||
$totals['user_ids'] = array_merge($totals['user_ids'], $row['user_ids']);
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
*/
|
||||
private function activeConsultants(Collection $users): int
|
||||
{
|
||||
return $users
|
||||
->filter(fn (User $user) => $user->m_level !== null && $user->payment_account !== null)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Support\Collection<int, \App\User> $users
|
||||
*/
|
||||
private function newPartners(Collection $users, int $month, int $year): int
|
||||
{
|
||||
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
|
||||
return $users
|
||||
->filter(function (User $user) use ($startDate, $endDate): bool {
|
||||
if ($user->m_level === null || $user->active_date === null || ! $this->hasActivePaymentAccount($user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activeDate = Carbon::parse($user->active_date);
|
||||
|
||||
return $activeDate->betweenIncluded($startDate, $endDate);
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
private function hasActivePaymentAccount(User $user): bool
|
||||
{
|
||||
return $user->payment_account !== null && Carbon::parse($user->payment_account)->isFuture();
|
||||
}
|
||||
|
||||
private function activeAboQuery(): Builder
|
||||
{
|
||||
return UserAbo::query()
|
||||
->where('active', true)
|
||||
->whereNotIn('status', [4, 5, 6]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
private function newAboCount(array $userIds, string $userColumn, string $isFor, int $month, int $year): int
|
||||
{
|
||||
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
||||
$endDate = Carbon::create($year, $month, 1)->endOfMonth();
|
||||
|
||||
return $this->activeAboQuery()
|
||||
->whereIn($userColumn, $userIds)
|
||||
->where('is_for', $isFor)
|
||||
->whereBetween('start_date', [$startDate, $endDate])
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $userIds
|
||||
* @return array{own_points: float, external_points: float, customer_abo_points: float, customer_single_order_points: float, customer_other_points: float, total_points: float, turnover_net: float}
|
||||
*/
|
||||
private function salesSummary(array $userIds, int $month, int $year): array
|
||||
{
|
||||
$summary = UserSalesVolume::query()
|
||||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->selectRaw('COALESCE(SUM(month_KP_points), 0) as own_points')
|
||||
->selectRaw('COALESCE(SUM(month_shop_points), 0) as external_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.is_abo = 1 THEN month_shop_points ELSE 0 END), 0) as customer_abo_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL) THEN month_shop_points ELSE 0 END), 0) as customer_single_order_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NULL THEN month_shop_points ELSE 0 END), 0) as customer_other_points')
|
||||
->selectRaw('COALESCE(SUM(month_total_net), 0) + COALESCE(SUM(month_shop_total_net), 0) as turnover_net')
|
||||
->first();
|
||||
|
||||
$ownPoints = (float) ($summary->own_points ?? 0);
|
||||
$externalPoints = (float) ($summary->external_points ?? 0);
|
||||
|
||||
return [
|
||||
'own_points' => $ownPoints,
|
||||
'external_points' => $externalPoints,
|
||||
'customer_abo_points' => (float) ($summary->customer_abo_points ?? 0),
|
||||
'customer_single_order_points' => (float) ($summary->customer_single_order_points ?? 0),
|
||||
'customer_other_points' => (float) ($summary->customer_other_points ?? 0),
|
||||
'total_points' => $ownPoints + $externalPoints,
|
||||
'turnover_net' => (float) ($summary->turnover_net ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[] $userIds
|
||||
*/
|
||||
private function shop1000Count(array $userIds, int $month, int $year): int
|
||||
{
|
||||
return UserSalesVolume::query()
|
||||
->whereIn('user_id', $userIds)
|
||||
->where('month', $month)
|
||||
->where('year', $year)
|
||||
->select('user_id')
|
||||
->selectRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) as total_points')
|
||||
->groupBy('user_id')
|
||||
->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) >= 1000')
|
||||
->get()
|
||||
->count();
|
||||
}
|
||||
}
|
||||
331
app/Services/Backoffice/BackofficeDrilldownService.php
Normal file
331
app/Services/Backoffice/BackofficeDrilldownService.php
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
<?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, 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),
|
||||
'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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.account', 'user_abo_items.product', 'user_abo_orders.shopping_order.shopping_payments.payment_transactions'])
|
||||
->whereIn('member_id', $userIds)
|
||||
->where('is_for', 'ot')
|
||||
->orderBy('member_id')
|
||||
->orderBy('next_date')
|
||||
->get()
|
||||
->map(fn (UserAbo $abo) => [
|
||||
'type' => 'customer_abo',
|
||||
'abo_id' => $abo->id,
|
||||
'user_id' => $abo->user_id,
|
||||
'member_id' => $abo->member_id,
|
||||
'name' => $abo->user ? $this->userName($abo->user) : ($abo->email ?: '#'.$abo->user_id),
|
||||
'email' => $abo->email ?: $abo->user?->email,
|
||||
'consultant_name' => $abo->member ? $this->userName($abo->member) : '#'.$abo->member_id,
|
||||
'career_level' => $abo->member ? $this->careerLevel($abo->member) : '-',
|
||||
'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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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('COALESCE(SUM(month_KP_points), 0) as own_points')
|
||||
->selectRaw('COALESCE(SUM(month_shop_points), 0) as external_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.is_abo = 1 THEN month_shop_points ELSE 0 END), 0) as customer_abo_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL) THEN month_shop_points ELSE 0 END), 0) as customer_single_order_points')
|
||||
->selectRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NULL THEN month_shop_points ELSE 0 END), 0) as customer_other_points')
|
||||
->selectRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) as total_points')
|
||||
->groupBy('user_sales_volumes.user_id');
|
||||
|
||||
if ($metric === 'own_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(month_KP_points), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'external_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(month_shop_points), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'customer_abo_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(CASE WHEN shopping_orders.is_abo = 1 THEN month_shop_points ELSE 0 END), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'customer_single_order_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL) THEN month_shop_points ELSE 0 END), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'customer_other_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(CASE WHEN shopping_orders.id IS NULL THEN month_shop_points ELSE 0 END), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'total_points') {
|
||||
$rows->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) > 0');
|
||||
}
|
||||
|
||||
if ($metric === 'shop_1000') {
|
||||
$rows->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) >= 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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue