From 27b4ad3eb51eec1dce4a87c3fe6096830c387f9e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 24 Jun 2026 13:00:38 +0000 Subject: [PATCH] Backoffice-Statistik vereinfacht & Dashboard-Monatsstatistik konsolidiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Statistik-Uebersicht: Tabelle auf Linie/Berater/Umsatz(Punkte)/Teamabos/ Teamkundenabos reduziert; Neupartner als grünes "+N"-Badge in der Berater-Spalte; 1000-Punkte-Shop & Neupartner als klickbare KPI-Cards (Augen-Icon/Liste anzeigen) - Teamkundenabos-Drilldown pro Teampartner gruppiert (Anzahl + Volumenpunkte gesamt, aufklappbar je Abo), CSV-Export angepasst - Home: fehlerhafte Monatsstatistik-Card (inkonsistente aktiv-Definition, kein Monatsbezug, schwere Live-Logik im Blade) mit der Abo-KPI-Card zusammengefuehrt; Logik nach HomeController::buildMonthlyStats() ausgelagert, konsistent ueber TeamAboOverviewService/BackofficeDashboardService - Team-Payline fuer laufenden Monat live berechnet und 6h gecacht (Snapshot) - Kacheln in Cluster (Umsatz/Neupartner/Abos) gruppiert - Services: teamKpis() um *_new ergaenzt, personalAboKpis() & newPartnersCount() neu - i18n (de/en/es/fr), Tests ergaenzt/aktualisiert Co-Authored-By: Claude Opus 4.8 --- app/Http/Controllers/HomeController.php | 92 ++++- .../User/BackofficeStatisticsController.php | 24 +- app/Services/Abo/TeamAboOverviewService.php | 26 +- .../Backoffice/BackofficeDashboardService.php | 24 ++ .../Backoffice/BackofficeDrilldownService.php | 53 +-- resources/lang/de/abo.php | 1 + resources/lang/de/home.php | 6 + resources/lang/en/abo.php | 1 + resources/lang/en/home.php | 6 + resources/lang/es/abo.php | 1 + resources/lang/es/home.php | 6 + resources/lang/fr/abo.php | 1 + resources/lang/fr/home.php | 6 + .../views/dashboard/_abo_team_kpis.blade.php | 33 -- .../views/dashboard/_statistics.blade.php | 351 +++++++----------- resources/views/home.blade.php | 2 - .../backoffice/statistics/details.blade.php | 37 +- .../backoffice/statistics/index.blade.php | 109 ++++-- .../BackofficeStatisticsAccessTest.php | 10 + .../BackofficeStatisticsDrilldownTest.php | 165 ++++++++ 20 files changed, 631 insertions(+), 323 deletions(-) delete mode 100644 resources/views/dashboard/_abo_team_kpis.blade.php create mode 100644 tests/Feature/BackofficeStatisticsDrilldownTest.php diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 6b27f5de..b785db8b 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -7,10 +7,15 @@ use App\Models\Incentive; use App\Models\IncentiveParticipant; use App\Models\ShoppingPayment; use App\Models\UserAbo; +use App\Models\UserBusiness; +use App\Services\Abo\TeamAboOverviewService; use App\Services\AboHelper; +use App\Services\Backoffice\BackofficeDashboardService; +use App\Services\BusinessPlan\TreeCalcBot; use App\User; use Carbon\Carbon; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Request; use Util; @@ -79,10 +84,9 @@ class HomeController extends Controller } } - $teamAboKpis = null; + $monthlyStats = null; if ($user->isActiveAccount()) { - $teamAboKpis = app(\App\Services\Abo\TeamAboOverviewService::class) - ->teamKpis($user, (int) Carbon::now()->month, (int) Carbon::now()->year); + $monthlyStats = $this->buildMonthlyStats($user); } $data = [ @@ -92,12 +96,92 @@ class HomeController extends Controller 'activeIncentive' => $activeIncentive, 'incentiveParticipant' => $incentiveParticipant, 'oneTimeAbo' => $oneTimeAbo, - 'teamAboKpis' => $teamAboKpis, + 'monthlyStats' => $monthlyStats, ]; return view('home', $data); } + /** + * Kennzahlen der Monatsstatistik-Card. Bündelt persönliche Punkte/Abos, Team-Abo-KPIs + * (konsistent über {@see TeamAboOverviewService}) und die Team-Payline-Punkte. + * + * @return array + */ + private function buildMonthlyStats(User $user): array + { + $month = max(1, min(12, (int) Request::get('stats_month', now()->month))); + $year = (int) Request::get('stats_year', now()->year); + + $salesVolume = $user->getUserSalesVolume($month, $year, 'first'); + [$paylinePoints, $paylineIsLive] = $this->teamPaylinePoints($user, $month, $year); + + $firstlineIds = User::where('m_sponsor', $user->id) + ->whereColumn('id', '!=', 'm_sponsor') + ->whereNull('deleted_at') + ->pluck('id') + ->map(fn ($id) => (int) $id) + ->all(); + + $dashboardService = app(BackofficeDashboardService::class); + $teamOverview = app(TeamAboOverviewService::class); + + return array_merge( + [ + 'month' => $month, + 'year' => $year, + 'customer_points' => $salesVolume ? (float) $salesVolume->getPointsKPSum() : 0.0, + 'team_payline_points' => $paylinePoints, + 'payline_is_live' => $paylineIsLive, + 'direct_new_partners' => $dashboardService->newPartnersCount($firstlineIds, $month, $year), + ], + $teamOverview->teamKpis($user, $month, $year), + $teamOverview->personalAboKpis($user, $month, $year) + ); + } + + /** + * Team-Payline-Punkte: aus dem UserBusiness-Snapshot (abgeschlossene Monate) oder + * – für den laufenden Monat – einmalig live berechnet und für Folge-Aufrufe gecacht. + * + * @return array{0: float, 1: bool} [Punkte, istLive] + */ + private function teamPaylinePoints(User $user, int $month, int $year): array + { + $userBusiness = UserBusiness::where('user_id', $user->id) + ->where('month', $month) + ->where('year', $year) + ->first(); + + if ($userBusiness) { + return [(float) ($userBusiness->payline_points ?? 0), false]; + } + + $points = Cache::remember( + "home_payline_{$user->id}_{$month}_{$year}", + now()->addHours(6), + fn (): float => $this->calculatePaylineLive($user, $month, $year) + ); + + return [(float) $points, true]; + } + + private function calculatePaylineLive(User $user, int $month, int $year): float + { + try { + $treeCalcBot = new TreeCalcBot($month, $year, 'member'); + $treeCalcBot->initBusinesslUserDetail($user); + + if ($treeCalcBot->business_user) { + return (float) ($treeCalcBot->business_user->payline_points ?? 0); + } + } catch (\Throwable $e) { + \Log::warning("Live-Berechnung Payline-Punkte fehlgeschlagen für User {$user->id}: ".$e->getMessage()); + } + + return 0.0; + } + public function loadingModal() { diff --git a/app/Http/Controllers/User/BackofficeStatisticsController.php b/app/Http/Controllers/User/BackofficeStatisticsController.php index 95244416..76f4b1ed 100644 --- a/app/Http/Controllers/User/BackofficeStatisticsController.php +++ b/app/Http/Controllers/User/BackofficeStatisticsController.php @@ -201,7 +201,11 @@ class BackofficeStatisticsController extends Controller */ private function csvHeaders(string $metric): array { - if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) { + if ($metric === 'team_customer_abos') { + return ['Teampartner', 'E-Mail', 'Karriere-Level', 'Anzahl Kundenabos', 'Volumenpunkte gesamt']; + } + + if ($metric === 'team_partner_abos') { return ['Name', 'E-Mail', 'Karriere-Level', 'Berater', 'Abo-Punkte', 'Status', 'Status-Grund', 'Besteht seit', 'Naechste Ausfuehrung', 'Lieferungen']; } @@ -218,7 +222,17 @@ class BackofficeStatisticsController extends Controller */ private function csvRow(string $metric, array $row): array { - if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) { + if ($metric === 'team_customer_abos') { + return [ + $row['name'] ?? '', + $row['email'] ?? '', + $row['career_level'] ?? '', + $row['abo_count'] ?? 0, + $row['points'] ?? 0, + ]; + } + + if ($metric === 'team_partner_abos') { return [ $row['name'] ?? '', $row['email'] ?? '', @@ -266,7 +280,11 @@ class BackofficeStatisticsController extends Controller $metric = $details['metric']; $summary = $details['summary']; - if (in_array($metric, ['team_partner_abos', 'team_customer_abos'], true)) { + if ($metric === 'team_customer_abos') { + return ['Summe', $summary['count'].' Teampartner', '', $summary['abo_count'], $summary['points']]; + } + + if ($metric === 'team_partner_abos') { return ['Summe', $summary['count'].' Eintraege', '', '', $summary['points'], '', '', '', '', $summary['deliveries']]; } diff --git a/app/Services/Abo/TeamAboOverviewService.php b/app/Services/Abo/TeamAboOverviewService.php index 777ad386..8076b008 100644 --- a/app/Services/Abo/TeamAboOverviewService.php +++ b/app/Services/Abo/TeamAboOverviewService.php @@ -53,7 +53,7 @@ class TeamAboOverviewService /** * Leichtgewichtige Kennzahlen für das Dashboard (ohne Punkte-/Mitglieder-Details). * - * @return array{team_total_abos: int, team_partner_abos: int, team_customer_abos: int, shop_1000: int} + * @return array{team_total_abos: int, team_partner_abos: int, team_partner_abos_new: int, team_customer_abos: int, team_customer_abos_new: int, shop_1000: int, new_partners: int} */ public function teamKpis(User $user, int $month, int $year): array { @@ -63,8 +63,11 @@ class TeamAboOverviewService return [ 'team_total_abos' => 0, 'team_partner_abos' => 0, + 'team_partner_abos_new' => 0, 'team_customer_abos' => 0, + 'team_customer_abos_new' => 0, 'shop_1000' => 0, + 'new_partners' => 0, ]; } @@ -74,8 +77,29 @@ class TeamAboOverviewService return [ 'team_total_abos' => $partnerAbos + $customerAbos, 'team_partner_abos' => $partnerAbos, + 'team_partner_abos_new' => $this->newAboCount($teamUserIds, 'user_id', 'me', $month, $year), 'team_customer_abos' => $customerAbos, + 'team_customer_abos_new' => $this->newAboCount($teamUserIds, 'member_id', 'ot', $month, $year), 'shop_1000' => $this->dashboardService->shop1000Count($teamUserIds, $month, $year), + 'new_partners' => $this->dashboardService->newPartnersCount($teamUserIds, $month, $year), + ]; + } + + /** + * Persönliche Abo-Kennzahlen des Beraters selbst (eigenes Abo + eigene Kundenabos), + * konsistent mit der activeAbo-Definition. Inklusive der im Monat neu hinzugekommenen. + * + * @return array{own_abos: int, own_abos_new: int, own_customer_abos: int, own_customer_abos_new: int} + */ + public function personalAboKpis(User $user, int $month, int $year): array + { + $userId = (int) $user->id; + + return [ + 'own_abos' => UserAbo::query()->activeAbo()->where('is_for', 'me')->where('user_id', $userId)->count(), + 'own_abos_new' => $this->newAboCount([$userId], 'user_id', 'me', $month, $year), + 'own_customer_abos' => UserAbo::query()->activeAbo()->where('is_for', 'ot')->where('member_id', $userId)->count(), + 'own_customer_abos_new' => $this->newAboCount([$userId], 'member_id', 'ot', $month, $year), ]; } diff --git a/app/Services/Backoffice/BackofficeDashboardService.php b/app/Services/Backoffice/BackofficeDashboardService.php index ebc45f2c..96a9312c 100644 --- a/app/Services/Backoffice/BackofficeDashboardService.php +++ b/app/Services/Backoffice/BackofficeDashboardService.php @@ -346,6 +346,30 @@ class BackofficeDashboardService ]; } + /** + * Anzahl der Neupartner (aktiver Account, im Monat aktiv geworden). + * Konsistent mit der Drilldown-Liste {@see BackofficeDrilldownService::newPartnerRows()}. + * + * @param int[] $userIds + */ + public function newPartnersCount(array $userIds, int $month, int $year): int + { + if ($userIds === []) { + return 0; + } + + $startDate = Carbon::create($year, $month, 1)->startOfMonth(); + $endDate = Carbon::create($year, $month, 1)->endOfMonth(); + + return User::query() + ->whereIn('id', $userIds) + ->whereNotNull('m_level') + ->whereNotNull('payment_account') + ->where('payment_account', '>', now()) + ->whereBetween('active_date', [$startDate, $endDate]) + ->count(); + } + /** * Anzahl der Berater mit >= 1000 Punkten Kundenumsatz (KP + Shop) im Monat. * diff --git a/app/Services/Backoffice/BackofficeDrilldownService.php b/app/Services/Backoffice/BackofficeDrilldownService.php index 8f8da4f1..bc094319 100644 --- a/app/Services/Backoffice/BackofficeDrilldownService.php +++ b/app/Services/Backoffice/BackofficeDrilldownService.php @@ -47,12 +47,13 @@ class BackofficeDrilldownService /** * @param array> $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} + * @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'), @@ -157,37 +158,47 @@ class BackofficeDrilldownService } /** + * 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> */ 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']) + ->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() - ->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), - ]) + ->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(); } diff --git a/resources/lang/de/abo.php b/resources/lang/de/abo.php index 334b5206..b8b0c921 100644 --- a/resources/lang/de/abo.php +++ b/resources/lang/de/abo.php @@ -209,6 +209,7 @@ return [ 'yes' => 'Ja', 'no' => 'Nein', 'shop_1000' => '1000-Punkte-Shops', + 'new_partners' => 'Neupartner', 'next_execution' => 'Nächste Ausführung', 'executions_count' => 'Ausführungen', 'abo_count_points' => 'Punkte', diff --git a/resources/lang/de/home.php b/resources/lang/de/home.php index 23245193..a8ce988b 100644 --- a/resources/lang/de/home.php +++ b/resources/lang/de/home.php @@ -55,6 +55,12 @@ return [ 'team_new_partners' => 'Neupartner im Team', 'customer_subscriptions' => 'Kundenabos', 'team_subscriptions' => 'Teamabos', + 'team_customer_subscriptions' => 'Team-Kundenabos', + 'own_subscriptions' => 'Eigene Abos', + 'own_customer_subscriptions' => 'Eigene Kundenabos', + 'stats_group_turnover' => 'Umsatz', + 'stats_group_new_partners' => 'Neupartner', + 'stats_group_subscriptions' => 'Abos', 'own' => 'Eigene', 'live_calculation_hint' => 'Live-Berechnung (noch nicht abgeschlossen)', 'live_calculation_hint_text' => 'Wird erst zum Monatsende berechnet.', diff --git a/resources/lang/en/abo.php b/resources/lang/en/abo.php index fabd7f2e..720fe5cc 100644 --- a/resources/lang/en/abo.php +++ b/resources/lang/en/abo.php @@ -210,6 +210,7 @@ return [ 'yes' => 'Yes', 'no' => 'No', 'shop_1000' => '1000-Point Shops', + 'new_partners' => 'New partners', 'next_execution' => 'Next execution', 'executions_count' => 'Executions', 'abo_count_points' => 'Points', diff --git a/resources/lang/en/home.php b/resources/lang/en/home.php index 30f68c4d..032d64dd 100644 --- a/resources/lang/en/home.php +++ b/resources/lang/en/home.php @@ -54,6 +54,12 @@ return [ 'team_new_partners' => 'New Partners in Team', 'customer_subscriptions' => 'Customer Subscriptions', 'team_subscriptions' => 'Team Subscriptions', + 'team_customer_subscriptions' => 'Team Customer Subscriptions', + 'own_subscriptions' => 'Own Subscriptions', + 'own_customer_subscriptions' => 'Own Customer Subscriptions', + 'stats_group_turnover' => 'Turnover', + 'stats_group_new_partners' => 'New Partners', + 'stats_group_subscriptions' => 'Subscriptions', 'own' => 'Own', 'live_calculation_hint' => 'Live calculation (not yet finalized)', 'live_calculation_hint_text' => 'Will be calculated at the end of the month.', diff --git a/resources/lang/es/abo.php b/resources/lang/es/abo.php index 8558d4b9..7343cbaa 100644 --- a/resources/lang/es/abo.php +++ b/resources/lang/es/abo.php @@ -183,6 +183,7 @@ return [ 'yes' => 'Sí', 'no' => 'No', 'shop_1000' => 'Tiendas de 1000 puntos', + 'new_partners' => 'Nuevos socios', 'next_execution' => 'Próxima ejecución', 'executions_count' => 'Ejecuciones', 'abo_count_points' => 'Puntos', diff --git a/resources/lang/es/home.php b/resources/lang/es/home.php index c3c03560..af679065 100644 --- a/resources/lang/es/home.php +++ b/resources/lang/es/home.php @@ -54,6 +54,12 @@ return [ 'team_new_partners' => 'Nuevos socios en el equipo', 'customer_subscriptions' => 'Suscripciones de clientes', 'team_subscriptions' => 'Suscripciones del equipo', + 'team_customer_subscriptions' => 'Suscripciones de clientes del equipo', + 'own_subscriptions' => 'Suscripciones propias', + 'own_customer_subscriptions' => 'Suscripciones de clientes propias', + 'stats_group_turnover' => 'Volumen', + 'stats_group_new_partners' => 'Nuevos socios', + 'stats_group_subscriptions' => 'Suscripciones', 'own' => 'Propio', 'live_calculation_hint' => 'Cálculo en vivo (no finalizado)', 'live_calculation_hint_text' => 'Se calculará al final del mes.', diff --git a/resources/lang/fr/abo.php b/resources/lang/fr/abo.php index b29b13fb..5b2559ff 100644 --- a/resources/lang/fr/abo.php +++ b/resources/lang/fr/abo.php @@ -182,6 +182,7 @@ return [ 'yes' => 'Oui', 'no' => 'Non', 'shop_1000' => 'Boutiques 1000 points', + 'new_partners' => 'Nouveaux partenaires', 'next_execution' => 'Prochaine exécution', 'executions_count' => 'Exécutions', 'abo_count_points' => 'Points', diff --git a/resources/lang/fr/home.php b/resources/lang/fr/home.php index 7d5f9bd6..583d321d 100644 --- a/resources/lang/fr/home.php +++ b/resources/lang/fr/home.php @@ -55,6 +55,12 @@ return [ 'team_new_partners' => 'Nouveaux partenaires dans l’équipe', 'customer_subscriptions' => 'Abonnements clients', 'team_subscriptions' => 'Abonnements équipe', + 'team_customer_subscriptions' => 'Abonnements clients équipe', + 'own_subscriptions' => 'Abonnements propres', + 'own_customer_subscriptions' => 'Abonnements clients propres', + 'stats_group_turnover' => 'Chiffre d\'affaires', + 'stats_group_new_partners' => 'Nouveaux partenaires', + 'stats_group_subscriptions' => 'Abonnements', 'own' => 'Propres', 'live_calculation_hint' => 'Calcul en direct (pas encore terminé)', 'live_calculation_hint_text' => 'Ne sera calculé qu’à la fin du mois.', diff --git a/resources/views/dashboard/_abo_team_kpis.blade.php b/resources/views/dashboard/_abo_team_kpis.blade.php deleted file mode 100644 index 78e71031..00000000 --- a/resources/views/dashboard/_abo_team_kpis.blade.php +++ /dev/null @@ -1,33 +0,0 @@ -@if (!empty($teamAboKpis) && ($teamAboKpis['team_total_abos'] > 0 || $teamAboKpis['shop_1000'] > 0)) - -@endif diff --git a/resources/views/dashboard/_statistics.blade.php b/resources/views/dashboard/_statistics.blade.php index 58ab6dca..1761c759 100644 --- a/resources/views/dashboard/_statistics.blade.php +++ b/resources/views/dashboard/_statistics.blade.php @@ -1,261 +1,168 @@ -@php - $selectedMonth = request()->get('stats_month', date('n')); - $selectedYear = request()->get('stats_year', date('Y')); +@if (! empty($monthlyStats)) + @php + $s = $monthlyStats; + $isVip = auth()->user()?->isVIP() ?? false; + $m = $s['month']; + $y = $s['year']; - // Start- und Enddatum für den Monat - $startDate = \Carbon\Carbon::createFromDate($selectedYear, $selectedMonth, 1)->startOfMonth(); - $endDate = \Carbon\Carbon::createFromDate($selectedYear, $selectedMonth, 1)->endOfMonth(); + $teamShow = route('user_abos_team_show'); + $shop1000Link = $isVip + ? route('user_backoffice_statistics_details', ['line' => 0, 'metric' => 'shop_1000', 'month' => $m, 'year' => $y]) + : $teamShow; + $newPartnersLink = $isVip + ? route('user_backoffice_statistics_details', ['line' => 0, 'metric' => 'new_partners', 'month' => $m, 'year' => $y]) + : null; - // UserBusiness für den Monat laden (enthält Payline-Punkte) - $userBusiness = \App\Models\UserBusiness::where('user_id', $user->id) - ->where('month', $selectedMonth) - ->where('year', $selectedYear) - ->first(); + $fmt = fn ($value) => number_format((float) $value, 0, ',', '.'); - // UserSalesVolume für KP-Punkte - $userSalesVolume = $user->getUserSalesVolume($selectedMonth, $selectedYear, 'first'); + $clusters = [ + [ + 'title' => __('home.stats_group_turnover'), + 'col' => 'col-md-6 col-lg-4', + 'tiles' => [ + [ + 'label' => __('home.customer_turnover_points'), 'value' => $fmt($s['customer_points']), + 'icon' => 'ion-md-cart', 'color' => 'primary', 'link' => null, 'new' => null, 'live' => false, + ], + [ + 'label' => __('home.team_turnover_points') . ' (Payline)', 'value' => $fmt($s['team_payline_points']), + 'icon' => 'ion-md-people', 'color' => 'success', 'link' => null, 'new' => null, 'live' => $s['payline_is_live'], + ], + [ + 'label' => __('abo.shop_1000'), 'value' => $fmt($s['shop_1000']), + 'icon' => 'ion-md-trophy', 'color' => 'success', 'link' => $shop1000Link, 'new' => null, 'live' => false, + ], + ], + ], + [ + 'title' => __('home.stats_group_new_partners'), + 'col' => 'col-md-6', + 'tiles' => [ + [ + 'label' => __('home.direct_new_partners'), 'value' => $fmt($s['direct_new_partners']), + 'icon' => 'ion-md-person-add', 'color' => 'info', 'link' => null, 'new' => null, 'live' => false, + ], + [ + 'label' => __('home.team_new_partners'), 'value' => $fmt($s['new_partners']), + 'icon' => 'ion-md-contacts', 'color' => 'warning', 'link' => $newPartnersLink, 'new' => null, 'live' => false, + ], + ], + ], + [ + 'title' => __('home.stats_group_subscriptions'), + 'col' => 'col-sm-6 col-lg-3', + 'tiles' => [ + [ + 'label' => __('home.own_subscriptions'), 'value' => $fmt($s['own_abos']), + 'icon' => 'ion-md-person', 'color' => 'secondary', 'link' => route('user_abos', 'me'), 'new' => $s['own_abos_new'], 'live' => false, + ], + [ + 'label' => __('home.own_customer_subscriptions'), 'value' => $fmt($s['own_customer_abos']), + 'icon' => 'ion-md-repeat', 'color' => 'secondary', 'link' => route('user_abos', 'ot'), 'new' => $s['own_customer_abos_new'], 'live' => false, + ], + [ + 'label' => __('home.team_subscriptions'), 'value' => $fmt($s['team_partner_abos']), + 'icon' => 'ion-md-sync', 'color' => 'dark', 'link' => $teamShow, 'new' => $s['team_partner_abos_new'], 'live' => false, + ], + [ + 'label' => __('home.team_customer_subscriptions'), 'value' => $fmt($s['team_customer_abos']), + 'icon' => 'ion-md-git-network', 'color' => 'dark', 'link' => $teamShow, 'new' => $s['team_customer_abos_new'], 'live' => false, + ], + ], + ], + ]; + @endphp - // Kunden-Umsatz Punkte (KP - Eigene Punkte + Shop) - $customerPoints = $userSalesVolume ? $userSalesVolume->getPointsKPSum() : 0; - - // Team-Umsatz Punkte (Payline) - Live-Berechnung wenn UserBusiness nicht verfügbar - $teamPaylinePoints = 0; - $isLiveCalculation = false; - - if ($userBusiness) { - // Gespeicherte Daten aus UserBusiness verwenden - $teamPaylinePoints = $userBusiness->payline_points ?? 0; - } else { - // Live-Berechnung über TreeCalcBot (für aktuellen Monat) - $isLiveCalculation = true; - try { - $treeCalcBot = new \App\Services\BusinessPlan\TreeCalcBot( - (int) $selectedMonth, - (int) $selectedYear, - 'member', - ); - $treeCalcBot->initBusinesslUserDetail($user); - - if ($treeCalcBot->business_user) { - $teamPaylinePoints = $treeCalcBot->business_user->payline_points ?? 0; - } - } catch (\Exception $e) { - \Log::warning("Live-Berechnung Payline-Punkte fehlgeschlagen für User {$user->id}: " . $e->getMessage()); - $teamPaylinePoints = 0; - } - } - - // Direkte Neupartner (Firstlines) in diesem Monat - $directNewPartners = \App\User::where('m_sponsor', $user->id) - ->whereColumn('id', '!=', 'm_sponsor') - ->where('active_date', '>=', $startDate) - ->where('active_date', '<=', $endDate) - ->where('m_level', '!=', null) - ->where('payment_account', '!=', null) - ->count(); - - // ===== REKURSIVE TEAM-STRUKTUR ===== - // Hilfsfunktion um alle Team-Mitglieder rekursiv zu sammeln (inkl. Sponsor-Kette) - $getAllTeamMemberIds = function ($sponsorId, $maxDepth = 20) use (&$getAllTeamMemberIds) { - static $cache = []; - - if (isset($cache[$sponsorId])) { - return $cache[$sponsorId]; - } - - $teamIds = []; - $currentLevel = [$sponsorId]; - $depth = 0; - - while (!empty($currentLevel) && $depth < $maxDepth) { - $children = \App\User::whereIn('m_sponsor', $currentLevel) - ->whereColumn('id', '!=', 'm_sponsor') - ->where('m_level', '!=', null) - ->where('payment_account', '!=', null) - ->whereNull('deleted_at') - ->pluck('id') - ->toArray(); - - $teamIds = array_merge($teamIds, $children); - $currentLevel = $children; - $depth++; - } - - $cache[$sponsorId] = $teamIds; - return $teamIds; - }; - - // Alle Team-Mitglieder rekursiv laden - $allTeamMemberIds = $getAllTeamMemberIds($user->id); - - // Neupartner im gesamten Team (rekursiv im Marketingplan) - $teamNewPartners = 0; - if (!empty($allTeamMemberIds)) { - $teamNewPartners = \App\User::whereIn('id', $allTeamMemberIds) - ->where('active_date', '>=', $startDate) - ->where('active_date', '<=', $endDate) - ->count(); - } - - // Kundenabos (is_for = 'customer' oder shop-bezogen) - $customerAbos = \App\Models\UserAbo::where('member_id', $user->id) - ->where('is_for', 'ot') - ->whereIn('status', [1, 2]) // aktive Abos - ->where('active', true) - ->count(); - - // Eigene Abos - $ownAbos = \App\Models\UserAbo::where('user_id', $user->id) - ->where('is_for', 'me') - ->whereIn('status', [1, 2]) - ->where('active', true) - ->count(); - - // Team-Abos (rekursiv über gesamtes Team im Marketingplan) - $teamAbos = 0; - if (!empty($allTeamMemberIds)) { - $teamAbos = \App\Models\UserAbo::whereIn('user_id', $allTeamMemberIds) - ->where('is_for', 'me') - ->whereIn('status', [1, 2]) - ->where('active', true) - ->count(); - } -@endphp - -@if ($user->isActiveAccount())
{{ __('home.monthly_statistics') }} - - {{ HTMLHelper::getMonth($selectedMonth) }} {{ $selectedYear }} + {{ HTMLHelper::getMonth($m) }} {{ $y }}
-
- {{-- Kunden-Umsatz Punkte --}} -
-
-
- -
-
-
{{ __('home.customer_turnover_points') }}
-
- {{ number_format($customerPoints, 0, ',', '.') }}
-
-
-
- - {{-- Team-Umsatz Punkte (Payline) --}} -
-
-
- -
-
-
- {{ __('home.team_turnover_points') }} (Payline) - @if ($isLiveCalculation) - Live - @endif + @foreach ($clusters as $cluster) +
+ {{ $cluster['title'] }} +
+ - - - {{-- Direkte Neupartner --}} -
-
-
- -
-
-
{{ __('home.direct_new_partners') }} -
-
{{ $directNewPartners }}
-
-
-
- - {{-- Neupartner im Team --}} -
-
-
- -
-
-
{{ __('home.team_new_partners') }}
-
{{ $teamNewPartners }}
-
-
-
- - {{-- Kundenabos --}} -
-
-
- -
-
-
{{ __('home.customer_subscriptions') }} -
-
{{ $customerAbos }} + - {{ $ownAbos }} - {{ __('home.own') }}
-
-
-
- - {{-- Team-Abos --}} -
-
-
- -
-
-
{{ __('home.team_subscriptions') }}
-
{{ $teamAbos }}
-
-
-
-
+ @endforeach
+ +