Statistik: abgeschlossene Monate werden vollstaendig eingefroren
Am ersten des Folgemonats wird der Monat abgerechnet, die Provision berechnet
und Gutschriften werden ausgestellt. Deswegen fliessen auch Stornos nicht mehr
rueckwirkend ein. Genau dieser Stand muss eingefroren werden — und zwar
einschliesslich Teamstruktur und Detaillisten, nicht nur der Kennzahlen.
Bisher hat `overlayCurrentAboStock()` beim Lesen eines gespeicherten Monats die
gesamte Kundenabo-Topliste live neu gerechnet, also auch abgerechnete
Monatswerte. Und die Detaillisten hinter den Kacheln lasen Monatsbuchungen und
Teamzuordnung grundsaetzlich neu — nach einem Sponsorwechsel oder einer
Loeschung konnte derselbe historische Klick andere Ergebnisse liefern als die
Kachel darueber.
Das Overlay entfaellt; der Snapshot wird unveraendert zurueckgegeben. Die
Detaillisten lesen die eingefrorene Mitgliedermenge aus dem Snapshot
(`user_ids` lag dort bereits, wurde nur nie benutzt) und die eingefrorenen
Abo-Ids aus dem neuen Feld `abo_ids`. Payload-Version 2.
Grenze, die bleibt: Ein Rebuild vergangener Monate friert die Struktur des
AUSFUEHRUNGSTAGS ein, nicht die des damaligen Monatsendes — frueheres
Sponsoring wird nirgends historisiert. Der `--force`-Lauf ist deshalb nur
richtig, wenn man bewusst einen einheitlichen Stand herstellt. Das ist hier der
Fall (Schemawechsel des Payloads), fuer den laufenden Betrieb genuegt der
naechtliche Lauf ohne `--force`.
Loest die August-Entscheidung F7 ("Abo-Bestaende tagesaktuell") ab. Zwei Tests
hielten die alte Regel fest und beschreiben jetzt die neue.
Mit drin, weil dieselben Dateien betroffen sind:
- Negative Monatssalden verschwanden aus dem Drilldown (`HAVING > 0`), obwohl
ihre Betraege in der Uebersicht steckten. Nur die 1000-Punkte-Liste behaelt
ihren Schwellenfilter (A03).
- Zusatzpunkte einer stornierten Bestellung werden anteilig verrechnet statt
voll abgezogen (A06).
- Unformatierte Sortierwerte fuer die Detailliste (A14, siehe Folgecommit).
Audit-Befunde A03, A04, A06, A12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1900860c74
commit
92344f7080
9 changed files with 1101 additions and 158 deletions
|
|
@ -26,8 +26,8 @@
|
|||
@if (!empty($performance['calculated_at']))
|
||||
<span class="ml-2">Snapshot vom {{ $performance['calculated_at'] }}</span>
|
||||
@endif
|
||||
@if (!empty($statistics['abo_stock_live']))
|
||||
<span class="badge badge-success ml-2" title="Abo-Bestandszahlen (Teamabos, Teamkundenabos, Führungskräfte, Top 10) zeigen den heutigen Stand; Punkte und Umsätze bleiben auf dem Stand des gewählten Monats.">Abo-Bestände tagesaktuell</span>
|
||||
@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
|
||||
|
|
@ -205,7 +205,7 @@
|
|||
<td class="font-weight-semibold">{{ $line['label'] }}</td>
|
||||
@foreach ($clickableMetrics as $metric)
|
||||
<td class="text-right">
|
||||
@if ($line[$metric] > 0)
|
||||
@if ($line[$metric] != 0)
|
||||
<a href="{{ route('user_backoffice_statistics_details', [
|
||||
'line' => $line['line'],
|
||||
'metric' => $metric,
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
{{ $formatValue($line[$metric], $metric) }}
|
||||
</a>
|
||||
@php $newMetric = $newAboMetric($line, $metric); @endphp
|
||||
@if ($newMetric && $line[$newMetric] > 0)
|
||||
@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>
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
<td>{{ $statistics['totals']['label'] }}</td>
|
||||
@foreach ($clickableMetrics as $metric)
|
||||
<td class="text-right">
|
||||
@if ($statistics['totals'][$metric] > 0)
|
||||
@if ($statistics['totals'][$metric] != 0)
|
||||
<a href="{{ route('user_backoffice_statistics_details', [
|
||||
'line' => 0,
|
||||
'metric' => $metric,
|
||||
|
|
@ -246,7 +246,7 @@
|
|||
{{ $formatValue($statistics['totals'][$metric], $metric) }}
|
||||
</a>
|
||||
@php $newMetric = $newAboMetric($statistics['totals'], $metric); @endphp
|
||||
@if ($newMetric && $statistics['totals'][$newMetric] > 0)
|
||||
@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>
|
||||
|
|
@ -354,11 +354,13 @@
|
|||
{{ number_format((float) ($leaderAbos['total'] ?? 0), 0, ',', '.') }}
|
||||
</span>
|
||||
@if ($leaderAbos !== null)
|
||||
<div class="text-muted small">
|
||||
<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>
|
||||
|
|
@ -383,8 +385,6 @@
|
|||
<th>Führungskraft</th>
|
||||
<th class="text-right">Eigene Abos</th>
|
||||
<th class="text-right">Kundenabos</th>
|
||||
<th class="text-right" title="Berater-Abos im eigenen Team bis zur nächsten Führungskraft — diese Spalten überschneiden sich nicht und ergeben die Kopf-Summe.">Teamabos (direkt)</th>
|
||||
<th class="text-right" title="Kundenabos im eigenen Team bis zur nächsten Führungskraft.">Team-Kundenabos (direkt)</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>
|
||||
|
|
@ -401,7 +401,10 @@
|
|||
<div class="font-weight-semibold">{{ $row['name'] }}</div>
|
||||
<div class="text-muted small">Linie {{ $row['line'] }} · {{ $row['career_level'] }}</div>
|
||||
</td>
|
||||
@foreach (['own_abo_count', 'customer_abo_count', 'team_abo_count', 'team_customer_abo_count', 'team_abo_count_total', 'team_customer_abo_count_total'] as $countKey)
|
||||
{{-- 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">
|
||||
|
|
@ -422,9 +425,10 @@
|
|||
</div>
|
||||
<div class="card-body pt-2 pb-3">
|
||||
<p class="text-muted small mb-0">
|
||||
<strong>direkt</strong> = Team bis zur nächsten Führungskraft (überschneidungsfrei, ergibt die Kopf-Summe).
|
||||
<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 Gesamt-Spalte lässt sich deshalb nicht aufsummieren.
|
||||
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
|
||||
|
|
@ -434,6 +438,8 @@
|
|||
|
||||
@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">
|
||||
|
|
@ -521,6 +527,18 @@
|
|||
</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">
|
||||
|
|
@ -528,6 +546,11 @@
|
|||
<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
|
||||
|
|
@ -625,6 +648,8 @@
|
|||
<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>
|
||||
|
|
@ -649,6 +674,14 @@
|
|||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue