mivita/resources/views/user/backoffice/statistics/details.blade.php
Kevin ac3bd6e8ea Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)
Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert

Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:56:02 +00:00

329 lines
22 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.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,
'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]) }}"
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>
</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">Sponsor <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">Kundenpunkte <i class="ion ion-ios-swap sort-icon"></i></span></th>
<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>
@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">{{ number_format($row['abo_count'], 0, ',', '.') }}</td>
<td class="text-right">
<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">{{ \App\Services\Util::formatNumber($row['points']) }}</td>
<td>
{!! $row['status_badge'] ?? '' !!}
@if (! empty($row['status_reason']))
<div class="small text-muted mt-1">{{ $row['status_reason'] }}</div>
@endif
</td>
<td>
{{ $row['start_date'] ?? '-' }}
@if (! empty($row['is_new_this_month']))
<span class="badge badge-success ml-1">Neu</span>
@endif
</td>
<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"><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>
@elseif ($details['metric'] === 'new_partners')
<td>{{ $row['sponsor'] ?? '-' }}</td>
<td class="text-right font-weight-bold">{{ \App\Services\Util::formatNumber($row['customer_points'] ?? 0) }}</td>
<td>{{ $row['active_date'] ?? '-' }}</td>
<td>{{ $row['payment_account'] ?? '-' }}</td>
@else
<td>{{ $row['active_date'] ?? '-' }}</td>
<td>
{{ $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 class="text-right">{{ \App\Services\Util::formatNumber($details['summary']['customer_points'] ?? 0) }}</td>
<td></td>
<td></td>
@else
<td></td>
<td></td>
@endif
</tr>
</tfoot>
</table>
</div>
@endif
</div>
</div>
@if ($details['rows'] !== [])
<style>
#backoffice-statistics-detail-table {
min-width: 980px;
}
#backoffice-statistics-detail-table th,
#backoffice-statistics-detail-table td {
vertical-align: middle;
}
#backoffice-statistics-detail-table th[data-sortable="true"] {
white-space: nowrap;
}
#backoffice-statistics-detail-table .sortable-label {
align-items: center;
display: inline-flex;
gap: .25rem;
white-space: nowrap;
width: 100%;
}
#backoffice-statistics-detail-table .sort-icon {
color: #adb5bd;
flex: 0 0 auto;
font-size: .875rem;
line-height: 1;
}
#backoffice-statistics-detail-table th[data-sort-direction="asc"] .sort-icon,
#backoffice-statistics-detail-table th[data-sort-direction="desc"] .sort-icon {
color: #4e73df;
}
</style>
<script>
$(document).ready(function() {
$('#backoffice-statistics-detail-search').on('keyup search', function() {
var search = $(this).val().toLowerCase();
$('#backoffice-statistics-detail-table tbody tr').each(function() {
var rowText = $(this).text().toLowerCase();
$(this).toggle(rowText.indexOf(search) !== -1);
});
});
$('#backoffice-statistics-detail-table th[data-sortable="true"]').css('cursor', 'pointer').on('click',
function() {
var $header = $(this);
var columnIndex = $header.index();
var sortType = $header.data('sort-type') || 'text';
var direction = $header.data('sort-direction') === 'asc' ? 'desc' : 'asc';
var rows = $('#backoffice-statistics-detail-table tbody tr').get();
rows.sort(function(a, b) {
var aValue = getSortValue($(a).children('td').eq(columnIndex).text(), sortType);
var bValue = getSortValue($(b).children('td').eq(columnIndex).text(), sortType);
if (aValue < bValue) {
return direction === 'asc' ? -1 : 1;
}
if (aValue > bValue) {
return direction === 'asc' ? 1 : -1;
}
return 0;
});
$('#backoffice-statistics-detail-table th[data-sortable="true"]')
.removeData('sort-direction')
.removeAttr('data-sort-direction');
$header.data('sort-direction', direction).attr('data-sort-direction', direction);
$.each(rows, function(index, row) {
$('#backoffice-statistics-detail-table tbody').append(row);
});
});
function getSortValue(value, sortType) {
value = $.trim(value);
if (sortType === 'number') {
return parseFloat(value.replace(/\./g, '').replace(',', '.').replace(/[^\d.-]/g, '')) || 0;
}
if (sortType === 'date') {
var parts = value.match(/(\d{2})\.(\d{2})\.(\d{4})/);
if (!parts) {
return 0;
}
return new Date(parts[3], parts[2] - 1, parts[1]).getTime();
}
return value.toLowerCase();
}
});
</script>
@endif
@endsection