mivita/resources/views/user/backoffice/statistics/index.blade.php
Kevin 44637d06a7 Team-Statistik: Neu-Badge-Link kannte $selectedTree nicht (ViewException auf Produktion)
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>
2026-09-14 15:40:22 +00:00

809 lines
48 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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.&nbsp;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