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 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-06-25 13:31:14 +00:00
parent 9565814221
commit 7a620d66ae
14 changed files with 666 additions and 70 deletions

View file

@ -89,11 +89,11 @@
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Nächste Ausführung <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Lieferungen <i class="ion ion-ios-swap sort-icon"></i></span></th>
@elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true))
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Eigenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Externe Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Kundenabo-Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Einzelbestellungs-Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Sonstige Kundenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number" style="background-color:#d4edda;"><span class="sortable-label justify-content-end">Eigenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Shop-Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Abo-Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Sonstige Punkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number" style="background-color:#d4edda;"><span class="sortable-label justify-content-end">Kundenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Gesamtpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
@else
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Aktiv seit <i class="ion ion-ios-swap sort-icon"></i></span></th>
@ -151,11 +151,11 @@
<td>{{ $row['next_date'] ?? '-' }}</td>
<td class="text-right">{{ number_format($row['deliveries'], 0, ',', '.') }}</td>
@elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true))
<td class="text-right">{{ \App\Services\Util::formatNumber($row['own_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($row['external_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_abo_points']) }}</td>
<td class="text-right"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['own_points']) }}</span></td>
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_single_order_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_abo_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($row['customer_other_points']) }}</td>
<td class="text-right font-weight-bold"><span class="badge" style="background-color:#d4edda;color:#155724;font-size:90%;">{{ \App\Services\Util::formatNumber($row['external_points']) }}</span></td>
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($row['total_points']) }}</td>
@else
<td>{{ $row['active_date'] ?? '-' }}</td>
@ -188,11 +188,11 @@
<td></td>
<td class="text-right">{{ number_format($details['summary']['deliveries'], 0, ',', '.') }}</td>
@elseif (in_array($details['metric'], ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true))
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['own_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['external_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_abo_points']) }}</td>
<td class="text-right" style="background-color:#d4edda;">{{ \App\Services\Util::formatNumber($details['summary']['own_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_single_order_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_abo_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_other_points']) }}</td>
<td class="text-right" style="background-color:#d4edda;">{{ \App\Services\Util::formatNumber($details['summary']['external_points']) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['total_points']) }}</td>
@else
<td></td>

View file

@ -3,7 +3,7 @@
@section('content')
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
<div>
{{ __('navigation.statistics') }}
{{ __('navigation.team_statistics') }}
<span class="badge badge-secondary ml-2">VIP</span>
</div>
</h4>
@ -17,12 +17,13 @@
Linienbasierte Übersicht für alle vorhandenen Team-Linien. Klickbare Zahlen führen direkt in die
passenden Namen- und Detaillisten.
</p>
@if (! empty($performance))
@if (!empty($performance))
<div class="small text-muted mt-2">
Datenquelle:
<span class="badge badge-outline-default">{{ $performance['source_label'] }}</span>
<span class="ml-2">Berechnet in {{ number_format($performance['duration_ms'], 2, ',', '.') }} ms</span>
@if (! empty($performance['calculated_at']))
<span class="ml-2">Berechnet in {{ number_format($performance['duration_ms'], 2, ',', '.') }}
ms</span>
@if (!empty($performance['calculated_at']))
<span class="ml-2">Snapshot vom {{ $performance['calculated_at'] }}</span>
@endif
</div>
@ -59,12 +60,7 @@
</div>
@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">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">Neupartner ({{ $selectedMonthName }} {{ $selectedYear }})</div>
@ -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">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">1000-Punkte-Shops ({{ $selectedMonthName }} {{ $selectedYear }})</div>
<div class="text-muted small">1000-Punkte-Shops ({{ $selectedMonthName }} {{ $selectedYear }})
</div>
<div class="display-4 font-weight-bold text-success">
{{ number_format((float) ($statistics['totals']['shop_1000'] ?? 0), 0, ',', '.') }}
</div>

View file

