mivita/resources/views/user/backoffice/statistics/details.blade.php
Kevin b7e8bbd951 Statistik: Neupartner zaehlen im Monat der Zahlung, nicht der Registrierung
Wer Ende August registriert und am 1. September bezahlt hat, tauchte in keiner
Neupartner-Zaehlung auf: Der August-Snapshot wurde um 04:45 eingefroren, da
fehlte noch das Abrechnungskonto; im September lag active_date im falschen
Monat. Betroffen sind 2026 acht Partner, auch im Recruiting ihrer Sponsoren.

Kundenentscheidung 16.09.2026: der Zeitpunkt der Zahlung ist massgeblich.

Neue Spalte users.partner_paid_at, gesetzt bei der ersten Kontozahlung
(Payment::paymentStatusPaidAction) und bei manueller Freischaltung durch den
Admin — nur, solange sie leer ist; Verlaengerung und Wiederherstellung aendern
sie nicht. Bestehende Partner werden per Migration aus der ersten bezahlten
Registrierungsbestellung rueckgefuellt (Zeitstempel der Punktebuchung, ersatzweise
Bestelldatum, ohne Bestellung das Aktivierungsdatum). Bewusst nicht
payment_account minus ein Jahr: das ergaebe bei Verlaengerern den Jahrestag der
Erstzahlung.

User::scopeNewPartnerIn() und isNewPartnerIn() lesen jetzt partner_paid_at —
damit Kachel, Recruiter-Ranking, Top 3 und Neupartner-Liste gleichermassen.
Die Liste und der CSV-Export zeigen "Bezahlt am" neben "Registriert am".

Unveraendert: die Teamstruktur haengt weiter an active_date (auch im
Provisionsbaum). Wer ist neu dazugekommen und wer gehoert zur Linie sind zwei
verschiedene Fragen.

Die eingefrorenen Monate Januar bis August enthalten sieben dieser Partner
nicht; ob sie per --force neu gebaut werden, ist eine Entscheidung — siehe
dev/2026-07-22/neupartner-zahlungsmonat.md.

Mit drin: die handgebaute SQLite-Tabelle in BackofficeDashboardServiceTest
kannte status_reason aus d6efe56 noch nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:48:01 +00:00

