From 54bf2e5595b1d9cb0dcac8a046d11e6c91111770 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 25 Jun 2026 13:31:14 +0000 Subject: [PATCH] Backoffice-Statistik: Punkte-Fix (kumulative Snapshots) & Meine Statistik 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 --- .../User/BackofficeStatisticsController.php | 29 ++- .../Backoffice/BackofficeDrilldownService.php | 26 +-- .../Backoffice/PersonalStatisticsService.php | 121 +++++++++++ app/Services/Backoffice/SalesVolumePoints.php | 71 ++++++ resources/lang/de/navigation.php | 2 + resources/lang/en/navigation.php | 2 + .../layouts/includes/layout-sidenav.blade.php | 20 +- .../backoffice/statistics/details.blade.php | 22 +- .../backoffice/statistics/index.blade.php | 25 ++- .../user/backoffice/statistics/my.blade.php | 204 ++++++++++++++++++ routes/domains/crm.php | 1 + .../BackofficeStatisticsAccessTest.php | 40 +++- .../BackofficeDashboardServiceTest.php | 51 +++-- .../PersonalStatisticsServiceTest.php | 122 +++++++++++ 14 files changed, 666 insertions(+), 70 deletions(-) create mode 100644 app/Services/Backoffice/PersonalStatisticsService.php create mode 100644 app/Services/Backoffice/SalesVolumePoints.php create mode 100644 resources/views/user/backoffice/statistics/my.blade.php create mode 100644 tests/Unit/Services/PersonalStatisticsServiceTest.php diff --git a/app/Http/Controllers/User/BackofficeStatisticsController.php b/app/Http/Controllers/User/BackofficeStatisticsController.php index 76f4b1ed..256fa345 100644 --- a/app/Http/Controllers/User/BackofficeStatisticsController.php +++ b/app/Http/Controllers/User/BackofficeStatisticsController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\User; use App\Http\Controllers\Controller; use App\Services\Backoffice\BackofficeDashboardService; use App\Services\Backoffice\BackofficeDrilldownService; +use App\Services\Backoffice\PersonalStatisticsService; use App\Services\HTMLHelper; use Illuminate\Http\Request; use Illuminate\View\View; @@ -18,7 +19,8 @@ class BackofficeStatisticsController extends Controller public function __construct( private BackofficeDashboardService $dashboardService, - private BackofficeDrilldownService $drilldownService + private BackofficeDrilldownService $drilldownService, + private PersonalStatisticsService $personalService ) { $this->middleware('active.account'); } @@ -48,6 +50,23 @@ class BackofficeStatisticsController extends Controller ]); } + public function my(Request $request): View + { + if (! $request->user()?->isVIP()) { + abort(404); + } + + [$selectedMonth, $selectedYear] = $this->selectedPeriod($request); + + return view('user.backoffice.statistics.my', [ + 'selectedMonth' => $selectedMonth, + 'selectedYear' => $selectedYear, + 'filterMonths' => HTMLHelper::getTransMonths(), + 'filterYears' => HTMLHelper::getYearRange(2022), + 'statistics' => $this->personalService->monthly($request->user(), $selectedMonth, $selectedYear), + ]); + } + public function details(Request $request): View { if (! $request->user()?->isVIP()) { @@ -210,7 +229,7 @@ class BackofficeStatisticsController extends Controller } 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', 'Eigenpunkte', 'Shop-Punkte', 'Abo-Punkte', 'Sonstige Punkte', 'Kundenpunkte', 'Gesamtpunkte']; } return ['Name', 'E-Mail', 'Karriere-Level', 'Aktiv seit', 'Account gueltig bis', 'Account Status']; @@ -253,10 +272,10 @@ class BackofficeStatisticsController extends Controller $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_abo_points'] ?? 0, $row['customer_other_points'] ?? 0, + $row['external_points'] ?? 0, $row['total_points'] ?? 0, ]; } @@ -289,7 +308,7 @@ class BackofficeStatisticsController extends Controller } 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', '', $summary['own_points'], $summary['customer_single_order_points'], $summary['customer_abo_points'], $summary['customer_other_points'], $summary['external_points'], $summary['total_points']]; } return ['Summe', $summary['count'].' Eintraege', '', '', '', '']; diff --git a/app/Services/Backoffice/BackofficeDrilldownService.php b/app/Services/Backoffice/BackofficeDrilldownService.php index bc094319..c4ac5a7b 100644 --- a/app/Services/Backoffice/BackofficeDrilldownService.php +++ b/app/Services/Backoffice/BackofficeDrilldownService.php @@ -215,40 +215,40 @@ class BackofficeDrilldownService ->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') + ->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('COALESCE(SUM(month_KP_points), 0) > 0'); + $rows->havingRaw(SalesVolumePoints::ownPoints().' > 0'); } if ($metric === 'external_points') { - $rows->havingRaw('COALESCE(SUM(month_shop_points), 0) > 0'); + $rows->havingRaw(SalesVolumePoints::externalPoints().' > 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'); + $rows->havingRaw(SalesVolumePoints::customerAboPoints().' > 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'); + $rows->havingRaw(SalesVolumePoints::customerSingleOrderPoints().' > 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'); + $rows->havingRaw(SalesVolumePoints::customerOtherPoints().' > 0'); } if ($metric === 'total_points') { - $rows->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) > 0'); + $rows->havingRaw(SalesVolumePoints::totalPoints().' > 0'); } if ($metric === 'shop_1000') { - $rows->havingRaw('COALESCE(SUM(month_KP_points), 0) + COALESCE(SUM(month_shop_points), 0) >= 1000'); + $rows->havingRaw(SalesVolumePoints::totalPoints().' >= 1000'); } $salesRows = $rows->orderByDesc('total_points')->get(); diff --git a/app/Services/Backoffice/PersonalStatisticsService.php b/app/Services/Backoffice/PersonalStatisticsService.php new file mode 100644 index 00000000..a597075f --- /dev/null +++ b/app/Services/Backoffice/PersonalStatisticsService.php @@ -0,0 +1,121 @@ +>}> + * } + */ + public function monthly(User $user, int $month, int $year): array + { + $volumes = UserSalesVolume::query() + ->with(['shopping_order.shopping_user']) + ->where('user_id', $user->id) + ->where('month', $month) + ->where('year', $year) + ->orderBy('id') + ->get(); + + $groups = [ + 'own' => $this->emptyGroup('own', 'Eigene Bestellungen'), + 'shop' => $this->emptyGroup('shop', 'Shop-Bestellungen'), + 'abo' => $this->emptyGroup('abo', 'Abos'), + 'other' => $this->emptyGroup('other', 'Sonstige'), + ]; + + foreach ($volumes as $volume) { + $key = $this->groupKey($volume); + $groups[$key]['rows'][] = $this->mapRow($volume); + $groups[$key]['points'] += (float) $volume->points; + $groups[$key]['net'] += (float) $volume->total_net; + } + + $totals = [ + 'own_points' => $groups['own']['points'], + 'shop_points' => $groups['shop']['points'], + 'abo_points' => $groups['abo']['points'], + 'other_points' => $groups['other']['points'], + 'total_points' => $groups['own']['points'] + $groups['shop']['points'] + $groups['abo']['points'] + $groups['other']['points'], + 'turnover_net' => $groups['own']['net'] + $groups['shop']['net'] + $groups['abo']['net'] + $groups['other']['net'], + ]; + + return [ + 'month' => $month, + 'year' => $year, + 'totals' => $totals, + 'groups' => $groups, + ]; + } + + private function groupKey(UserSalesVolume $volume): string + { + $status = (int) $volume->status; + $order = $volume->shopping_order; + + if ($status === 1) { + return 'own'; + } + + if ($status === 2 && $order) { + return $order->is_abo ? 'abo' : 'shop'; + } + + return 'other'; + } + + /** + * @return array{key: string, label: string, points: float, net: float, rows: array>} + */ + private function emptyGroup(string $key, string $label): array + { + return [ + 'key' => $key, + 'label' => $label, + 'points' => 0.0, + 'net' => 0.0, + 'rows' => [], + ]; + } + + /** + * @return array + */ + private function mapRow(UserSalesVolume $volume): array + { + $order = $volume->shopping_order; + $customer = $order?->shopping_user; + + return [ + 'date' => $volume->date, + 'points' => (float) $volume->points, + 'total_net' => (float) $volume->total_net, + 'status' => (int) $volume->status, + 'status_label' => $volume->getStatusType(), + 'status_color' => $volume->getStatusColor(), + 'order_id' => $order?->id, + 'order_type' => $order ? ((int) $volume->status === 1 ? 'own' : 'shop') : null, + 'customer_name' => $customer ? trim(($customer->billing_firstname ?? '').' '.($customer->billing_lastname ?? '')) : null, + 'customer_email' => $customer?->billing_email, + 'info' => $volume->info, + 'message' => $volume->message, + ]; + } +} diff --git a/app/Services/Backoffice/SalesVolumePoints.php b/app/Services/Backoffice/SalesVolumePoints.php new file mode 100644 index 00000000..99cfc32d --- /dev/null +++ b/app/Services/Backoffice/SalesVolumePoints.php @@ -0,0 +1,71 @@ + 'Versandkosten', 'start_site' => 'Startseite Shop', 'statistics' => 'Statistik', + 'team_statistics' => 'Team-Statistik', + 'my_statistics' => 'Meine Statistik', 'structure' => 'Struktur', 'system_settings' => 'System-E.', 'translate' => 'Übersetzungen', diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index f1c68378..33c619d3 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -57,6 +57,8 @@ return [ 'shipping_costs' => 'shipping', 'start_site' => 'home page', 'statistics' => 'Statistics', + 'team_statistics' => 'Team Statistics', + 'my_statistics' => 'My Statistics', 'structure' => 'structure', 'system_settings' => 'system E.', 'tags' => 'Tags', diff --git a/resources/views/layouts/includes/layout-sidenav.blade.php b/resources/views/layouts/includes/layout-sidenav.blade.php index 710ba372..23db1844 100755 --- a/resources/views/layouts/includes/layout-sidenav.blade.php +++ b/resources/views/layouts/includes/layout-sidenav.blade.php @@ -56,14 +56,28 @@ @if (Auth::user()->isVIP()) -
  • - +
  • + +
    {{ __('navigation.statistics') }}
    VIP
    +
  • @endif
  • diff --git a/resources/views/user/backoffice/statistics/details.blade.php b/resources/views/user/backoffice/statistics/details.blade.php index 61c39166..467955e8 100644 --- a/resources/views/user/backoffice/statistics/details.blade.php +++ b/resources/views/user/backoffice/statistics/details.blade.php @@ -89,11 +89,11 @@ Nächste Ausführung Lieferungen @elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) - Eigenpunkte - Externe Punkte - Kundenabo-Punkte - Einzelbestellungs-Punkte - Sonstige Kundenpunkte + Eigenpunkte + Shop-Punkte + Abo-Punkte + Sonstige Punkte + Kundenpunkte Gesamtpunkte @else Aktiv seit @@ -151,11 +151,11 @@ {{ $row['next_date'] ?? '-' }} {{ number_format($row['deliveries'], 0, ',', '.') }} @elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) - {{ \App\Services\Util::formatNumber($row['own_points']) }} - {{ \App\Services\Util::formatNumber($row['external_points']) }} - {{ \App\Services\Util::formatNumber($row['customer_abo_points']) }} + {{ \App\Services\Util::formatNumber($row['own_points']) }} {{ \App\Services\Util::formatNumber($row['customer_single_order_points']) }} + {{ \App\Services\Util::formatNumber($row['customer_abo_points']) }} {{ \App\Services\Util::formatNumber($row['customer_other_points']) }} + {{ \App\Services\Util::formatNumber($row['external_points']) }} {{ \App\Services\Util::formatNumber($row['total_points']) }} @else {{ $row['active_date'] ?? '-' }} @@ -188,11 +188,11 @@ {{ number_format($details['summary']['deliveries'], 0, ',', '.') }} @elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) - {{ \App\Services\Util::formatNumber($details['summary']['own_points']) }} - {{ \App\Services\Util::formatNumber($details['summary']['external_points']) }} - {{ \App\Services\Util::formatNumber($details['summary']['customer_abo_points']) }} + {{ \App\Services\Util::formatNumber($details['summary']['own_points']) }} {{ \App\Services\Util::formatNumber($details['summary']['customer_single_order_points']) }} + {{ \App\Services\Util::formatNumber($details['summary']['customer_abo_points']) }} {{ \App\Services\Util::formatNumber($details['summary']['customer_other_points']) }} + {{ \App\Services\Util::formatNumber($details['summary']['external_points']) }} {{ \App\Services\Util::formatNumber($details['summary']['total_points']) }} @else diff --git a/resources/views/user/backoffice/statistics/index.blade.php b/resources/views/user/backoffice/statistics/index.blade.php index 6267006f..b01fcd54 100644 --- a/resources/views/user/backoffice/statistics/index.blade.php +++ b/resources/views/user/backoffice/statistics/index.blade.php @@ -3,7 +3,7 @@ @section('content')

    - {{ __('navigation.statistics') }} + {{ __('navigation.team_statistics') }} VIP

    @@ -17,12 +17,13 @@ Linienbasierte Übersicht für alle vorhandenen Team-Linien. Klickbare Zahlen führen direkt in die passenden Namen- und Detaillisten.

    - @if (! empty($performance)) + @if (!empty($performance))
    Datenquelle: {{ $performance['source_label'] }} - Berechnet in {{ number_format($performance['duration_ms'], 2, ',', '.') }} ms - @if (! empty($performance['calculated_at'])) + Berechnet in {{ number_format($performance['duration_ms'], 2, ',', '.') }} + ms + @if (!empty($performance['calculated_at'])) Snapshot vom {{ $performance['calculated_at'] }} @endif
    @@ -59,12 +60,7 @@ @php - $clickableMetrics = [ - 'consultants', - 'total_points', - 'team_partner_abos', - 'team_customer_abos', - ]; + $clickableMetrics = ['consultants', 'total_points', 'team_partner_abos', 'team_customer_abos']; $metricHeadings = [ 'consultants' => 'Berater', @@ -100,7 +96,8 @@ 'metric' => 'new_partners', 'month' => $selectedMonth, 'year' => $selectedYear, - ]) }}" class="card h-100 text-decoration-none backoffice-kpi-card"> + ]) }}" + class="card h-100 text-decoration-none backoffice-kpi-card">
    Neupartner ({{ $selectedMonthName }} {{ $selectedYear }})
    @@ -121,10 +118,12 @@ 'metric' => 'shop_1000', 'month' => $selectedMonth, 'year' => $selectedYear, - ]) }}" class="card h-100 text-decoration-none backoffice-kpi-card"> + ]) }}" + class="card h-100 text-decoration-none backoffice-kpi-card">
    -
    1000-Punkte-Shops ({{ $selectedMonthName }} {{ $selectedYear }})
    +
    1000-Punkte-Shops ({{ $selectedMonthName }} {{ $selectedYear }}) +
    {{ number_format((float) ($statistics['totals']['shop_1000'] ?? 0), 0, ',', '.') }}
    diff --git a/resources/views/user/backoffice/statistics/my.blade.php b/resources/views/user/backoffice/statistics/my.blade.php new file mode 100644 index 00000000..3b3f97ac --- /dev/null +++ b/resources/views/user/backoffice/statistics/my.blade.php @@ -0,0 +1,204 @@ +@extends('layouts.layout-2') + +@section('content') + @php + $totals = $statistics['totals']; + $selectedMonthName = $filterMonths[(int) $selectedMonth] ?? $selectedMonth; + $fmt = fn($value) => \App\Services\Util::formatNumber($value); + + $kpiCards = [ + [ + 'label' => 'Eigenpunkte', + 'value' => $totals['own_points'], + 'icon' => 'ion-md-person', + 'color' => 'success', + 'highlight' => true, + ], + [ + 'label' => 'Shop-Punkte', + 'value' => $totals['shop_points'], + 'icon' => 'ion-md-cart', + 'color' => 'primary', + 'highlight' => false, + ], + [ + 'label' => 'Abo-Punkte', + 'value' => $totals['abo_points'], + 'icon' => 'ion-md-repeat', + 'color' => 'info', + 'highlight' => false, + ], + [ + 'label' => 'Gesamtpunkte', + 'value' => $totals['total_points'], + 'icon' => 'ion-md-stats', + 'color' => 'success', + 'highlight' => true, + ], + ]; + @endphp + +

    +
    + {{ __('navigation.my_statistics') }} + VIP +
    +

    + +
    +
    +
    +
    +
    {{ __('navigation.my_statistics') }} — {{ $selectedMonthName }} {{ $selectedYear }} +
    +

    + Nachvollziehbare Auflistung deiner Punkte und Umsätze des Monats, getrennt nach eigenen + Bestellungen, Shop-Bestellungen und Abos. +

    +
    +
    +
    + + + +
    +
    +
    +
    +
    + +
    + @foreach ($kpiCards as $card) +
    +
    +
    +
    +
    {{ $card['label'] }}
    +
    {{ $fmt($card['value']) }} +
    +
    + +
    +
    +
    + @endforeach +
    + +
    +
    + + Gesamtpunkte: + {{ $fmt($totals['total_points']) }} + + + Umsatz (netto): + {{ $fmt($totals['turnover_net']) }} € + +
    +
    + + @foreach ($statistics['groups'] as $group) + @if ($group['rows'] !== []) +
    +
    +
    {{ $group['label'] }}
    +
    + {{ count($group['rows']) }} Einträge · + {{ $fmt($group['points']) }} Punkte · + {{ $fmt($group['net']) }} € +
    +
    +
    +
    + + + + + + + + + @if (in_array($group['key'], ['shop', 'abo'], true)) + + @endif + + + + + @foreach ($group['rows'] as $row) + + + + + + + @if (in_array($group['key'], ['shop', 'abo'], true)) + + @endif + + + @endforeach + + + + + + + + + +
    DatumPunkteNettoTypBestellungKundeInfo
    {{ $row['date'] }}{{ $fmt($row['points']) }}{{ $fmt($row['total_net']) }} € + {{ $row['status_label'] }} + + @if ($row['order_id'] && $row['order_type'] === 'own') + {{ $row['order_id'] }} + @elseif ($row['order_id'] && $row['order_type'] === 'shop') + {{ $row['order_id'] }} + @else + — + @endif + + @if ($row['customer_name']) + {{ $row['customer_name'] }}
    + {{ $row['customer_email'] }}
    + @else + — + @endif +
    {{ $row['info'] ?: $row['message'] }}
    Summe{{ $fmt($group['points']) }}{{ $fmt($group['net']) }} €
    +
    +
    +
    + @endif + @endforeach + + @php + $hasRows = collect($statistics['groups'])->contains(fn($group) => $group['rows'] !== []); + @endphp + @unless ($hasRows) +
    +
    +

    {{ __('tables.no_data_available') }}

    +
    +
    + @endunless +@endsection diff --git a/routes/domains/crm.php b/routes/domains/crm.php index 881c9558..5b26bbf1 100644 --- a/routes/domains/crm.php +++ b/routes/domains/crm.php @@ -127,6 +127,7 @@ Route::domain(config('app.pre_url_crm').config('app.domain').config('app.tld_car // user backoffice statistics Route::get('/user/backoffice/statistics', 'User\BackofficeStatisticsController@index')->name('user_backoffice_statistics'); + Route::get('/user/backoffice/statistics/my', 'User\BackofficeStatisticsController@my')->name('user_my_statistics'); Route::get('/user/backoffice/statistics/details', 'User\BackofficeStatisticsController@details')->name('user_backoffice_statistics_details'); Route::get('/user/backoffice/statistics/export', 'User\BackofficeStatisticsController@export')->name('user_backoffice_statistics_export'); Route::get('/user/backoffice/statistics/export-overview', 'User\BackofficeStatisticsController@overviewExport')->name('user_backoffice_statistics_overview_export'); diff --git a/tests/Feature/BackofficeStatisticsAccessTest.php b/tests/Feature/BackofficeStatisticsAccessTest.php index 0c4092c6..7dd035fe 100644 --- a/tests/Feature/BackofficeStatisticsAccessTest.php +++ b/tests/Feature/BackofficeStatisticsAccessTest.php @@ -4,6 +4,7 @@ use App\Http\Controllers\User\BackofficeStatisticsController; use App\Models\ShoppingOrder; use App\Services\Backoffice\BackofficeDashboardService; use App\Services\Backoffice\BackofficeDrilldownService; +use App\Services\Backoffice\PersonalStatisticsService; use App\User; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -31,12 +32,13 @@ function makeBackofficeStatisticsRequest(User $user): Request return $request; } -function makeBackofficeStatisticsController(?BackofficeDashboardService $dashboardService = null, ?BackofficeDrilldownService $drilldownService = null): BackofficeStatisticsController +function makeBackofficeStatisticsController(?BackofficeDashboardService $dashboardService = null, ?BackofficeDrilldownService $drilldownService = null, ?PersonalStatisticsService $personalService = null): BackofficeStatisticsController { $dashboardService ??= Mockery::mock(BackofficeDashboardService::class); $drilldownService ??= Mockery::mock(BackofficeDrilldownService::class); + $personalService ??= Mockery::mock(PersonalStatisticsService::class); - return new BackofficeStatisticsController($dashboardService, $drilldownService); + return new BackofficeStatisticsController($dashboardService, $drilldownService, $personalService); } function backofficeStatisticsStreamedContent(StreamedResponse $response): string @@ -113,6 +115,34 @@ it('blockiert die Backoffice-Statistik fuer normale aktive User', function () { ->toThrow(NotFoundHttpException::class); }); +it('blockiert die persoenliche Statistik fuer normale aktive User', function () { + $user = makeBackofficeStatisticsUser(0); + $controller = makeBackofficeStatisticsController(); + + expect(fn () => $controller->my(makeBackofficeStatisticsRequest($user))) + ->toThrow(NotFoundHttpException::class); +}); + +it('liefert die persoenliche Statistik fuer VIP-User', function () { + $vip = makeBackofficeStatisticsUser(1); + $personalService = Mockery::mock(PersonalStatisticsService::class); + $personalService + ->shouldReceive('monthly') + ->once() + ->andReturn(['month' => 5, 'year' => 2026, 'totals' => [], 'groups' => []]); + + $controller = makeBackofficeStatisticsController(null, null, $personalService); + $request = makeBackofficeStatisticsRequest($vip); + $request->query->set('month', 5); + $request->query->set('year', 2026); + + $response = $controller->my($request); + + expect($response->name())->toBe('user.backoffice.statistics.my'); + expect($response->getData()['selectedMonth'])->toBe(5); + expect($response->getData()['statistics']['year'])->toBe(2026); +}); + it('erstellt einen CSV-Export fuer die Statistik-Uebersicht', function () { $vip = makeBackofficeStatisticsUser(1); $dashboardService = Mockery::mock(BackofficeDashboardService::class); @@ -229,9 +259,9 @@ it('erstellt einen CSV-Export fuer Detaildaten mit Karriere-Level und Summenzeil expect($response)->toBeInstanceOf(StreamedResponse::class); expect($response->headers->get('content-disposition'))->toContain('backoffice-statistik-shop_1000-linie-alle-05-2026.csv'); - expect($content)->toContain('Name;E-Mail;Karriere-Level;Eigenpunkte;"Externe Punkte"'); - expect($content)->toContain('"Max Mustermann";max@example.test;Partner;400;700;700;0;0;1100'); - expect($content)->toContain('Summe;"1 Eintraege";;400;700;700;0;0;1100'); + expect($content)->toContain('Name;E-Mail;Karriere-Level;Eigenpunkte;Shop-Punkte;Abo-Punkte;"Sonstige Punkte";Kundenpunkte;Gesamtpunkte'); + expect($content)->toContain('"Max Mustermann";max@example.test;Partner;400;0;700;0;700;1100'); + expect($content)->toContain('Summe;"1 Eintraege";;400;0;700;0;700;1100'); }); it('nutzt den gespeicherten Zeitraum fuer Detailansicht und Export', function () { diff --git a/tests/Unit/Services/BackofficeDashboardServiceTest.php b/tests/Unit/Services/BackofficeDashboardServiceTest.php index 186381e2..cfe6cefd 100644 --- a/tests/Unit/Services/BackofficeDashboardServiceTest.php +++ b/tests/Unit/Services/BackofficeDashboardServiceTest.php @@ -122,6 +122,10 @@ beforeEach(function () { $table->unsignedInteger('shopping_order_id')->nullable(); $table->unsignedTinyInteger('month')->nullable(); $table->unsignedSmallInteger('year')->nullable(); + $table->decimal('points', 13, 2)->nullable(); + $table->decimal('total_net', 13, 2)->nullable(); + $table->unsignedTinyInteger('status')->nullable(); + $table->unsignedTinyInteger('status_turnover')->nullable(); $table->decimal('month_KP_points', 13, 2)->nullable(); $table->decimal('month_shop_points', 13, 2)->nullable(); $table->decimal('month_total_net', 13, 2)->nullable(); @@ -276,27 +280,32 @@ it('aggregiert Linien, Abos und Punkte fuer die Backoffice-Statistik', function 'updated_at' => now(), ]); - UserSalesVolume::forceCreate([ - 'user_id' => $lineOne->id, - 'shopping_order_id' => $aboOrderId, - 'month' => 5, - 'year' => 2026, - 'month_KP_points' => 400, - 'month_shop_points' => 700, - 'month_total_net' => 100, - 'month_shop_total_net' => 200, - ]); + // Mehrere Zeilen je User/Monat: Die month_*-Spalten sind kumulative Laufstände + // (NICHT Einzelbeträge). Eine SUM() darüber zählt mehrfach – die Auswertung muss + // stattdessen je Zeile `points`/`total_net` nach `status` summieren. + $makeVolume = function (array $attributes): void { + UserSalesVolume::forceCreate(array_merge([ + 'month' => 5, + 'year' => 2026, + 'status_turnover' => null, + 'month_KP_points' => 0, + 'month_shop_points' => 0, + 'month_total_net' => 0, + 'month_shop_total_net' => 0, + ], $attributes)); + }; - UserSalesVolume::forceCreate([ - 'user_id' => $lineTwo->id, - 'shopping_order_id' => $singleOrderId, - 'month' => 5, - 'year' => 2026, - 'month_KP_points' => 200, - 'month_shop_points' => 300, - 'month_total_net' => 50, - 'month_shop_total_net' => 60, - ]); + // lineOne: Eigenpunkte 150 + 250 = 400, Shop (Abo-Order) 300 + 400 = 700 + $makeVolume(['user_id' => $lineOne->id, 'shopping_order_id' => $aboOrderId, 'status' => 1, 'points' => 150, 'total_net' => 40, 'month_KP_points' => 150, 'month_total_net' => 40]); + $makeVolume(['user_id' => $lineOne->id, 'shopping_order_id' => $aboOrderId, 'status' => 1, 'points' => 250, 'total_net' => 60, 'month_KP_points' => 400, 'month_total_net' => 100]); + $makeVolume(['user_id' => $lineOne->id, 'shopping_order_id' => $aboOrderId, 'status' => 2, 'points' => 300, 'total_net' => 120, 'month_shop_points' => 300, 'month_shop_total_net' => 120]); + $makeVolume(['user_id' => $lineOne->id, 'shopping_order_id' => $aboOrderId, 'status' => 2, 'points' => 400, 'total_net' => 80, 'month_shop_points' => 700, 'month_shop_total_net' => 200]); + // status 3 (pending) darf nirgends mitgezählt werden + $makeVolume(['user_id' => $lineOne->id, 'shopping_order_id' => null, 'status' => 3, 'points' => 999, 'total_net' => 999]); + + // lineTwo: Eigenpunkte 200, Shop (Einzelbestellung) 300 + $makeVolume(['user_id' => $lineTwo->id, 'shopping_order_id' => $singleOrderId, 'status' => 1, 'points' => 200, 'total_net' => 50, 'month_KP_points' => 200, 'month_total_net' => 50]); + $makeVolume(['user_id' => $lineTwo->id, 'shopping_order_id' => $singleOrderId, 'status' => 2, 'points' => 300, 'total_net' => 60, 'month_shop_points' => 300, 'month_shop_total_net' => 60]); $overview = (new BackofficeDashboardService)->overview($root, 5, 2026); @@ -312,12 +321,14 @@ it('aggregiert Linien, Abos und Punkte fuer die Backoffice-Statistik', function expect($overview['lines'][1]['customer_single_order_points'])->toBe(0.0); expect($overview['lines'][1]['customer_other_points'])->toBe(0.0); expect($overview['lines'][1]['total_points'])->toBe(1100.0); + expect($overview['lines'][1]['turnover_net'])->toBe(300.0); expect($overview['lines'][1]['shop_1000'])->toBe(1); expect($overview['lines'][2]['consultants'])->toBe(1); expect($overview['lines'])->toHaveKey(9); expect($overview['lines'])->not->toHaveKey(10); expect($overview['lines'][9]['consultants'])->toBe(1); expect($overview['totals']['total_points'])->toBe(1600.0); + expect($overview['totals']['turnover_net'])->toBe(410.0); $details = (new BackofficeDrilldownService(new BackofficeDashboardService))->details($root, 0, 'total_points', 5, 2026); diff --git a/tests/Unit/Services/PersonalStatisticsServiceTest.php b/tests/Unit/Services/PersonalStatisticsServiceTest.php new file mode 100644 index 00000000..25aba3d9 --- /dev/null +++ b/tests/Unit/Services/PersonalStatisticsServiceTest.php @@ -0,0 +1,122 @@ +increments('id'); + $table->string('email')->unique(); + $table->string('password'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('shopping_users', function ($table) { + $table->increments('id'); + $table->string('billing_firstname')->nullable(); + $table->string('billing_lastname')->nullable(); + $table->string('billing_email')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('shopping_orders', function ($table) { + $table->increments('id'); + $table->unsignedInteger('shopping_user_id')->nullable(); + $table->boolean('is_abo')->default(false); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('user_sales_volumes', function ($table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('shopping_order_id')->nullable(); + $table->unsignedTinyInteger('month')->nullable(); + $table->unsignedSmallInteger('year')->nullable(); + $table->decimal('points', 13, 2)->nullable(); + $table->decimal('total_net', 13, 2)->nullable(); + $table->unsignedTinyInteger('status')->nullable(); + $table->unsignedTinyInteger('status_turnover')->nullable(); + $table->string('date')->nullable(); + $table->string('info')->nullable(); + $table->string('message')->nullable(); + $table->timestamps(); + }); +}); + +it('gruppiert die Monatspunkte nach Eigen, Shop, Abo und Sonstige', function () { + $user = User::forceCreate(['email' => 'me@test.test', 'password' => 'secret']); + + $customer = DB::table('shopping_users')->insertGetId([ + 'billing_firstname' => 'Erika', 'billing_lastname' => 'Musterfrau', 'billing_email' => 'erika@test.test', + 'created_at' => now(), 'updated_at' => now(), + ]); + + $shopOrderId = DB::table('shopping_orders')->insertGetId([ + 'shopping_user_id' => $customer, 'is_abo' => false, 'created_at' => now(), 'updated_at' => now(), + ]); + $aboOrderId = DB::table('shopping_orders')->insertGetId([ + 'shopping_user_id' => $customer, 'is_abo' => true, 'created_at' => now(), 'updated_at' => now(), + ]); + $ownOrderId = DB::table('shopping_orders')->insertGetId([ + 'is_abo' => false, 'created_at' => now(), 'updated_at' => now(), + ]); + + $make = function (array $attributes) use ($user): void { + UserSalesVolume::forceCreate(array_merge([ + 'user_id' => $user->id, 'month' => 6, 'year' => 2026, 'date' => '01.06.2026', + ], $attributes)); + }; + + // Eigene Bestellungen (status 1): 150 + 250 = 400 + $make(['shopping_order_id' => $ownOrderId, 'status' => 1, 'points' => 150, 'total_net' => 40]); + $make(['shopping_order_id' => $ownOrderId, 'status' => 1, 'points' => 250, 'total_net' => 60]); + // Shop-Bestellung (status 2, keine Abo-Order): 300 + $make(['shopping_order_id' => $shopOrderId, 'status' => 2, 'points' => 300, 'total_net' => 120]); + // Abo (status 2, Abo-Order): 500 + $make(['shopping_order_id' => $aboOrderId, 'status' => 2, 'points' => 500, 'total_net' => 200]); + // Sonstige (status 5, ohne Order): 50 + $make(['shopping_order_id' => null, 'status' => 5, 'points' => 50, 'total_net' => 10]); + + $result = (new PersonalStatisticsService)->monthly($user, 6, 2026); + + expect($result['totals']['own_points'])->toBe(400.0); + expect($result['totals']['shop_points'])->toBe(300.0); + expect($result['totals']['abo_points'])->toBe(500.0); + expect($result['totals']['other_points'])->toBe(50.0); + expect($result['totals']['total_points'])->toBe(1250.0); + expect($result['totals']['turnover_net'])->toBe(430.0); + + expect($result['groups']['own']['rows'])->toHaveCount(2); + expect($result['groups']['shop']['rows'])->toHaveCount(1); + expect($result['groups']['abo']['rows'])->toHaveCount(1); + expect($result['groups']['other']['rows'])->toHaveCount(1); + + expect($result['groups']['own']['rows'][0]['order_type'])->toBe('own'); + expect($result['groups']['shop']['rows'][0]['order_type'])->toBe('shop'); + expect($result['groups']['shop']['rows'][0]['customer_name'])->toBe('Erika Musterfrau'); + expect($result['groups']['abo']['rows'][0]['customer_email'])->toBe('erika@test.test'); +}); + +it('liefert leere Gruppen ohne Umsaetze', function () { + $user = User::forceCreate(['email' => 'empty@test.test', 'password' => 'secret']); + + $result = (new PersonalStatisticsService)->monthly($user, 6, 2026); + + expect($result['totals']['total_points'])->toBe(0.0); + expect($result['groups']['own']['rows'])->toBe([]); + expect($result['groups']['shop']['rows'])->toBe([]); +});