Abo Einmalprodukte: Review-Gate (VIP), Verbindlichkeit & Summen-Layout

- Live-Review-Gate: Einmalprodukte nur fuer VIP im Sales Center sichtbar,
  Portal ausgeblendet (AboHelper::isOneTimeFeatureVisible + Gates in Controllern)
- Nur verbindlich bestaetigte Einmal-Artikel fliessen in die Lieferung;
  Service-Helfer confirmedItems/pendingItems/pendingGross
- Footer-Layout der Einmalprodukt-Liste: bestaetigte Summe + Gesamtbetrag,
  Trennstrich, offener Betrag und neue Gesamtsumme (dunkelgruen)
- Uebersetzungen DE/EN/ES/FR (onetime_new_total u.a.), Tests angepasst/ergaenzt

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin 2026-06-08 14:59:22 +00:00
parent 2269ce031f
commit 8288ea59ac
16 changed files with 356 additions and 46 deletions

View file

@ -157,10 +157,10 @@ class AboHelper
public static function getAboTypeBadge($abo_type)
{
if ($abo_type === 'base') {
return '<span class="badge badge-pill badge-warning"><i class="fas fa-star"></i> '.__('abo.'.$abo_type).'</span></a>';
return '<span class="badge badge-pill badge-warning"><i class="fas fa-star"></i> ' . __('abo.' . $abo_type) . '</span></a>';
}
if ($abo_type === 'upgrade') {
return '<span class="badge badge-pill badge-info"><i class="far fa-star"></i> '.__('abo.'.$abo_type).'</span></a>';
return '<span class="badge badge-pill badge-info"><i class="far fa-star"></i> ' . __('abo.' . $abo_type) . '</span></a>';
}
return '';
@ -209,6 +209,27 @@ class AboHelper
return $daysUntilExecution <= self::getOneTimeWindowDays();
}
/**
* Sichtbarkeit des Einmalprodukte-Features während der Live-Abstimmung.
*
* Temporäre Einschränkung: Das Feature wird vorerst nur VIP-Usern (Admins)
* im Sales Center angezeigt. Das Endkunden-Portal läuft über den
* `customers`-Guard und hat keinen VIP-User auf dem `user`-Guard, daher
* bleibt das Feature dort vollständig ausgeblendet. Nach Abschluss der
* Abstimmung kann diese Methode wieder auf reines isOneTimeWindowOpen()
* zurückgesetzt werden, um das Feature für alle freizuschalten.
*/
public static function isOneTimeFeatureVisible(UserAbo $userAbo): bool
{
/* if (! self::isOneTimeWindowOpen($userAbo)) {
return false;
}*/
$user = \Auth::guard('user')->user();
return $user instanceof User && $user->isVIP();
}
public static function getFirstAboDate($date, $abo_interval)
{
$reference = Carbon::parse($date)->startOfDay();
@ -391,7 +412,7 @@ class AboHelper
{
$ret = [];
foreach (self::$txaction_filter_text as $key => $val) {
$ret[$key] = trans('payment.'.$val);
$ret[$key] = trans('payment.' . $val);
}
return $ret;

View file

@ -158,6 +158,40 @@ class AboOneTimeService
return null;
}
/**
* Verbindlich bestätigte Einmal-Artikel (werden mit der nächsten Lieferung versandt).
*
* @return \Illuminate\Support\Collection<int, UserAboOneTimeItem>
*/
public static function confirmedItems(UserAbo $userAbo): \Illuminate\Support\Collection
{
return $userAbo->one_time_items()
->with('product')
->get()
->filter(fn (UserAboOneTimeItem $item): bool => $item->isConfirmed())
->values();
}
/**
* Noch nicht bestätigte Einmal-Artikel (Entwurf, nicht verbindlich).
*
* @return \Illuminate\Support\Collection<int, UserAboOneTimeItem>
*/
public static function pendingItems(UserAbo $userAbo): \Illuminate\Support\Collection
{
return $userAbo->one_time_items()
->with('product')
->get()
->reject(fn (UserAboOneTimeItem $item): bool => $item->isConfirmed())
->values();
}
public static function pendingGross(UserAbo $userAbo): float
{
return round(self::pendingItems($userAbo)
->sum(fn (UserAboOneTimeItem $item): float => (float) $item->price * (int) $item->qty), 2);
}
public static function hasUnconfirmedChanges(UserAbo $userAbo): bool
{
return UserAboOneTimeItem::withTrashed()

View file

@ -246,7 +246,14 @@ class AboOrderCart
$yard = Yard::instance(self::INSTANCE);
foreach ($user_abo->one_time_items()->get() as $item) {
// Nur verbindlich bestätigte Einmal-Artikel fließen in die nächste Lieferung
// (Snapshot-Menge == bestätigte Menge). Unbestätigte Entwürfe bleiben außen vor.
$confirmedItems = $user_abo->one_time_items()
->whereNotNull('confirmed_at')
->whereColumn('confirmed_qty', 'qty')
->get();
foreach ($confirmedItems as $item) {
$product = Product::find($item->product_id);
if (! $product) {
continue;