Kundenabo-Seite: Vorschau taggenau, Liste mit den tatsaechlich gebuchten Punkten

Vorschau und Liste liessen sich nicht gegeneinander nachrechnen — zwei Ursachen,
beide hier behoben.

1. Die Vorschau nahm eine pauschale Monatssumme als Basis. Damit enthielt die
   Kachel zum 5. auch schon die Lieferungen vom 10. Jetzt zaehlt je Termin, was
   bis zu diesem Tag gebucht war, plus die bis dahin offenen Lieferungen.
   Buchungen nach dem letzten Liefertag (Nachzuegler, manuelle Gutschriften)
   haengen sich an die letzte Kachel, damit die den Monatsstand zeigt. Jede
   Kachel weist ihre Aufschluesselung aus ("457 abgerechnet + 131 offen").

   Beraterin 454 im September: 132 / 457 / 588 / 776 statt 0 / 0 / 131 / 319.

2. Die Liste zeigte nur den wiederkehrenden Abo-Inhalt. Einmal-Artikel und
   sonstige Zubuchungen einer Lieferung werden nach der Ausfuehrung aus dem Abo
   entfernt und tauchten deshalb nirgends mehr auf — bei Abo 266 waren das 43
   Punkte, bei Abo 418 rund 39. Neue Spalte "Diesen Monat" mit den je Abo
   tatsaechlich gebuchten Punkten, bei Abweichung mit dem Zusatz "+43 einmalig",
   dazu eine Summenzeile. Ein nicht zuordenbarer Rest (geloeschtes Abo, Buchung
   ohne Ausfuehrungsbezug) wird gesondert ausgewiesen, damit die Addition aufgeht.

   Die Spalte erscheint nur ohne Monatsfilter; fuer einen vergangenen Monat waere
   sie irrefuehrend.

Summe der Monatsspalte = Basis der letzten Kachel; Summe der Abo-Punkte der Abos
mit Termin 20./25. = deren offener Teil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-11 08:19:35 +00:00
parent cb5664a483
commit 685f7a90b9
11 changed files with 458 additions and 59 deletions

View file

