Die Closure fuer den Link des gruenen "+N"-Badges band die neue Variable nicht per use ein; sobald eine Linie neue Abos zaehlte, brach die Uebersicht mit "Undefined variable $selectedTree" ab. Regressionstest rendert die Uebersicht mit einem neuen Teamabo und prueft den Badge-Link. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
809 lines
48 KiB
PHP
809 lines
48 KiB
PHP
@extends('layouts.layout-2')
|
||
|
||
@section('content')
|
||
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
|
||
<div>
|
||
{{ __('navigation.team_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.statistics') }} / Backoffice MVP</h5>
|
||
<p class="text-muted mb-1">
|
||
Linienbasierte Übersicht für alle vorhandenen Team-Linien. Klickbare Zahlen führen direkt in die
|
||
passenden Namen- und Detaillisten.
|
||
</p>
|
||
<p class="text-muted small mb-md-0">
|
||
Baum-Definition:
|
||
<span class="badge badge-outline-primary">{{ $treeLabels[$selectedTree] ?? $selectedTree }}</span>
|
||
@if ($selectedTree === \App\Services\Backoffice\BackofficeDashboardService::TREE_COMMISSION)
|
||
— dieselben Mitglieder und dieselben Punkte (Provisionspunkte) wie die
|
||
<a href="{{ route('user_team_structure') }}">Team-Struktur</a>.
|
||
@if (($statistics['tree_source'] ?? 'live') === 'stored_structure')
|
||
Mitglieder je Linie stammen aus dem gespeicherten Provisionslauf dieses Monats.
|
||
@else
|
||
Im laufenden Monat rechnen beide Seiten live; die Provisions-Kachel ist bis zu 6 Stunden
|
||
zwischengespeichert.
|
||
@endif
|
||
@else
|
||
— jeder nicht gelöschte Teampartner zählt, auch ohne Mitgliedsebene oder Abrechnungskonto und
|
||
unterhalb solcher Partner. Diese Sicht weicht bewusst von der Team-Struktur ab.
|
||
@endif
|
||
</p>
|
||
@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">Snapshot vom {{ $performance['calculated_at'] }}</span>
|
||
@endif
|
||
@if (($performance['source'] ?? null) === 'snapshot')
|
||
<span class="badge badge-secondary ml-2" title="Abgeschlossener Monat: Punkte, Umsätze, Abo-Bestände, Team-Struktur und alle Detaillisten zeigen den Stand zum Monatsabschluss. Spätere Änderungen — Korrekturen, Kündigungen, Sponsorwechsel, neue Teampartner — verändern diesen Monat nicht mehr.">Monat abgeschlossen · Stand eingefroren</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
<div class="col-md-4 mt-3 mt-md-0">
|
||
<form method="GET" action="{{ route('user_backoffice_statistics') }}"
|
||
class="form-inline justify-content-md-end">
|
||
<a href="{{ route('user_backoffice_statistics_overview_export', ['month' => $selectedMonth, 'year' => $selectedYear, 'tree' => $selectedTree]) }}"
|
||
class="btn btn-sm btn-outline-secondary mr-2">
|
||
<span class="ion ion-md-download mr-1"></span> CSV
|
||
</a>
|
||
<select name="tree" class="form-control custom-select form-control-sm mr-2" title="Baum-Definition">
|
||
@foreach ($treeLabels as $treeKey => $treeLabel)
|
||
<option value="{{ $treeKey }}" {{ $selectedTree === $treeKey ? 'selected' : '' }}>
|
||
{{ $treeLabel }}
|
||
</option>
|
||
@endforeach
|
||
</select>
|
||
<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>
|
||
|
||
@php
|
||
$clickableMetrics = ['consultants', 'total_points', 'team_partner_abos', 'team_customer_abos'];
|
||
|
||
$metricHeadings = [
|
||
'consultants' => 'Berater',
|
||
'total_points' => 'Umsatz (Punkte)',
|
||
'team_partner_abos' => 'Teamabos',
|
||
'team_customer_abos' => 'Teamkundenabos',
|
||
];
|
||
|
||
$formatValue = function ($value, string $metric): string {
|
||
if ($metric === 'total_points') {
|
||
return \App\Services\Util::formatNumber($value);
|
||
}
|
||
|
||
return number_format((float) $value, 0, ',', '.');
|
||
};
|
||
|
||
$newAboMetric = function (array $row, string $metric): ?string {
|
||
return match ($metric) {
|
||
'consultants' => 'new_partners',
|
||
'team_partner_abos' => 'team_partner_abos_new',
|
||
'team_customer_abos' => 'team_customer_abos_new',
|
||
default => null,
|
||
};
|
||
};
|
||
|
||
// Ziel-Link des grünen "+N"-Badges: Neupartner-Liste bzw. Abo-Drilldown
|
||
// gefiltert auf die im Monat neuen Abos (only_new).
|
||
$newBadgeLink = function (int $lineNo, string $metric) use ($selectedMonth, $selectedYear, $selectedTree): string {
|
||
if ($metric === 'consultants') {
|
||
return route('user_backoffice_statistics_details', [
|
||
'line' => $lineNo,
|
||
'metric' => 'new_partners',
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
]);
|
||
}
|
||
|
||
return route('user_backoffice_statistics_details', [
|
||
'line' => $lineNo,
|
||
'metric' => $metric,
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
'only_new' => 1,
|
||
]);
|
||
};
|
||
|
||
$selectedMonthName = $filterMonths[(int) $selectedMonth] ?? $selectedMonth;
|
||
@endphp
|
||
|
||
<div class="row mb-4">
|
||
<div class="col-md-4 mb-3 mb-md-0">
|
||
<a href="{{ route('user_backoffice_statistics_details', [
|
||
'line' => 0,
|
||
'metric' => 'new_partners',
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
]) }}"
|
||
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>
|
||
<div class="display-4 font-weight-bold text-primary">
|
||
{{ number_format((float) ($statistics['totals']['new_partners'] ?? 0), 0, ',', '.') }}
|
||
</div>
|
||
<div class="text-primary small font-weight-semibold mt-1">
|
||
<span class="ion ion-md-eye mr-1"></span> Liste anzeigen
|
||
</div>
|
||
</div>
|
||
<span class="ion ion-md-person-add display-4 text-muted"></span>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div class="col-md-4 mb-3 mb-md-0">
|
||
<a href="{{ route('user_backoffice_statistics_details', [
|
||
'line' => 0,
|
||
'metric' => 'shop_1000',
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
]) }}"
|
||
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="display-4 font-weight-bold text-success">
|
||
{{ number_format((float) ($statistics['totals']['shop_1000'] ?? 0), 0, ',', '.') }}
|
||
</div>
|
||
<div class="text-primary small font-weight-semibold mt-1">
|
||
<span class="ion ion-md-eye mr-1"></span> Liste anzeigen
|
||
</div>
|
||
</div>
|
||
<span class="ion ion-md-trophy display-4 text-muted"></span>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
@if (isset($commission))
|
||
<div class="col-md-4">
|
||
<div class="card h-100">
|
||
<div class="card-body d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<div class="text-muted small">
|
||
Provision ({{ $selectedMonthName }} {{ $selectedYear }})
|
||
@if (! empty($commission['is_live']))
|
||
<span class="badge badge-pill badge-info ml-1"
|
||
title="Vorläufig, live berechnet — final nach Monatsabschluss. Aktualisierung ca. alle 6 Stunden.">Live</span>
|
||
@endif
|
||
</div>
|
||
<div class="display-4 font-weight-bold text-warning">
|
||
{{ number_format((float) ($commission['total'] ?? 0), 2, ',', '.') }} €
|
||
</div>
|
||
<div class="text-muted small mt-1">
|
||
Payline {{ number_format((float) ($commission['pp'] ?? 0), 2, ',', '.') }} €
|
||
· Shop {{ number_format((float) ($commission['shop'] ?? 0), 2, ',', '.') }} €
|
||
· WB {{ number_format((float) ($commission['growth'] ?? 0), 2, ',', '.') }} €
|
||
</div>
|
||
</div>
|
||
<span class="ion ion-md-cash display-4 text-muted"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="card mb-4">
|
||
<div class="card-body p-0">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Linie</th>
|
||
@foreach ($clickableMetrics as $metric)
|
||
<th class="text-right">{{ $metricHeadings[$metric] }}</th>
|
||
@endforeach
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($statistics['lines'] as $line)
|
||
<tr>
|
||
<td class="font-weight-semibold">{{ $line['label'] }}</td>
|
||
@foreach ($clickableMetrics as $metric)
|
||
<td class="text-right">
|
||
@if ($line[$metric] != 0)
|
||
<a href="{{ route('user_backoffice_statistics_details', [
|
||
'line' => $line['line'],
|
||
'metric' => $metric,
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
]) }}"
|
||
class="badge badge-pill badge-outline-default backoffice-statistics-click-badge">
|
||
{{ $formatValue($line[$metric], $metric) }}
|
||
</a>
|
||
@php $newMetric = $newAboMetric($line, $metric); @endphp
|
||
@if ($newMetric && $line[$newMetric] != 0)
|
||
<a href="{{ $newBadgeLink($line['line'], $metric) }}"
|
||
class="badge badge-pill badge-success ml-1 backoffice-statistics-click-badge"
|
||
title="Neue im Monat anzeigen">+{{ number_format($line[$newMetric], 0, ',', '.') }}</a>
|
||
@endif
|
||
@else
|
||
<span
|
||
class="badge badge-pill badge-light backoffice-statistics-empty-badge">0</span>
|
||
@endif
|
||
</td>
|
||
@endforeach
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="font-weight-bold bg-light">
|
||
<td>{{ $statistics['totals']['label'] }}</td>
|
||
@foreach ($clickableMetrics as $metric)
|
||
<td class="text-right">
|
||
@if ($statistics['totals'][$metric] != 0)
|
||
<a href="{{ route('user_backoffice_statistics_details', [
|
||
'line' => 0,
|
||
'metric' => $metric,
|
||
'month' => $selectedMonth,
|
||
'year' => $selectedYear,
|
||
'tree' => $selectedTree,
|
||
]) }}"
|
||
class="badge badge-pill badge-secondary backoffice-statistics-click-badge">
|
||
{{ $formatValue($statistics['totals'][$metric], $metric) }}
|
||
</a>
|
||
@php $newMetric = $newAboMetric($statistics['totals'], $metric); @endphp
|
||
@if ($newMetric && $statistics['totals'][$newMetric] != 0)
|
||
<a href="{{ $newBadgeLink(0, $metric) }}"
|
||
class="badge badge-pill badge-success ml-1 backoffice-statistics-click-badge"
|
||
title="Neue im Monat anzeigen">+{{ number_format($statistics['totals'][$newMetric], 0, ',', '.') }}</a>
|
||
@endif
|
||
@else
|
||
<span
|
||
class="badge badge-pill badge-light backoffice-statistics-empty-badge">0</span>
|
||
@endif
|
||
</td>
|
||
@endforeach
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
@php
|
||
$nonPaylinePoints = $statistics['non_payline_points'] ?? null;
|
||
$stornoFloorPoints = $statistics['storno_floor_points'] ?? null;
|
||
$isLegacyPayload = (int) ($statistics['payload_version'] ?? 1) < 3;
|
||
@endphp
|
||
<div class="card-body py-2 border-top">
|
||
<p class="text-muted small mb-0">
|
||
@if ($isLegacyPayload)
|
||
<strong>Umsatz (Punkte)</strong> = Eigen- + Kundenpunkte (ältere Berechnung dieses Monats,
|
||
inkl. Punkte ohne Payline-Wirkung).
|
||
@if ($nonPaylinePoints !== null && $nonPaylinePoints > 0)
|
||
Davon <strong>{{ \App\Services\Util::formatNumber($nonPaylinePoints) }} Punkte</strong> ohne
|
||
Payline-Wirkung — die Team-Struktur fällt um diesen Betrag niedriger aus.
|
||
@endif
|
||
@else
|
||
<strong>Umsatz (Punkte)</strong> = Provisionspunkte (Teampunkte + Kundenpunkte), so wie sie in
|
||
der Team-Struktur je Linie stehen und verprovisioniert werden.
|
||
@if ($nonPaylinePoints !== null && $nonPaylinePoints > 0)
|
||
Nicht enthalten: <strong>{{ \App\Services\Util::formatNumber($nonPaylinePoints) }} Punkte</strong>
|
||
ohne Payline-Wirkung (z. B. Neupartner-Einschreibungspunkte) — sie zählen nur als
|
||
Karrierepunkte des jeweiligen Beraters.
|
||
@endif
|
||
@if ($stornoFloorPoints !== null && $stornoFloorPoints > 0)
|
||
Nicht abgezogen: <strong>{{ \App\Services\Util::formatNumber($stornoFloorPoints) }} Punkte</strong>
|
||
aus negativen Monatssalden einzelner Berater (Stornos über die Buchungen des Monats hinaus) —
|
||
die Provisionsberechnung wertet einen negativen Saldo mit 0.
|
||
@endif
|
||
@endif
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@php
|
||
$newCustomerAbos = $statistics['new_customer_abos'] ?? null;
|
||
$leaderAbos = $statistics['leader_abos'] ?? null;
|
||
@endphp
|
||
|
||
<div class="row">
|
||
<div class="col-lg-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h5 class="mb-0">Neue Kundenabos</h5>
|
||
<div class="text-muted small">{{ $selectedMonthName }} {{ $selectedYear }} · nach Anzahl sortiert</div>
|
||
</div>
|
||
<span class="display-4 font-weight-bold text-primary mb-0">
|
||
{{ number_format((float) ($statistics['totals']['team_customer_abos_new'] ?? 0), 0, ',', '.') }}
|
||
</span>
|
||
</div>
|
||
@if ($newCustomerAbos === null)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
|
||
</p>
|
||
</div>
|
||
@elseif (count($newCustomerAbos) === 0)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">In diesem Monat wurden keine neuen Kundenabos abgeschlossen.</p>
|
||
</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Berater</th>
|
||
<th class="text-right">Gesamt</th>
|
||
<th class="text-right">Neu</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($newCustomerAbos as $row)
|
||
<tr>
|
||
<td>
|
||
<div class="font-weight-semibold">
|
||
{{ $row['name'] }}
|
||
@if (!empty($row['is_self']))
|
||
<span class="badge badge-pill badge-info ml-1">ich</span>
|
||
@endif
|
||
</div>
|
||
<div class="text-muted small">{{ !empty($row['is_self']) ? 'Eigene Kundenabos' : 'Linie '.$row['line'] }} · {{ $row['career_level'] }}</div>
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
<span class="badge badge-pill badge-outline-default backoffice-statistics-click-badge">
|
||
{{ number_format((float) ($row['total_abo_count'] ?? 0), 0, ',', '.') }}
|
||
</span>
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
<span class="badge badge-pill badge-success backoffice-statistics-click-badge">
|
||
+{{ number_format((float) $row['abo_count'], 0, ',', '.') }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h5 class="mb-0">Abos Führungskräfte</h5>
|
||
<div class="text-muted small">ab Stufe {{ $leaderAbos['level_name'] ?? 'Vertriebsleiter' }} · aktueller Stand</div>
|
||
</div>
|
||
<div class="text-right">
|
||
<span class="display-4 font-weight-bold text-primary mb-0">
|
||
{{ number_format((float) ($leaderAbos['total'] ?? 0), 0, ',', '.') }}
|
||
</span>
|
||
@if ($leaderAbos !== null)
|
||
<div class="text-muted small"
|
||
title="Jedes Abo zählt genau einmal: das Team einer Führungskraft reicht hier nur bis zur nächsten Führungskraft. Die Spalten in der Tabelle zeigen dagegen die komplette Downline und überschneiden sich — deshalb ist diese Summe kleiner.">
|
||
{{ number_format((float) ($leaderAbos['total_own'] ?? 0), 0, ',', '.') }} eigene ·
|
||
{{ number_format((float) ($leaderAbos['total_customer'] ?? 0), 0, ',', '.') }} Kunden ·
|
||
{{ number_format((float) ($leaderAbos['total_team'] ?? 0), 0, ',', '.') }} Team ·
|
||
{{ number_format((float) ($leaderAbos['total_team_customer'] ?? 0), 0, ',', '.') }} Teamkunden
|
||
<span class="ion ion-md-information-circle-outline"></span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@if ($leaderAbos === null)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
|
||
</p>
|
||
</div>
|
||
@elseif (count($leaderAbos['rows']) === 0)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Im Team gibt es aktuell keine Führungskräfte ab Stufe {{ $leaderAbos['level_name'] }}.
|
||
</p>
|
||
</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Führungskraft</th>
|
||
<th class="text-right">Eigene Abos</th>
|
||
<th class="text-right">Kundenabos</th>
|
||
<th class="text-right" title="Berater-Abos der kompletten Downline — Teams untergeordneter Führungskräfte zählen hier mit, die Spalte lässt sich deshalb nicht aufsummieren.">Teamabos (gesamt)</th>
|
||
<th class="text-right" title="Kundenabos der kompletten Downline — inklusive der Teams untergeordneter Führungskräfte.">Team-Kundenabos (gesamt)</th>
|
||
<th class="text-right" title="Eigenes Abo + Kundenabos + Teamabos (gesamt) + Team-Kundenabos (gesamt)">Gesamt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($leaderAbos['rows'] as $row)
|
||
@php
|
||
$leaderTotal = $row['total_abo_count_full']
|
||
?? (($row['own_abo_count'] ?? 0) + ($row['customer_abo_count'] ?? 0) + ($row['team_abo_count_total'] ?? ($row['team_abo_count'] ?? 0)) + ($row['team_customer_abo_count_total'] ?? ($row['team_customer_abo_count'] ?? 0)));
|
||
@endphp
|
||
<tr>
|
||
<td>
|
||
<div class="font-weight-semibold">{{ $row['name'] }}</div>
|
||
<div class="text-muted small">Linie {{ $row['line'] }} · {{ $row['career_level'] }}</div>
|
||
</td>
|
||
{{-- Die "direkt"-Spalten sind auf Kundenwunsch (07.09.2026) raus; die
|
||
Kopf-Summe rechnet intern weiter mit ihnen, weil nur sie
|
||
ueberschneidungsfrei ist (siehe Fussnote unter der Tabelle). --}}
|
||
@foreach (['own_abo_count', 'customer_abo_count', 'team_abo_count_total', 'team_customer_abo_count_total'] as $countKey)
|
||
<td class="text-right align-middle">
|
||
@if (($row[$countKey] ?? 0) > 0)
|
||
<span class="badge badge-pill badge-outline-default backoffice-statistics-click-badge">
|
||
{{ number_format((float) $row[$countKey], 0, ',', '.') }}
|
||
</span>
|
||
@else
|
||
<span class="badge badge-pill badge-light backoffice-statistics-empty-badge">0</span>
|
||
@endif
|
||
</td>
|
||
@endforeach
|
||
<td class="text-right align-middle font-weight-bold">
|
||
{{ number_format((float) $leaderTotal, 0, ',', '.') }}
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card-body pt-2 pb-3">
|
||
<p class="text-muted small mb-0">
|
||
<strong>gesamt</strong> = komplette Downline inklusive der Teams untergeordneter Führungskräfte — dieselben Abos
|
||
können dort bei mehreren Führungskräften zählen, die Spalten lassen sich deshalb nicht aufsummieren.
|
||
Die <strong>Summe oben rechts</strong> zählt jedes Abo genau einmal (Team jeweils nur bis zur nächsten
|
||
Führungskraft) und ist deshalb kleiner als die Spaltensummen.
|
||
</p>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@php
|
||
$recruiterRanking = $statistics['recruiter_ranking'] ?? null;
|
||
// Aeltere Snapshots kennen die Summenzeile noch nicht -> dann bleibt sie weg.
|
||
$recruiterTotals = $statistics['recruiter_totals'] ?? null;
|
||
@endphp
|
||
|
||
<div class="row">
|
||
<div class="col-lg-6 mb-4">
|
||
<div class="card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<h5 class="mb-0">Recruiter des Monats</h5>
|
||
<div class="text-muted small"
|
||
title="Neupartner wie in der Neupartner-Kennzahl (aktiver Account, im Monat aktiv geworden). Neues Volumen = Monats-Gesamtpunkte der Neupartner (Eigen- und Kundenpunkte, inkl. Starterpaket) plus Kettenvolumen: Punkte der Neupartner, die diese Neupartner im selben Monat selbst geworben haben.">
|
||
{{ $selectedMonthName }} {{ $selectedYear }} · nach Anzahl gesponserter Neupartner
|
||
<span class="ion ion-md-information-circle-outline"></span>
|
||
</div>
|
||
</div>
|
||
<span class="display-4 font-weight-bold text-primary mb-0">
|
||
{{ number_format((float) ($statistics['totals']['new_partners'] ?? 0), 0, ',', '.') }}
|
||
</span>
|
||
</div>
|
||
@if ($recruiterRanking === null)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
|
||
</p>
|
||
</div>
|
||
@elseif (count($recruiterRanking) === 0)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">In diesem Monat wurden keine Neupartner gesponsert.</p>
|
||
</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Sponsor</th>
|
||
<th class="text-right">Neupartner</th>
|
||
<th class="text-right" title="Monats-Gesamtpunkte der Neupartner inkl. Kettenvolumen">neues Volumen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($recruiterRanking as $row)
|
||
<tr data-toggle="collapse" data-target="#recruiter-partners-{{ $loop->index }}"
|
||
role="button" style="cursor: pointer;"
|
||
title="Klick: geworbene Neupartner anzeigen">
|
||
<td>
|
||
<div class="font-weight-semibold">
|
||
{{ $row['name'] }}
|
||
@if (!empty($row['is_self']))
|
||
<span class="badge badge-pill badge-info ml-1">ich</span>
|
||
@endif
|
||
</div>
|
||
<div class="text-muted small">{{ !empty($row['is_self']) ? 'Eigene Neupartner' : 'Linie '.$row['line'] }} · {{ $row['career_level'] }}</div>
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
<span class="badge badge-pill badge-success backoffice-statistics-click-badge">
|
||
+{{ number_format((float) $row['new_partner_count'], 0, ',', '.') }}
|
||
</span>
|
||
</td>
|
||
<td class="text-right align-middle font-weight-bold">
|
||
{{ \App\Services\Util::formatNumber($row['volume_points']) }} P.
|
||
</td>
|
||
</tr>
|
||
<tr class="collapse bg-light" id="recruiter-partners-{{ $loop->index }}">
|
||
<td colspan="3" class="py-2">
|
||
<ul class="list-unstyled small mb-0">
|
||
@foreach ($row['partners'] ?? [] as $partner)
|
||
<li class="{{ $loop->last ? 'mb-0' : 'mb-1' }}">
|
||
<span class="font-weight-semibold">{{ $partner['name'] }}</span>
|
||
<span class="text-muted">· {{ $partner['career_level'] }}</span>
|
||
· {{ \App\Services\Util::formatNumber($partner['total_points']) }} P.
|
||
@if (!empty($partner['children']))
|
||
<ul class="list-unstyled text-muted ml-3 mb-0">
|
||
@foreach ($partner['children'] as $child)
|
||
<li>
|
||
↳ {{ $child['name'] }}
|
||
· {{ $child['career_level'] }}
|
||
· {{ \App\Services\Util::formatNumber($child['total_points']) }} P.
|
||
</li>
|
||
@endforeach
|
||
</ul>
|
||
@endif
|
||
</li>
|
||
@endforeach
|
||
</ul>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
@if ($recruiterTotals !== null)
|
||
{{-- Summe ueber die deduplizierte Neupartner-Menge, NICHT ueber die
|
||
Spalte: dort steckt jeder Neupartner durch das Kettenvolumen
|
||
mehrfach drin (Kundenwunsch 2026-09-07). --}}
|
||
<tfoot>
|
||
<tr class="font-weight-bold">
|
||
<td>Summe</td>
|
||
<td class="text-right">{{ number_format((float) $recruiterTotals['new_partner_count'], 0, ',', '.') }}</td>
|
||
<td class="text-right">{{ \App\Services\Util::formatNumber($recruiterTotals['volume_points']) }}</td>
|
||
</tr>
|
||
</tfoot>
|
||
@endif
|
||
</table>
|
||
</div>
|
||
<div class="card-body pt-2 pb-3">
|
||
<p class="text-muted small mb-0">
|
||
<strong>neues Volumen</strong> = Monats-Gesamtpunkte (Eigen- und Kundenpunkte, inkl. Starterpaket)
|
||
der geworbenen Neupartner — plus rekursiv die Punkte der Neupartner, die diese im selben Monat
|
||
selbst geworben haben. Klick auf eine Zeile zeigt, wer geworben wurde.
|
||
@if ($recruiterTotals !== null)
|
||
<br><strong>Summe</strong> = Volumen aller Neupartner des Monats, jeder genau einmal gezählt.
|
||
Sie ist kleiner als die Spalte darüber, weil ein Neupartner dort zusätzlich im Kettenvolumen
|
||
seines Sponsors steckt.
|
||
@endif
|
||
</p>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@php
|
||
$topCustomerAbos = $statistics['top_customer_abos'] ?? null;
|
||
@endphp
|
||
|
||
<div class="row">
|
||
<div class="col-lg-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Top 10 Kundenabos (Anzahl)</h5>
|
||
<div class="text-muted small"
|
||
title="Zählt den heutigen Bestand aktiver Kundenabos. Die Incentive-Rangliste zählt anders: dort bleiben alle im Aktionszeitraum gestarteten Abos dauerhaft gezählt (inkl. Eigenabo und inzwischen beendeter Abos) — die Zahlen weichen deshalb bewusst voneinander ab.">
|
||
aktueller Bestand aktiver Kundenabos · weicht bewusst von der Incentive-Zählung ab
|
||
<span class="ion ion-md-information-circle-outline"></span>
|
||
</div>
|
||
</div>
|
||
@if ($topCustomerAbos === null)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
|
||
</p>
|
||
</div>
|
||
@elseif (count($topCustomerAbos['by_count']) === 0)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">Im Team gibt es aktuell keine aktiven Kundenabos.</p>
|
||
</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 4rem;">Platz</th>
|
||
<th>Berater</th>
|
||
<th class="text-right">Kundenabos</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($topCustomerAbos['by_count'] as $row)
|
||
<tr>
|
||
<td class="align-middle font-weight-bold">{{ $loop->iteration }}.</td>
|
||
<td>
|
||
<div class="font-weight-semibold">
|
||
{{ $row['name'] }}
|
||
@if (!empty($row['is_self']))
|
||
<span class="badge badge-pill badge-info ml-1">ich</span>
|
||
@endif
|
||
</div>
|
||
<div class="text-muted small">{{ !empty($row['is_self']) ? 'Eigene Kundenabos' : 'Linie '.$row['line'] }} · {{ $row['career_level'] }}</div>
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
<span class="badge badge-pill badge-outline-default backoffice-statistics-click-badge">
|
||
{{ number_format((float) $row['abo_count'], 0, ',', '.') }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">Top 10 Kundenabos (Umsatz)</h5>
|
||
<div class="text-muted small"
|
||
title="Abgerechnet = Kundenabo-Punkte, die im gewählten Monat tatsächlich angerechnet wurden (Hauptwert, Sortierung). Bestand/Lieferung = Punkte, die die heute aktiven Kundenabos pro Lieferung bringen — kann abweichen, wenn Abos im Monat dazukamen oder beendet wurden.">
|
||
sortiert nach abgerechneten Kundenabo-Punkten {{ $selectedMonthName }} {{ $selectedYear }}
|
||
<span class="ion ion-md-information-circle-outline"></span>
|
||
</div>
|
||
</div>
|
||
@if ($topCustomerAbos === null)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">
|
||
Für diesen Monat liegt noch ein älterer Snapshot ohne diese Liste vor.
|
||
</p>
|
||
</div>
|
||
@elseif (count($topCustomerAbos['by_points']) === 0)
|
||
<div class="card-body">
|
||
<p class="text-muted mb-0">Im Team gibt es aktuell keine Kundenabo-Umsätze.</p>
|
||
</div>
|
||
@else
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 4rem;">Platz</th>
|
||
<th>Berater</th>
|
||
<th class="text-right">Abos</th>
|
||
<th class="text-right" title="Im gewählten Monat abgerechnete Kundenabo-Punkte">Abgerechnet (Punkte)</th>
|
||
<th class="text-right" title="Davon aus den wiederkehrenden Abo-Positionen.">davon reine Abo-Punkte</th>
|
||
<th class="text-right" title="Davon aus Artikeln, die einmalig zur Abo-Lieferung dazubestellt wurden. Erst ab 06/2026 erfasst — davor steht alles unter „reine Abo-Punkte".">einmalig dazu</th>
|
||
<th class="text-right" title="Punkte der heute aktiven Kundenabos pro Lieferung">Bestand/Lieferung</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($topCustomerAbos['by_points'] as $row)
|
||
<tr>
|
||
<td class="align-middle font-weight-bold">{{ $loop->iteration }}.</td>
|
||
<td>
|
||
<div class="font-weight-semibold">
|
||
{{ $row['name'] }}
|
||
@if (!empty($row['is_self']))
|
||
<span class="badge badge-pill badge-info ml-1">ich</span>
|
||
@endif
|
||
</div>
|
||
<div class="text-muted small">{{ !empty($row['is_self']) ? 'Eigene Kundenabos' : 'Linie '.$row['line'] }} · {{ $row['career_level'] }}</div>
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
<span class="badge badge-pill badge-outline-default backoffice-statistics-click-badge">
|
||
{{ number_format((float) ($row['abo_count'] ?? 0), 0, ',', '.') }}
|
||
</span>
|
||
</td>
|
||
<td class="text-right align-middle font-weight-bold">
|
||
{{ \App\Services\Util::formatNumber($row['points_actual']) }}
|
||
</td>
|
||
{{-- Aufteilung erst ab 06/2026 erfasst; aeltere Snapshots haben die
|
||
Schluessel gar nicht -> dann bleiben die Zellen leer. --}}
|
||
<td class="text-right align-middle">
|
||
{{ isset($row['points_recurring']) ? \App\Services\Util::formatNumber($row['points_recurring']) : '–' }}
|
||
</td>
|
||
<td class="text-right align-middle {{ ($row['points_addon'] ?? 0) > 0 ? 'text-success' : 'text-muted' }}">
|
||
{{ isset($row['points_addon']) ? \App\Services\Util::formatNumber($row['points_addon']) : '–' }}
|
||
</td>
|
||
<td class="text-right align-middle">
|
||
{{ \App\Services\Util::formatNumber($row['points_target']) }}
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-body">
|
||
<h5 class="mb-2">Begriffe</h5>
|
||
<p class="text-muted mb-2">
|
||
Teamabos sind Berater-/Eigenabos im Team. Teamkundenabos sind Kundenabos, die einem Berater
|
||
aus der jeweiligen Linie zugeordnet sind.
|
||
</p>
|
||
<p class="text-muted mb-0">
|
||
Umsatz = Gesamtpunkte der Linie (Eigen- und Kundenpunkte zusammen). Ein Klick auf eine Zahl
|
||
öffnet die zugehörige Namensliste.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6 mb-4">
|
||
<div class="card h-100">
|
||
<div class="card-body">
|
||
<h5 class="mb-2">Nächster Ausbau</h5>
|
||
<p class="text-muted mb-0">
|
||
Die Detailansichten liefern bereits Namen und Basisdaten. Im nächsten Schritt werden Abo-Punkte
|
||
und Umsatzarten noch feiner nach Abo, Einzelbestellung und Shop getrennt.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.backoffice-kpi-card {
|
||
cursor: pointer;
|
||
transition: box-shadow .15s ease-in-out, transform .15s ease-in-out, border-color .15s ease-in-out;
|
||
border: 1px solid rgba(0, 0, 0, .08);
|
||
}
|
||
|
||
.backoffice-kpi-card:hover,
|
||
.backoffice-kpi-card:focus {
|
||
border-color: #4e73df;
|
||
box-shadow: 0 .25rem .75rem rgba(78, 115, 223, .25);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.backoffice-statistics-click-badge,
|
||
.backoffice-statistics-empty-badge {
|
||
display: inline-block;
|
||
font-size: .85rem;
|
||
line-height: 1.35;
|
||
min-width: 2.35rem;
|
||
padding: .35rem .55rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.backoffice-statistics-click-badge {
|
||
text-decoration: none;
|
||
}
|
||
|
||
.backoffice-statistics-click-badge:hover,
|
||
.backoffice-statistics-click-badge:focus {
|
||
text-decoration: none;
|
||
}
|
||
</style>
|
||
@endsection
|