WS-4: Bestandsschutz im Kundenbereich als echtes Paket darstellen

/admin/me/buchungen-add-ons: Bestandsschutz raus aus dem gedrängten
"Aktueller Tarif"-Panel, eigener prominenter Paket-Block:
- Portal-Badge (fehlte bisher), Schild-Icon, Akzentleiste, Rahmen (ring)
- größere Typo, Netto-Preis + Intervall, Feature-Liste (unbegrenzte PMs,
  Konditionen unverändert, nächste Rechnung), Bündel-Hinweis bei mehreren Posten
- Helper legacyPortalLabel()/legacyIntervalLabel() im Volt-Component
- BookingsPageTest auf die neue Darstellung + Portal-Anzeige aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-16 15:29:21 +00:00
parent be7d1799a5
commit afefa86711
2 changed files with 119 additions and 24 deletions

View file

@ -1,8 +1,10 @@
<?php
use App\Enums\Portal;
use App\Enums\SinglePurchaseStatus;
use App\Enums\UserPaymentOptionStatus;
use App\Models\Plan;
use App\Models\UserPaymentOption;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
@ -30,6 +32,28 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
};
}
/**
* Lesbares Portal-Label für einen Bestandsschutz-Posten (aus den
* Legacy-Konditionen). Fällt auf „Portal unbekannt" zurück.
*/
public function legacyPortalLabel(UserPaymentOption $option): string
{
$value = data_get($option->legacy_conditions, 'legacy_portal');
return Portal::tryFrom((string) $value)?->label() ?? __('Portal unbekannt');
}
/**
* Abrechnungsintervall eines Bestandsschutz-Postens als Label.
*/
public function legacyIntervalLabel(UserPaymentOption $option): string
{
return match (data_get($option->legacy_conditions, 'interval')) {
'monthly' => __('Monat'),
default => __('Jahr'),
};
}
public function with(): array
{
$user = auth()->user();
@ -127,17 +151,97 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</div>
</header>
{{-- ============== BESTANDSSCHUTZ-PAKETE ============== --}}
{{-- Migrierte Legacy-Vereinbarungen als echte, gebuchte Pakete dargestellt:
eigener Block, Rahmen, Icon, Portal-Badge, klare Typo. --}}
@if ($legacyOptions->isNotEmpty())
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Ihr Bestandsschutz') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ trans_choice('Ihr gebuchtes Paket|Ihre gebuchten Pakete', $legacyOptions->count()) }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[640px] m-0">
{{ __('Ihre bisherigen Konditionen gelten unverändert weiter — unbegrenzte Pressemitteilungen, Abrechnung wie gewohnt per Rechnung.') }}
</p>
</div>
<div @class(['grid gap-4', 'md:grid-cols-2' => $legacyOptions->count() > 1])>
@foreach ($legacyOptions as $option)
@php
$netCents = (int) data_get($option->legacy_conditions, 'net_cents', 0);
$legacyName = data_get($option->legacy_conditions, 'name')
?? ($option->paymentOption?->article_number ?? __('Bestehende Vereinbarung'));
@endphp
<article class="panel overflow-hidden ring-1 ring-[color:var(--color-ok)]/25" wire:key="legacy-{{ $option->id }}">
{{-- Akzent-Leiste --}}
<div class="h-1 bg-[color:var(--color-ok)]/60"></div>
<div class="p-6 space-y-5">
<div class="flex items-start justify-between gap-4">
<div class="flex items-start gap-4 min-w-0">
<div class="w-12 h-12 rounded-[8px] flex items-center justify-center flex-shrink-0
bg-[color:var(--color-ok-soft)] border border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.shield-check class="size-6" />
</div>
<div class="min-w-0">
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
<span class="badge ok dot">{{ __('Bestandsschutz') }}</span>
<span class="badge hub">{{ $this->legacyPortalLabel($option) }}</span>
</div>
<h3 class="text-[18px] font-bold tracking-[-0.3px] text-[color:var(--color-ink)] m-0 break-words">
{{ $legacyName }}
</h3>
</div>
</div>
<div class="text-right flex-shrink-0">
<div class="text-[26px] font-bold tracking-[-0.6px] leading-none text-[color:var(--color-ink)]">
{{ $this->formatEuro($netCents) }}
</div>
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-1">
{{ __('netto / :interval', ['interval' => $this->legacyIntervalLabel($option)]) }}
</div>
</div>
</div>
<ul class="m-0 p-0 list-none space-y-2.5 text-[12.5px] text-[color:var(--color-ink-2)]
border-t border-[color:var(--color-bg-rule)] pt-4">
<li class="flex items-start gap-2">
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-gain-deep)]" />
<span><strong class="text-[color:var(--color-ink)]">{{ __('Unbegrenzte') }}</strong> {{ __('Pressemitteilungen') }}</span>
</li>
<li class="flex items-start gap-2">
<flux:icon.check class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-gain-deep)]" />
{{ __('Konditionen gelten unverändert weiter') }}
</li>
@if ($option->current_period_end)
<li class="flex items-start gap-2">
<flux:icon.calendar-days class="size-4 flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
{{ __('Nächste Rechnung am :date', ['date' => $option->current_period_end->format('d.m.Y')]) }}
</li>
@endif
</ul>
</div>
</article>
@endforeach
</div>
@if ($legacyOptions->count() > 1)
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Ihre Pakete werden gemeinsam geführt und sind nur zusammen kündbar (keine Einzelkündigung).') }}
</p>
@endif
</section>
@endif
{{-- ============== AKTUELLER TARIF ============== --}}
{{-- Erscheint erst, wenn eine Buchung existiert vorher würde hier nur
„kein Tarif" stehen und das Kontingent wäre irreführend. --}}
@if ($subscription || $legacyOptions->isNotEmpty() || $openPurchases->isNotEmpty())
@if ($subscription || $openPurchases->isNotEmpty())
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktueller Tarif') }}</span>
@if ($currentPlan)
<span class="badge hub">{{ $currentPlan->name }}</span>
@elseif ($legacyOptions->isNotEmpty())
<span class="badge ok dot">{{ __('Bestandstarif') }}</span>
@else
<span class="badge hub">{{ __('Einzel-PM') }}</span>
@endif
@ -164,23 +268,6 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
{{ __('Gekündigt — läuft am :date aus.', ['date' => $subscription->ends_at?->format('d.m.Y')]) }}
</p>
@endif
@elseif ($legacyOptions->isNotEmpty())
@foreach ($legacyOptions as $option)
<div>
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
{{ data_get($option->legacy_conditions, 'name') ?? ($option->paymentOption?->article_number ?? __('Bestehende Vereinbarung')) }}
</div>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ __('Unbegrenzte Pressemitteilungen (Bestandsschutz).') }}
@if ($option->current_period_end)
{{ __('Nächste Rechnung am :date.', ['date' => $option->current_period_end->format('d.m.Y')]) }}
@endif
</p>
</div>
@endforeach
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Ihre bisherigen Konditionen gelten unverändert weiter; die Abrechnung erfolgt wie gewohnt per Rechnung.') }}
</p>
@elseif ($openPurchases->isNotEmpty())
<div class="text-[15px] font-semibold text-[color:var(--color-ink)]">
{{ trans_choice(':count Einzel-Pressemitteilung verfügbar|:count Einzel-Pressemitteilungen verfügbar', $openPurchases->count(), ['count' => $openPurchases->count()]) }}