@ -3,7 +3,18 @@
nach ihrer Vorgabe: Kunde, Liefertag, Abo-Punkte, nächste Ausführung,
Startdatum, Ausführungen.
Erwartet $listAbos (Collection<UserAbo>) und $customerAboForecast|null. --}}
Erwartet $listAbos (Collection<UserAbo>), $customerAboForecast|null sowie
$customerAboBilled|null (user_abo_id => im Monat abgerechnete Punkte) und
$customerAboBilledTotal|null. Die beiden letzten sind null, sobald auf einen
anderen Monat gefiltert wird — dann waere eine Monatsspalte irrefuehrend. --}}
@php
// Defensiv: die Teilansicht wird auch aus Kontexten eingebunden, die keine
// Monatswerte kennen (gefilterter Monat, Tests).
$customerAboForecast = $customerAboForecast ?? null;
$customerAboBilled = $customerAboBilled ?? null;
$customerAboBilledTotal = $customerAboBilledTotal ?? null;
@endphp
@if (! empty($customerAboForecast['days']))
<div class="card mb-4">
<div class="card-body">
@ -25,6 +36,16 @@
<div class="small {{ $forecastDay['upcoming'] > 0 ? 'text-success' : 'text-muted' }}">
{{ $forecastDay['is_past'] && $forecastDay['upcoming'] <= 0 ? __('home.forecast_done') : __('navigation.points') }}
</div>
@if ($forecastDay['upcoming'] != 0.0)
{{-- Aufschluesselung, damit die Kachel gegen die Liste
darunter nachrechenbar ist. --}}
<div class="small text-muted mt-1">
{{ __('abo.customer_abo_forecast_split', [
'billed' => \App\Services\Util::formatNumber($forecastDay['billed'] ?? 0.0),
'upcoming' => \App\Services\Util::formatNumber($forecastDay['upcoming']),
]) }}
</div>
@endif
</div>
</div>
</div>
@ -57,7 +78,10 @@
<tr>
<th data-sortable="true"><span class="sortable-label">{{ __('tables.customer') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="number"><span class="sortable-label">{{ __('tables.abo_delivery_day') }} <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">{{ __('navigation.points') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th class="text-right" data-sortable="true" data-sort-type="number" title="{{ __('abo.customer_abo_points_recurring_hint') }}"><span class="sortable-label justify-content-end">{{ __('abo.customer_abo_points_recurring') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
@if ($customerAboBilled !== null)
<th class="text-right" data-sortable="true" data-sort-type="number" title="{{ __('abo.customer_abo_points_billed_hint') }}"><span class="sortable-label justify-content-end">{{ __('abo.customer_abo_points_billed') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
@endif
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.next_date') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
<th data-sortable="true" data-sort-type="date"><span class="sortable-label">{{ __('tables.start_date') }} <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">{{ __('tables.abo_delivery') }} <i class="ion ion-ios-swap sort-icon"></i></span></th>
@ -65,6 +89,17 @@
<th></th>
</tr>
</thead>
@php
// Fuer die Summenzeile: was steht in der Liste, und bleibt vom
// Monatswert etwas uebrig, das keiner gelisteten Abo-Zeile zugeordnet
// werden kann (geloeschtes Abo, Buchung ohne Ausfuehrungsbezug)?
$billedShown = $customerAboBilled === null
? 0.0
: (float) $listAbos->sum(fn ($abo) => (float) ($customerAboBilled[$abo->id] ?? 0.0));
$billedRest = $customerAboBilled === null
? 0.0
: round((float) $customerAboBilledTotal - $billedShown, 2);
@endphp
<tbody>
@foreach ($listAbos as $listAbo)
<tr>
@ -87,6 +122,26 @@
</td>
<td data-sort-value="{{ (int) $listAbo->abo_interval }}">{{ \App\Services\HTMLHelper::getAboStrLang($listAbo->abo_interval) }}</td>
<td class="text-right font-weight-bold text-primary" data-sort-value="{{ $listAbo->getTotalPoints() }}">{{ $listAbo->getFormattedTotalPoints() }}</td>
@if ($customerAboBilled !== null)
@php
$billedPoints = (float) ($customerAboBilled[$listAbo->id] ?? 0.0);
// Differenz zum wiederkehrenden Aboinhalt: Einmal-Artikel und
// sonstige Zubuchungen der Lieferung, die danach wieder aus
// dem Abo entfernt werden. Nur ausweisen, wenn das Abo diesen
// Monat ueberhaupt geliefert hat.
$extraPoints = $billedPoints != 0.0
? round($billedPoints - (float) $listAbo->getTotalPoints(), 2)
: 0.0;
@endphp
<td class="text-right" data-sort-value="{{ $billedPoints }}">
<span class="font-weight-bold">{{ \App\Services\Util::formatNumber($billedPoints) }}</span>
@if ($extraPoints != 0.0)
<div class="small text-muted" title="{{ __('abo.customer_abo_points_extra_hint') }}">
{{ $extraPoints > 0 ? '+' : '' }}{{ \App\Services\Util::formatNumber($extraPoints) }} {{ __('abo.customer_abo_points_extra') }}
</div>
@endif
</td>
@endif
<td data-sort-value="{{ $listAbo->getRawOriginal('next_date') }}">{{ $listAbo->next_date }}</td>
<td data-sort-value="{{ $listAbo->getRawOriginal('start_date') }}">{{ $listAbo->start_date }}</td>
<td class="text-right" data-sort-value="{{ $listAbo->getCountPaidOrders() }}">{{ $listAbo->getCountPaidOrders() }}</td>
@ -99,6 +154,28 @@
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="font-weight-bold bg-light">
<td colspan="2">{{ __('abo.customer_abo_list_sum') }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($listAbos->sum(fn ($abo) => (float) $abo->getTotalPoints())) }}</td>
@if ($customerAboBilled !== null)
<td class="text-right">{{ \App\Services\Util::formatNumber($billedShown) }}</td>
@endif
<td colspan="5"></td>
</tr>
@if ($customerAboBilled !== null && $billedRest != 0.0)
<tr class="small text-muted bg-light">
<td colspan="3">{{ __('abo.customer_abo_list_sum_rest') }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber($billedRest) }}</td>
<td colspan="5"></td>
</tr>
<tr class="font-weight-bold bg-light">
<td colspan="3">{{ __('abo.customer_abo_list_sum_month') }}</td>
<td class="text-right">{{ \App\Services\Util::formatNumber((float) $customerAboBilledTotal) }}</td>
<td colspan="5"></td>
</tr>
@endif
</tfoot>
</table>
</div>
</div>

View file

@ -51,7 +51,9 @@
<div class="row">
<div class="col-12">
@php($abo_payment_links = $abo_payment_links ?? collect())
@php
$abo_payment_links = $abo_payment_links ?? collect();
@endphp
@if($view === 'ot' && $abo_payment_links->isNotEmpty())
@foreach($abo_payment_links as $paymentLink)
@ -140,7 +142,19 @@
</div>
@endif
@foreach($user_abos as $user_abo)
@php
// Kundenabos laufen ueber die sortierbare Liste; als Karte bleiben nur
// die Ausnahmefaelle mit offener Erstzahlung, weil dort Aktionsbuttons
// haengen (Kundenwunsch 2026-09-07).
$cardAbos = $view === 'ot'
? collect($user_abos)->filter(fn ($abo) => $abo->isPendingInitialPayment())
: collect($user_abos);
$listAbos = $view === 'ot'
? collect($user_abos)->reject(fn ($abo) => $abo->isPendingInitialPayment())->values()
: collect();
@endphp
@foreach($cardAbos as $user_abo)
<div class="card mb-4">
@if($user_abo->isPendingInitialPayment())
<div class="card-body pb-0">
@ -262,6 +276,15 @@
@endif
</div>
@endforeach
@if($view === 'ot' && $listAbos->isNotEmpty())
@include('user.abo._customer_abo_list', [
'listAbos' => $listAbos,
'customerAboForecast' => $customerAboForecast ?? null,
'customerAboBilled' => $customerAboBilled ?? null,
'customerAboBilledTotal' => $customerAboBilledTotal ?? null,
])
@endif
</div>
</div>
@endsection