257 lines
22 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')
@php
// Baum-Definition der aufrufenden Uebersicht; Fallback fuer direkte Aufrufe ohne Controller-Variable.
$selectedTree = $selectedTree ?? ($details['tree'] ?? \App\Services\Backoffice\BackofficeDashboardService::TREE_COMMISSION);
@endphp
<h4 class="font-weight-bold py-2 mb-2 d-flex justify-content-between align-items-center w-100">
<div>
{{ __('navigation.statistics') }} / {{ $details['metric_label'] }}
<span class="badge badge-secondary ml-2">VIP</span>
@if (! empty($details['only_new']))
<span class="badge badge-success ml-1">Nur neue im {{ $details['month'] }}/{{ $details['year'] }}</span>
@endif
</div>
<div>
<a href="{{ route('user_backoffice_statistics_export', [
'line' => $details['line'],
'metric' => $details['metric'],
'month' => $selectedMonth,
'year' => $selectedYear,
'tree' => $selectedTree,
'only_new' => ($details['only_new'] ?? false) ? 1 : 0,
]) }}"
class="btn btn-sm btn-outline-secondary mr-2">
<span class="ion ion-md-download mr-1"></span> CSV
</a>
<a href="{{ route('user_backoffice_statistics', ['month' => $selectedMonth, 'year' => $selectedYear, 'tree' => $selectedTree]) }}"
class="btn btn-sm btn-default">
{{ __('back') }}
</a>
</div>
</h4>
<div class="card mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-2">
<div class="text-muted small">Linie</div>
<strong>{{ $details['line_label'] }}</strong>
</div>
<div class="col-md-3 mb-2">
<div class="text-muted small">Zeitraum</div>
<strong>{{ $details['month'] }}/{{ $details['year'] }}</strong>
</div>
<div class="col-md-3 mb-2">
<div class="text-muted small">Kennzahl</div>
<strong>{{ $details['metric_label'] }}</strong>
</div>
<div class="col-md-3 mb-2">
<div class="text-muted small">Treffer</div>
<strong>{{ number_format($details['summary']['count'], 0, ',', '.') }}</strong>
</div>
<div class="col-md-12">
<div class="text-muted small">Baum-Definition</div>
<span class="badge badge-primary">{{ $details['tree_label'] ?? '' }}</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-0">
<div class="alert alert-warning rounded-0 border-left-0 border-right-0 border-top-0 mb-0">
<strong>Datenschutz-Hinweis:</strong>
Die Anzeige personenbezogener Detaildaten befindet sich noch in rechtlicher Klärung und ist aktuell nur für berechtigte VIP-Auswertungen vorgesehen.
</div>
@if ($details['rows'] === [])
<div class="p-4">
<p class="text-muted mb-0">{{ __('tables.no_data_available') }}</p>
</div>
@else
<div class="p-3 border-bottom">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<span class="ion ion-ios-search"></span>
</span>
</div>
<input type="search" id="backoffice-statistics-detail-search" class="form-control"
placeholder="Detailtabelle durchsuchen...">
</div>
<small class="text-muted d-block mt-2">
Suche nach Name, E-Mail, Status, Punkten oder Datum. Die Summenzeile bleibt unverändert.
</small>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" id="backoffice-statistics-detail-table">
<thead>
<tr>
<th data-sortable="true"><span class="sortable-label">Name <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true"><span class="sortable-label">E-Mail <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true"><span class="sortable-label">Karriere-Level <i class="ion ion-ios-swap sort-icon"></i></span></th>
@if ($details['metric'] === 'team_customer_abos')
<th class="text-right" data-sortable="true" data-sort-type="number"><span class="sortable-label justify-content-end">Kundenabos <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">Volumenpunkte gesamt <i class="ion ion-ios-swap sort-icon"></i></span></th>
@elseif ($details['metric'] === 'team_partner_abos')
<th data-sortable="true"><span class="sortable-label">Berater <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 data-sortable="true"><span class="sortable-label">Status <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Besteht seit <i class="ion ion-ios-swap sort-icon"></i></span></th>
<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" 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>
@elseif ($details['metric'] === 'new_partners')
<th data-sortable="true"><span class="sortable-label">Telefon <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true"><span class="sortable-label">Sponsor <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number" title="Punkte aus den Bestellungen SEINER Kunden (Shop und Abo) — ohne seine eigenen Bestellungen."><span class="sortable-label justify-content-end">Punkte seiner Kunden <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number" title="Punkte seiner Kunden PLUS seine eigenen Bestellungen — hier steckt vor allem das Starterpaket drin."><span class="sortable-label justify-content-end">Gesamt (inkl. Eigenbestellung) <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="date" title="Zeitpunkt der ersten Kontozahlung — er entscheidet, in welchem Monat der Partner als neu zählt."><span class="sortable-label">Bezahlt am <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="date" title="Abschluss der Registrierung — kann vor dem Zahlungsmonat liegen."><span class="sortable-label">Registriert am <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Account gültig bis <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>
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">Account gültig bis <i class="ion ion-ios-swap sort-icon"></i></span></th>
@endif
</tr>
</thead>
<tbody>
@foreach ($details['rows'] as $row)
<tr
class="{{ isset($row['is_account_active']) && ! $row['is_account_active'] ? 'table-danger' : '' }} {{ ! empty($row['is_new_this_month']) ? 'table-success' : '' }}">
<td class="font-weight-semibold">{{ $row['name'] }}</td>
<td>{{ $row['email'] ?? '-' }}</td>
<td>{{ $row['career_level'] ?? '-' }}</td>
@if ($details['metric'] === 'team_customer_abos')
<td class="text-right" data-sort-value="{{ $row['abo_count'] }}">{{ number_format($row['abo_count'], 0, ',', '.') }}</td>
<td class="text-right" data-sort-value="{{ $row['points'] }}">
<span class="font-weight-bold">{{ \App\Services\Util::formatNumber($row['points']) }}</span>
@if (! empty($row['abos']))
<a class="d-block small" data-toggle="collapse"
href="#customer-abos-{{ $loop->index }}" role="button">
Abos anzeigen
</a>
<div class="collapse text-left" id="customer-abos-{{ $loop->index }}">
<ul class="list-unstyled small text-muted mt-2 mb-0">
@foreach ($row['abos'] as $aboItem)
<li>
{{ $aboItem['start_date'] ?? '-' }} →
{{ $aboItem['next_date'] ?? '-' }}:
<span class="font-weight-semibold">{{ \App\Services\Util::formatNumber($aboItem['points']) }}</span> P.
@if (! empty($aboItem['is_new_this_month']))
<span class="badge badge-success ml-1">Neu</span>
@endif
</li>
@endforeach
</ul>
</div>
@endif
</td>
@elseif ($details['metric'] === 'team_partner_abos')
<td>{{ $row['consultant_name'] ?? $row['name'] }}</td>
<td class="text-right" data-sort-value="{{ $row['points'] }}">{{ \App\Services\Util::formatNumber($row['points']) }}</td>
<td data-sort-value="{{ $row['status_label'] ?? '' }}">
{!! $row['status_badge'] ?? '' !!}
@if (! empty($row['status_reason']))
<div class="small text-muted mt-1">{{ $row['status_reason'] }}</div>
@endif
</td>
<td data-sort-value="{{ $row['start_date_sort'] ?? '' }}">
{{ $row['start_date'] ?? '-' }}
@if (! empty($row['is_new_this_month']))
<span class="badge badge-success ml-1">Neu</span>
@endif
</td>
<td data-sort-value="{{ $row['next_date_sort'] ?? '' }}">{{ $row['next_date'] ?? '-' }}</td>
<td class="text-right" data-sort-value="{{ $row['deliveries'] }}">{{ 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" data-sort-value="{{ $row['own_points'] }}"><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" data-sort-value="{{ $row['customer_single_order_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_single_order_points']) }}</td>
<td class="text-right" data-sort-value="{{ $row['customer_abo_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_abo_points']) }}</td>
<td class="text-right" data-sort-value="{{ $row['customer_other_points'] }}">{{ \App\Services\Util::formatNumber($row['customer_other_points']) }}</td>
<td class="text-right font-weight-bold" data-sort-value="{{ $row['external_points'] }}"><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" data-sort-value="{{ $row['total_points'] }}">
{{ \App\Services\Util::formatNumber($row['total_points']) }}
@if (! empty($row['storno_floor_points']))
<div class="text-muted small font-weight-normal" title="Negativer Monatssaldo — die Provisionsberechnung wertet ihn mit 0.">Saldo −{{ \App\Services\Util::formatNumber($row['storno_floor_points']) }} → 0</div>
@endif
</td>
@elseif ($details['metric'] === 'new_partners')
<td>{{ ($row['phone'] ?? '') !== '' ? $row['phone'] : '-' }}</td>
<td>{{ $row['sponsor'] ?? '-' }}</td>
<td class="text-right font-weight-bold" data-sort-value="{{ $row['customer_points'] ?? 0 }}">{{ \App\Services\Util::formatNumber($row['customer_points'] ?? 0) }}</td>
<td class="text-right font-weight-bold" data-sort-value="{{ $row['total_points'] ?? 0 }}">{{ \App\Services\Util::formatNumber($row['total_points'] ?? 0) }}</td>
<td data-sort-value="{{ $row['paid_at_sort'] ?? '' }}" class="font-weight-semibold">{{ $row['paid_at'] ?? '-' }}</td>
<td data-sort-value="{{ $row['active_date_sort'] ?? '' }}">{{ $row['active_date'] ?? '-' }}</td>
<td data-sort-value="{{ $row['payment_account_sort'] ?? '' }}">{{ $row['payment_account'] ?? '-' }}</td>
@else
<td data-sort-value="{{ $row['active_date_sort'] ?? '' }}">{{ $row['active_date'] ?? '-' }}</td>
<td data-sort-value="{{ $row['payment_account_sort'] ?? '' }}">
{{ $row['payment_account'] ?? '-' }}
@if (isset($row['is_account_active']))
<span
class="badge badge-{{ $row['is_account_active'] ? 'success' : 'danger' }} ml-1">
{{ $row['account_status'] }}
</span>
@endif
</td>
@endif
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="font-weight-bold bg-light">
<td>Summe</td>
<td class="text-muted">{{ number_format($details['summary']['count'], 0, ',', '.') }} Einträge</td>
<td></td>
@if ($details['metric'] === 'team_customer_abos')
<td class="text-right">{{ number_format($details['summary']['abo_count'], 0, ',', '.') }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['points']) }}</td>
@elseif ($details['metric'] === 'team_partner_abos')
<td></td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['points']) }}</td>
<td></td>
<td></td>
<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" 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>
@elseif ($details['metric'] === 'new_partners')
<td></td>
<td></td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_points'] ?? 0) }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['total_points'] ?? 0) }}</td>
<td></td>
<td></td>
<td></td>
@else
<td></td>
<td></td>
@endif
</tr>
</tfoot>
</table>
</div>
@endif
</div>
</div>
@if ($details['rows'] !== [])
@include('components._sortable_table', [
'tableId' => 'backoffice-statistics-detail-table',
'searchId' => 'backoffice-statistics-detail-search',
])
@endif
@endsection