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:
parent
9565814221
commit
7a620d66ae
14 changed files with 666 additions and 70 deletions
|
|
@ -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();
|
||||
|
|
|
|||
121
app/Services/Backoffice/PersonalStatisticsService.php
Normal file
121
app/Services/Backoffice/PersonalStatisticsService.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Backoffice;
|
||||
|
||||
use App\Models\UserSalesVolume;
|
||||
use App\User;
|
||||
|
||||
/**
|
||||
* Persönliche Monatsstatistik eines Beraters: nachvollziehbare Auflistung der
|
||||
* Punkte/Umsätze des Monats, gruppiert nach eigenen Bestellungen, Shop-Bestellungen
|
||||
* und Abos.
|
||||
*
|
||||
* Im Gegensatz zur Team-Statistik ({@see BackofficeDashboardService}) werden hier die
|
||||
* einzelnen `user_sales_volumes`-Zeilen (per-Zeile `points`/`total_net`) des Users selbst
|
||||
* gruppiert – nicht die kumulativen month_*-Snapshots.
|
||||
*/
|
||||
class PersonalStatisticsService
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* month: int,
|
||||
* year: int,
|
||||
* totals: array{own_points: float, shop_points: float, abo_points: float, other_points: float, total_points: float, turnover_net: float},
|
||||
* groups: array<string, array{key: string, label: string, points: float, net: float, rows: array<int, array<string, mixed>>}>
|
||||
* }
|
||||
*/
|
||||
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<int, array<string, mixed>>}
|
||||
*/
|
||||
private function emptyGroup(string $key, string $label): array
|
||||
{
|
||||
return [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'points' => 0.0,
|
||||
'net' => 0.0,
|
||||
'rows' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
71
app/Services/Backoffice/SalesVolumePoints.php
Normal file
71
app/Services/Backoffice/SalesVolumePoints.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Backoffice;
|
||||
|
||||
/**
|
||||
* Liefert SQL-Aggregat-Ausdrücke für die monatliche Punkte-/Umsatzauswertung
|
||||
* der Backoffice-Statistik.
|
||||
*
|
||||
* Wichtig: In `user_sales_volumes` existiert pro Bestellung eine Zeile, und die
|
||||
* Spalten `month_KP_points` / `month_shop_points` / `month_*_total_net` sind der
|
||||
* kumulative Monats-Laufstand zum Zeitpunkt der Bestellung – KEINE Einzelbeträge.
|
||||
* Eine SUM() über diese Spalten zählt mehrfach. Korrekt wird je Zeile die
|
||||
* `points`/`total_net`-Spalte summiert und nach `status` gefiltert – analog zu
|
||||
* {@see \App\Services\BusinessPlan\SalesPointsVolume::reCalculateSalesPointsVolume()}.
|
||||
*
|
||||
* Status-Zuordnung:
|
||||
* - Eigenpunkte (KP): status 1, 4, 5, 6
|
||||
* - Shop-/Kundenpunkte: status 2 (sowie status 6 mit status_turnover = 2)
|
||||
* - status 0 (nicht zugeordnet) und 3 (pending) zählen nicht
|
||||
*
|
||||
* Alle Spalten werden voll qualifiziert, da `shopping_orders` ebenfalls eine
|
||||
* `points`-Spalte besitzt (Join-Mehrdeutigkeit).
|
||||
*/
|
||||
class SalesVolumePoints
|
||||
{
|
||||
private const OWN_PREDICATE = 'user_sales_volumes.status IN (1, 4, 5, 6)';
|
||||
|
||||
private const SHOP_PREDICATE = '(user_sales_volumes.status = 2 OR (user_sales_volumes.status = 6 AND user_sales_volumes.status_turnover = 2))';
|
||||
|
||||
private const TURNOVER_PREDICATE = 'user_sales_volumes.status IN (1, 2, 4, 5, 6)';
|
||||
|
||||
public static function ownPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::OWN_PREDICATE, 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function externalPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::SHOP_PREDICATE, 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function customerAboPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.is_abo = 1', 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function customerSingleOrderPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.id IS NOT NULL AND (shopping_orders.is_abo = 0 OR shopping_orders.is_abo IS NULL)', 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function customerOtherPoints(): string
|
||||
{
|
||||
return self::sumWhere(self::SHOP_PREDICATE.' AND shopping_orders.id IS NULL', 'user_sales_volumes.points');
|
||||
}
|
||||
|
||||
public static function totalPoints(): string
|
||||
{
|
||||
return self::ownPoints().' + '.self::externalPoints();
|
||||
}
|
||||
|
||||
public static function turnoverNet(): string
|
||||
{
|
||||
return self::sumWhere(self::TURNOVER_PREDICATE, 'user_sales_volumes.total_net');
|
||||
}
|
||||
|
||||
private static function sumWhere(string $predicate, string $column): string
|
||||
{
|
||||
return 'COALESCE(SUM(CASE WHEN '.$predicate.' THEN '.$column.' ELSE 0 END), 0)';
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue