mivita/app/Http/Controllers/User/BackofficeStatisticsController.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

341 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\Backoffice\BackofficeDashboardService;
use App\Services\Backoffice\BackofficeDrilldownService;
use App\Services\Backoffice\PersonalStatisticsService;
use App\Services\BusinessPlan\LiveCommissionService;
use App\Services\HTMLHelper;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class BackofficeStatisticsController extends Controller
{
private const SESSION_MONTH_KEY = 'backoffice_statistics_month';
private const SESSION_YEAR_KEY = 'backoffice_statistics_year';
public function __construct(
private BackofficeDashboardService $dashboardService,
private BackofficeDrilldownService $drilldownService,
private PersonalStatisticsService $personalService
) {
$this->middleware('active.account');
}
public function index(Request $request): View
{
if (! $request->user()?->isVIP()) {
abort(404);
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$startTime = microtime(true);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
$performance = [
'duration_ms' => round((microtime(true) - $startTime) * 1000, 2),
'source_label' => $statistics['_meta']['source_label'] ?? 'Live',
'calculated_at' => $statistics['_meta']['calculated_at'] ?? null,
];
return view('user.backoffice.statistics.index', [
'selectedMonth' => $selectedMonth,
'selectedYear' => $selectedYear,
'filterMonths' => HTMLHelper::getTransMonths(),
'filterYears' => HTMLHelper::getYearRange(2022),
'statistics' => $statistics,
'performance' => $performance,
'commission' => app(LiveCommissionService::class)->commission($request->user(), $selectedMonth, $selectedYear),
]);
}
public function my(Request $request): View
{
if (! $request->user()?->isVIP()) {
abort(404);
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
return view('user.backoffice.statistics.my', [
'selectedMonth' => $selectedMonth,
'selectedYear' => $selectedYear,
'filterMonths' => HTMLHelper::getTransMonths(),
'filterYears' => HTMLHelper::getYearRange(2022),
'statistics' => $this->personalService->monthly($request->user(), $selectedMonth, $selectedYear),
]);
}
public function details(Request $request): View
{
if (! $request->user()?->isVIP()) {
abort(404);
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$line = (int) $request->get('line', 1);
$metric = (string) $request->get('metric', 'consultants');
$onlyNew = $request->boolean('only_new');
return view('user.backoffice.statistics.details', [
'selectedMonth' => $selectedMonth,
'selectedYear' => $selectedYear,
'details' => $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew),
]);
}
public function export(Request $request): StreamedResponse
{
if (! $request->user()?->isVIP()) {
abort(404);
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$line = (int) $request->get('line', 1);
$metric = (string) $request->get('metric', 'consultants');
$onlyNew = $request->boolean('only_new');
$details = $this->drilldownService->details($request->user(), $line, $metric, $selectedMonth, $selectedYear, $onlyNew);
$filename = sprintf(
'backoffice-statistik-%s%s-linie-%s-%02d-%d.csv',
$metric,
$details['only_new'] ? '-neu' : '',
$line === 0 ? 'alle' : $line,
$selectedMonth,
$selectedYear
);
return response()->streamDownload(function () use ($details): void {
$output = fopen('php://output', 'w');
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, $this->csvHeaders($details['metric']), ';');
foreach ($details['rows'] as $row) {
fputcsv($output, $this->csvRow($details['metric'], $row), ';');
}
fputcsv($output, []);
fputcsv($output, $this->csvSummaryRow($details), ';');
fclose($output);
}, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function overviewExport(Request $request): StreamedResponse
{
if (! $request->user()?->isVIP()) {
abort(404);
}
[$selectedMonth, $selectedYear] = $this->selectedPeriod($request);
$statistics = $this->dashboardService->overview($request->user(), $selectedMonth, $selectedYear);
$filename = sprintf('backoffice-statistik-uebersicht-%02d-%d.csv', $selectedMonth, $selectedYear);
return response()->streamDownload(function () use ($statistics): void {
$output = fopen('php://output', 'w');
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, $this->overviewCsvHeaders(), ';');
foreach ($statistics['lines'] as $line) {
fputcsv($output, $this->overviewCsvRow($line), ';');
}
fputcsv($output, []);
fputcsv($output, $this->overviewCsvRow($statistics['totals']), ';');
fclose($output);
}, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* @return array{0: int, 1: int}
*/
private function selectedPeriod(Request $request): array
{
$selectedMonth = max(1, min(12, (int) $request->get('month', session(self::SESSION_MONTH_KEY, now()->month))));
$selectedYear = max(2020, min((int) now()->year + 1, (int) $request->get('year', session(self::SESSION_YEAR_KEY, now()->year))));
session([
self::SESSION_MONTH_KEY => $selectedMonth,
self::SESSION_YEAR_KEY => $selectedYear,
]);
return [$selectedMonth, $selectedYear];
}
/**
* @return string[]
*/
private function overviewCsvHeaders(): array
{
return [
'Linie',
'Berater',
'Neupartner',
'Teamabos',
'Neue Teamabos',
'Teamkundenabos',
'Neue Teamkundenabos',
'Eigenpunkte',
'Externe Punkte',
'Kundenabo-Punkte',
'Einzelbestellungs-Punkte',
'Sonstige Kundenpunkte',
'Gesamtpunkte',
'1000 Punkte Shop',
'Umsatz Netto',
];
}
/**
* @param array<string, mixed> $row
* @return array<int, mixed>
*/
private function overviewCsvRow(array $row): array
{
return [
$row['label'] ?? '',
$row['consultants'] ?? 0,
$row['new_partners'] ?? 0,
$row['team_partner_abos'] ?? 0,
$row['team_partner_abos_new'] ?? 0,
$row['team_customer_abos'] ?? 0,
$row['team_customer_abos_new'] ?? 0,
$row['own_points'] ?? 0,
$row['external_points'] ?? 0,
$row['customer_abo_points'] ?? 0,
$row['customer_single_order_points'] ?? 0,
$row['customer_other_points'] ?? 0,
$row['total_points'] ?? 0,
$row['shop_1000'] ?? 0,
$row['turnover_net'] ?? 0,
];
}
/**
* @return string[]
*/
private function csvHeaders(string $metric): array
{
if ($metric === 'team_customer_abos') {
return ['Teampartner', 'E-Mail', 'Karriere-Level', 'Anzahl Kundenabos', 'Volumenpunkte gesamt'];
}
if ($metric === 'team_partner_abos') {
return ['Name', 'E-Mail', 'Karriere-Level', 'Berater', 'Abo-Punkte', 'Status', 'Status-Grund', 'Besteht seit', 'Naechste Ausfuehrung', 'Lieferungen'];
}
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
return ['Name', 'E-Mail', 'Karriere-Level', 'Eigenpunkte', 'Shop-Punkte', 'Abo-Punkte', 'Sonstige Punkte', 'Kundenpunkte', 'Gesamtpunkte'];
}
if ($metric === 'new_partners') {
return ['Name', 'E-Mail', 'Karriere-Level', 'Sponsor', 'Kundenpunkte', 'Aktiv seit', 'Account gueltig bis'];
}
return ['Name', 'E-Mail', 'Karriere-Level', 'Aktiv seit', 'Account gueltig bis', 'Account Status'];
}
/**
* @param array<string, mixed> $row
* @return array<int, mixed>
*/
private function csvRow(string $metric, array $row): array
{
if ($metric === 'team_customer_abos') {
return [
$row['name'] ?? '',
$row['email'] ?? '',
$row['career_level'] ?? '',
$row['abo_count'] ?? 0,
$row['points'] ?? 0,
];
}
if ($metric === 'team_partner_abos') {
return [
$row['name'] ?? '',
$row['email'] ?? '',
$row['career_level'] ?? '',
$row['consultant_name'] ?? $row['name'] ?? '',
$row['points'] ?? 0,
$row['status_label'] ?? '',
$row['status_reason'] ?? '',
$row['start_date'] ?? '',
$row['next_date'] ?? '',
$row['deliveries'] ?? 0,
];
}
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
return [
$row['name'] ?? '',
$row['email'] ?? '',
$row['career_level'] ?? '',
$row['own_points'] ?? 0,
$row['customer_single_order_points'] ?? 0,
$row['customer_abo_points'] ?? 0,
$row['customer_other_points'] ?? 0,
$row['external_points'] ?? 0,
$row['total_points'] ?? 0,
];
}
if ($metric === 'new_partners') {
return [
$row['name'] ?? '',
$row['email'] ?? '',
$row['career_level'] ?? '',
$row['sponsor'] ?? '',
$row['customer_points'] ?? 0,
$row['active_date'] ?? '',
$row['payment_account'] ?? '',
];
}
return [
$row['name'] ?? '',
$row['email'] ?? '',
$row['career_level'] ?? '',
$row['active_date'] ?? '',
$row['payment_account'] ?? '',
$row['account_status'] ?? '',
];
}
/**
* @param array<string, mixed> $details
* @return array<int, mixed>
*/
private function csvSummaryRow(array $details): array
{
$metric = $details['metric'];
$summary = $details['summary'];
if ($metric === 'team_customer_abos') {
return ['Summe', $summary['count'].' Teampartner', '', $summary['abo_count'], $summary['points']];
}
if ($metric === 'team_partner_abos') {
return ['Summe', $summary['count'].' Eintraege', '', '', $summary['points'], '', '', '', '', $summary['deliveries']];
}
if (in_array($metric, ['own_points', 'external_points', 'customer_abo_points', 'customer_single_order_points', 'customer_other_points', 'total_points', 'shop_1000'], true)) {
return ['Summe', $summary['count'].' Eintraege', '', $summary['own_points'], $summary['customer_single_order_points'], $summary['customer_abo_points'], $summary['customer_other_points'], $summary['external_points'], $summary['total_points']];
}
if ($metric === 'new_partners') {
return ['Summe', $summary['count'].' Eintraege', '', '', $summary['customer_points'], '', '', ''];
}
return ['Summe', $summary['count'].' Eintraege', '', '', '', ''];
}
}