@ -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
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
<div>
{{ __('navigation.my_statistics') }}
<span class="badge badge-secondary ml-2">VIP</span>
</div>
</h4>
<div class="card mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="mb-2">{{ __('navigation.my_statistics') }} — {{ $selectedMonthName }} {{ $selectedYear }}
</h5>
<p class="text-muted mb-md-0">
Nachvollziehbare Auflistung deiner Punkte und Umsätze des Monats, getrennt nach eigenen
Bestellungen, Shop-Bestellungen und Abos.
</p>
</div>
<div class="col-md-4 mt-3 mt-md-0">
<form method="GET" action="{{ route('user_my_statistics') }}"
class="form-inline justify-content-md-end">
<select name="month" class="form-control custom-select form-control-sm mr-2">
@foreach ($filterMonths as $monthNumber => $monthName)
<option value="{{ $monthNumber }}"
{{ (int) $selectedMonth === (int) $monthNumber ? 'selected' : '' }}>
{{ $monthName }}
</option>
@endforeach
</select>
<select name="year" class="form-control custom-select form-control-sm mr-2">
@foreach ($filterYears as $year)
<option value="{{ $year }}"
{{ (int) $selectedYear === (int) $year ? 'selected' : '' }}>
{{ $year }}
</option>
@endforeach
</select>
<button type="submit" class="btn btn-sm btn-primary">Anzeigen</button>
</form>
</div>
</div>
</div>
</div>
<div class="row mb-2">
@foreach ($kpiCards as $card)
<div class="col-md-6 col-lg-3 mb-4">
<div class="card h-100"
@if ($card['highlight']) style="background-color:#eafaf0;border:1px solid #c3e6cb;" @endif>
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small">{{ $card['label'] }}</div>
<div class="display-4 font-weight-bold text-{{ $card['color'] }}">{{ $fmt($card['value']) }}
</div>
</div>
<span class="ion {{ $card['icon'] }} display-4 text-muted"></span>
</div>
</div>
</div>
@endforeach
</div>
<div class="card mb-4">
<div class="card-body py-3 d-flex flex-wrap align-items-center">
<span class="mr-4 mb-1">
<span class="text-muted small">Gesamtpunkte:</span>
<strong>{{ $fmt($totals['total_points']) }}</strong>
</span>
<span class="mb-1">
<span class="text-muted small">Umsatz (netto):</span>
<strong>{{ $fmt($totals['turnover_net']) }} &euro;</strong>
</span>
</div>
</div>
@foreach ($statistics['groups'] as $group)
@if ($group['rows'] !== [])
<div class="card mb-4">
<h5 class="card-header with-elements d-flex justify-content-between align-items-center flex-wrap">
<div class="card-header-title">{{ $group['label'] }}</div>
<div class="text-muted small">
{{ count($group['rows']) }} Einträge ·
<strong class="text-body">{{ $fmt($group['points']) }}</strong> Punkte ·
<strong class="text-body">{{ $fmt($group['net']) }} &euro;</strong>
</div>
</h5>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Datum</th>
<th class="text-right">Punkte</th>
<th class="text-right">Netto</th>
<th>Typ</th>
<th>Bestellung</th>
@if (in_array($group['key'], ['shop', 'abo'], true))
<th>Kunde</th>
@endif
<th>Info</th>
</tr>
</thead>
<tbody>
@foreach ($group['rows'] as $row)
<tr>
<td class="font-weight-semibold">{{ $row['date'] }}</td>
<td class="text-right font-weight-semibold">{{ $fmt($row['points']) }}</td>
<td class="text-right">{{ $fmt($row['total_net']) }} &euro;</td>
<td>
<span
class="badge badge-pill badge-{{ $row['status_color'] }}">{{ $row['status_label'] }}</span>
</td>
<td>
@if ($row['order_id'] && $row['order_type'] === 'own')
<a href="{{ route('user_order_detail', [$row['order_id']]) }}"
class="btn btn-xs btn-primary">{{ $row['order_id'] }}</a>
@elseif ($row['order_id'] && $row['order_type'] === 'shop')
<a href="{{ route('user_shop_order_detail', [$row['order_id']]) }}"
class="btn btn-xs btn-secondary">{{ $row['order_id'] }}</a>
@else
<span class="text-muted">—</span>
@endif
</td>
@if (in_array($group['key'], ['shop', 'abo'], true))
<td>
@if ($row['customer_name'])
<span class="no-line-break">{{ $row['customer_name'] }}<br>
<small
class="text-muted">{{ $row['customer_email'] }}</small></span>
@else
<span class="text-muted">—</span>
@endif
</td>
@endif
<td><span class="no-line-break">{{ $row['info'] ?: $row['message'] }}</span></td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="font-weight-bold bg-light">
<td>Summe</td>
<td class="text-right">{{ $fmt($group['points']) }}</td>
<td class="text-right">{{ $fmt($group['net']) }} &euro;</td>
<td colspan="{{ in_array($group['key'], ['shop', 'abo'], true) ? 4 : 3 }}"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
@endif
@endforeach
@php
$hasRows = collect($statistics['groups'])->contains(fn($group) => $group['rows'] !== []);
@endphp
@unless ($hasRows)
<div class="card mb-4">
<div class="card-body">
<p class="text-muted mb-0">{{ __('tables.no_data_available') }}</p>
</div>
</div>
@endunless
@endsection