UI-Feinschliff Buchungen: Tarife oben, Credits darunter, Preisliste, USt.-Modal

- Reihenfolge: Tarife -> Einzel-PM -> Credit-Wallet & Add-ons -> Preisliste
- Buchungs-Bestaetigungs-Modal mit USt.-Aufschluesselung (netto/USt./gesamt,
  Reverse-Charge & steuerbefreit) vor Stripe; greift fuer Tarife, Einzel-PM
  und Credit-Pakete (selectBooking)
- Proaktiver Banner oben, wenn die Rechnungsadresse fehlt (Link ins Profil);
  im Modal Adress-Gate statt Stripe-Link
- Extra-PM-Karte nur bei Guthaben > 0 oder aktivem Abo (sonst Preisliste)
- Pruefkontingent ausgeblendet (Phase 2)
- Preisliste: Extra-PM nach Tarif (eigener Tarif markiert), Boost-Laufzeiten,
  Nachweis-PDF

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-17 15:26:04 +00:00
parent 77a4476fd0
commit 3c6190099f
2 changed files with 463 additions and 167 deletions

View file

@ -2,15 +2,17 @@
use App\Enums\Portal;
use App\Enums\SinglePurchaseStatus;
use App\Enums\Tier;
use App\Enums\UserPaymentOptionStatus;
use App\Enums\VatTreatment;
use App\Exceptions\InsufficientCreditsException;
use App\Models\Plan;
use App\Models\UserPaymentOption;
use App\Services\Billing\CreditPricingService;
use App\Services\Billing\CreditTopupService;
use App\Services\Billing\CreditWalletService;
use App\Services\Billing\ExtraPmPurchaseService;
use App\Services\PressRelease\ReviewCheckService;
use App\Services\Billing\VatResolver;
use Flux\Flux;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
@ -18,8 +20,9 @@ use Livewire\Volt\Component;
/**
* Buchungen & Add-ons (Phase 9F): Tarif-Raster mit Stripe-Checkout,
* Einzel-PM als separater No-Abo-Block, Bestandstarife (MAN-Kreis) und
* echte Buchungsdaten. Die Credit-Wallet (Topup, Extra-PM, Prüfkontingent)
* bildet den Add-on-Block laut Decision-Update (Rev. 4) ab.
* echte Buchungsdaten. Die Credit-Wallet (Topup, Extra-PM) bildet den
* Add-on-Block laut Decision-Update (Rev. 4) ab. Jede Buchung läuft über ein
* Bestätigungs-Modal mit USt.-Aufschlüsselung. Prüfkontingent folgt mit Phase 2.
*/
new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class extends Component {
/** Erfolgs-/Fehlerhinweis des Wallet-Blocks (Extra-PM-Kauf). */
@ -27,11 +30,104 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
public ?string $walletError = null;
/** Im Bestätigungs-Modal angezeigte Buchung (inkl. USt.-Aufschlüsselung). */
public ?array $booking = null;
public function formatEuro(int $cents): string
{
return number_format($cents / 100, $cents % 100 === 0 ? 0 : 2, ',', '.') . ' €';
}
/**
* Öffnet das Bestätigungs-Modal für eine Buchung (Tarif-Abo, Einzel-PM
* oder Credit-Paket) mit USt.-Aufschlüsselung anhand der Rechnungsadresse.
* Bestätigt wird per Weiterleitung auf die jeweilige Checkout-Route.
*/
public function selectBooking(string $type, ?string $ref = null, ?string $interval = null): void
{
$user = auth()->user();
[$title, $subtitle, $netCents, $url] = match ($type) {
'subscription' => $this->subscriptionBooking($ref, $interval),
'single_pm' => [
__('Einzel-Pressemitteilung'),
__('Genau eine Veröffentlichung inkl. Prüfung — eingelöst erst bei Veröffentlichung.'),
(int) config('billing.single_pm_price_cents'),
route('me.checkout.single-pm'),
],
'credit_pack' => $this->creditPackBooking($ref),
default => [null, null, 0, null],
};
if (! $url) {
return;
}
$resolver = app(VatResolver::class);
$treatment = $resolver->resolve($user->billingAddress?->country_code, $user->billingAddress?->vat_id);
$vatCents = $resolver->taxCentsFor($netCents, $treatment);
$this->booking = [
'title' => $title,
'subtitle' => $subtitle,
'interval' => $interval,
'net' => $this->formatEuro($netCents),
'vat_percent' => (int) round($resolver->rateFor($treatment) * 100),
'vat' => $this->formatEuro($vatCents),
'gross' => $this->formatEuro($netCents + $vatCents),
'reverse_charge' => $treatment === VatTreatment::ReverseCharge,
'tax_exempt' => $treatment->isTaxExempt(),
'url' => $url,
'needs_address' => ! $user->hasCompleteBillingAddress(),
];
Flux::modal('confirm-booking')->show();
}
/**
* @return array{0: ?string, 1: ?string, 2: int, 3: ?string}
*/
private function subscriptionBooking(?string $slug, ?string $interval): array
{
$plan = Plan::query()->active()->where('slug', $slug)->first();
if (! $plan) {
return [null, null, 0, null];
}
$isYearly = $interval === 'yearly';
return [
$plan->name . ' — ' . ($isYearly ? __('Jährlich') : __('Monatlich')),
__(':quota Pressemitteilungen pro Monat, Prüfung und Veröffentlichung inklusive.', ['quota' => $plan->press_release_quota]),
$isYearly ? (int) $plan->yearly_price_cents : (int) $plan->monthly_price_cents,
route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => $isYearly ? 'yearly' : 'monthly']),
];
}
/**
* @return array{0: ?string, 1: ?string, 2: int, 3: ?string}
*/
private function creditPackBooking(?string $key): array
{
$pack = app(CreditTopupService::class)->findPack((string) $key);
if (! $pack) {
return [null, null, 0, null];
}
$bonus = $pack['credits'] - intdiv($pack['price_cents'], 100);
return [
__(':credits Credits Guthaben', ['credits' => $pack['credits']]),
$bonus > 0
? __('Enthält :bonus Bonus-Credits. 1 Credit = 1 € beim Einlösen.', ['bonus' => $bonus])
: __('1 Credit = 1 € beim Einlösen.'),
(int) $pack['price_cents'],
route('me.checkout.credit-topup', ['pack' => $pack['key']]),
];
}
/**
* Extra-PM aus der Wallet nachkaufen (tier-gestaffelter Preis). Reicht das
* Guthaben nicht, zeigt der kontextuelle Mini-Checkout den Fehlbetrag.
@ -91,6 +187,8 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
$user = auth()->user();
$subscription = $user->subscription();
$currentPlan = $user->currentPlan();
$currentTier = $user->currentTier();
$pricing = app(CreditPricingService::class);
$currentInterval = null;
if ($currentPlan && $subscription) {
@ -125,14 +223,23 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
'singlePmAvailable' => (bool) config('billing.single_pm_stripe_price_id'),
// Credit-Wallet & Add-ons (Decision-Update Rev. 4).
'hasCompleteBillingAddress' => $user->hasCompleteBillingAddress(),
'creditBalance' => $user->creditBalance(),
'creditPacks' => app(CreditTopupService::class)->packs(),
'extraPmPrice' => app(CreditPricingService::class)->extraPmCreditsFor($user),
'reviewFreeRemaining' => app(ReviewCheckService::class)->freeRemaining($user),
'reviewFreeQuota' => app(CreditPricingService::class)->reviewFreeQuota($user->currentTier()),
'reviewUsedToday' => app(ReviewCheckService::class)->usedToday($user),
'reviewDailyLimit' => app(CreditPricingService::class)->reviewDailyLimit(),
'extraPmPrice' => $pricing->extraPmCreditsFor($user),
// Extra-PM nur anbieten, wenn man Guthaben hat oder im Abo ist —
// sonst ergibt sich der Preis aus der Preisliste (Decision-Update).
'showExtraPm' => $user->creditBalance() > 0 || (bool) $subscription,
'recentTransactions' => $user->creditTransactions()->latest()->limit(6)->get(),
// Preisliste (Add-on-Credits).
'extraPmStaffel' => collect(Tier::cases())->map(fn (Tier $t): array => [
'label' => $t->label(),
'credits' => $pricing->extraPmCredits($t),
'isCurrent' => $t === $currentTier,
])->all(),
'boostStaffel' => $pricing->boostOptions(),
'proofPrice' => $pricing->proofPdfCredits(),
];
}
}; ?>
@ -170,6 +277,23 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</div>
@endif
{{-- ============== RECHNUNGSADRESSE FEHLT ============== --}}
{{-- Proaktiver Hinweis: jede Buchung setzt eine vollständige
Rechnungsadresse voraus (sonst leitet der Checkout-Guard ins Profil). --}}
@if (! $hasCompleteBillingAddress)
<div
class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
<flux:icon.exclamation-triangle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-accent-deep)]" />
<div class="flex-1">
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Rechnungsadresse fehlt.') }}</span>
{{ __('Für jede Buchung ist eine vollständige Rechnungsadresse nötig — sie wird an Stripe übergeben.') }}
<a href="{{ route('me.profile') }}" wire:navigate
class="font-semibold text-[color:var(--color-hub)] underline underline-offset-2">{{ __('Jetzt hinterlegen') }}</a>
</div>
</div>
@endif
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
@ -350,153 +474,6 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</article>
@endif
{{-- ============== CREDIT-WALLET & ADD-ONS ============== --}}
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Credit-Wallet & Add-ons') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ __('Guthaben, Extra-PM und Prüfungen') }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[640px] m-0">
{{ __('1 Credit = 1 €. Mit Guthaben kaufen Sie Extra-Pressemitteilungen, Boosts und Veröffentlichungsnachweise — größere Pakete enthalten Bonus-Credits.') }}
</p>
</div>
{{-- Guthaben + Aufladen --}}
<article class="panel">
<div class="p-5 grid gap-5 md:grid-cols-[0.9fr_1.6fr] md:items-center">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-[8px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.wallet class="size-6" />
</div>
<div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Ihr Guthaben') }}</div>
<div class="text-[28px] font-bold tracking-[-0.7px] leading-none text-[color:var(--color-ink)]">
{{ $creditBalance }} <span class="text-[14px] font-normal text-[color:var(--color-ink-3)]">{{ __('Credits') }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Guthaben aufladen') }}</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
@foreach ($creditPacks as $pack)
@php($bonus = $pack['credits'] - intdiv($pack['price_cents'], 100))
<a href="{{ route('me.checkout.credit-topup', ['pack' => $pack['key']]) }}"
class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-subtle)] p-3 text-center transition-colors hover:border-[color:var(--color-hub)] hover:bg-[color:var(--color-hub-soft)]"
wire:key="pack-{{ $pack['key'] }}">
<div class="text-[16px] font-bold text-[color:var(--color-ink)]">{{ $pack['credits'] }} {{ __('Credits') }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $this->formatEuro($pack['price_cents']) }} {{ __('netto') }}</div>
@if ($bonus > 0)
<span class="badge ok mt-1 inline-block">+{{ $bonus }} {{ __('Bonus') }}</span>
@endif
</a>
@endforeach
</div>
</div>
</div>
</article>
{{-- Mini-Checkout-Hinweise (Extra-PM) --}}
@if ($walletNotice)
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">{{ $walletNotice }}</div>
</div>
@endif
@if ($walletError)
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
<div class="flex-1">{{ $walletError }}</div>
</div>
@endif
<div class="grid gap-4 md:grid-cols-2">
{{-- Extra-PM --}}
<article class="panel">
<div class="p-5 space-y-4">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
<flux:icon.document-plus class="size-5" />
</div>
<div class="min-w-0">
<h3 class="text-[15px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Extra-Pressemitteilung') }}</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ __('Eine weitere PM, wenn Ihr Monatskontingent voll ist — ohne Tarifwechsel. Preis je nach Tarif.') }}
</p>
</div>
</div>
<div class="flex items-center justify-between gap-3 border-t border-[color:var(--color-bg-rule)] pt-4">
<div>
<span class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $extraPmPrice }}</span>
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Credits') }}</span>
</div>
<flux:button size="sm" variant="primary" wire:click="buyExtraPm" wire:loading.attr="disabled">
{{ __('Aus Guthaben buchen') }}
</flux:button>
</div>
</div>
</article>
{{-- Prüfkontingent --}}
<article class="panel">
<div class="p-5 space-y-4">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
<flux:icon.shield-check class="size-5" />
</div>
<div class="min-w-0">
<h3 class="text-[15px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Prüfkontingent') }}</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ __('Inklusive Vorab-Prüfungen pro Monat. Darüber hinaus zieht jede Prüfung 1 Credit.') }}
</p>
</div>
</div>
<div class="border-t border-[color:var(--color-bg-rule)] pt-4 space-y-1.5">
<div class="flex items-baseline justify-between">
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">{{ __('Frei diesen Monat') }}</span>
<span class="text-[16px] font-semibold text-[color:var(--color-ink)]">{{ $reviewFreeRemaining }} / {{ $reviewFreeQuota }}</span>
</div>
<div class="flex items-baseline justify-between">
<span class="text-[12.5px] text-[color:var(--color-ink-3)]">{{ __('Heute genutzt') }}</span>
<span class="text-[13px] text-[color:var(--color-ink-2)]">{{ $reviewUsedToday }} / {{ $reviewDailyLimit }} {{ __('Tageslimit') }}</span>
</div>
</div>
</div>
</article>
</div>
{{-- Wallet-Verlauf --}}
@if ($recentTransactions->isNotEmpty())
<article class="panel">
<div class="p-5">
<div class="text-[12px] text-[color:var(--color-ink-3)] mb-3">{{ __('Letzte Wallet-Buchungen') }}</div>
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@foreach ($recentTransactions as $tx)
<div class="py-2.5 first:pt-0 last:pb-0 flex items-center justify-between gap-4"
wire:key="tx-{{ $tx->id }}">
<div class="min-w-0">
<div class="text-[13px] text-[color:var(--color-ink)] truncate">{{ $tx->description ?? $tx->type->label() }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $tx->created_at?->format('d.m.Y H:i') }}</div>
</div>
<div @class([
'text-[14px] font-semibold flex-shrink-0',
'text-[color:var(--color-gain-deep)]' => $tx->amount_credits > 0,
'text-[color:var(--color-ink-2)]' => $tx->amount_credits < 0,
])>
{{ $tx->amount_credits > 0 ? '+' : '' }}{{ $tx->amount_credits }}
</div>
</div>
@endforeach
</div>
</div>
</article>
@endif
</section>
{{-- ============== TARIF-RASTER ============== --}}
<section class="space-y-4" x-data="{ interval: '{{ $currentInterval ?? 'monthly' }}' }">
<div class="flex items-end justify-between gap-4 flex-wrap">
@ -599,13 +576,13 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
@else
<div x-show="interval === 'monthly'">
<flux:button variant="primary" class="w-full"
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'monthly']) }}">
wire:click="selectBooking('subscription', '{{ $plan->slug }}', 'monthly')">
{{ __('Monatlich buchen') }}
</flux:button>
</div>
<div x-show="interval === 'yearly'" x-cloak>
<flux:button variant="primary" class="w-full"
href="{{ route('me.checkout.subscription', ['planSlug' => $plan->slug, 'interval' => 'yearly']) }}">
wire:click="selectBooking('subscription', '{{ $plan->slug }}', 'yearly')">
{{ __('Jährlich buchen') }}
</flux:button>
</div>
@ -648,7 +625,7 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
<div class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $singlePmPrice }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ __('netto zzgl. USt.') }}</div>
</div>
<flux:button size="sm" variant="primary" href="{{ route('me.checkout.single-pm') }}"
<flux:button size="sm" variant="primary" wire:click="selectBooking('single_pm')"
:disabled="!$singlePmAvailable">
{{ __('Jetzt buchen') }}
</flux:button>
@ -656,6 +633,199 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</div>
</article>
{{-- ============== CREDIT-WALLET & ADD-ONS ============== --}}
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Credit-Wallet & Add-ons') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ __('Guthaben & Credit-Add-ons') }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[640px] m-0">
{{ __('1 Credit = 1 €. Mit Guthaben kaufen Sie Extra-Pressemitteilungen, Boosts und Veröffentlichungsnachweise — größere Pakete enthalten Bonus-Credits.') }}
</p>
</div>
{{-- Guthaben + Aufladen --}}
<article class="panel">
<div class="p-5 grid gap-5 md:grid-cols-[0.9fr_1.6fr] md:items-center">
<div class="flex items-center gap-4">
<div
class="w-12 h-12 rounded-[8px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] border border-[color:var(--color-hub-soft-2)] text-[color:var(--color-hub)]">
<flux:icon.wallet class="size-6" />
</div>
<div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Ihr Guthaben') }}</div>
<div class="text-[28px] font-bold tracking-[-0.7px] leading-none text-[color:var(--color-ink)]">
{{ $creditBalance }} <span class="text-[14px] font-normal text-[color:var(--color-ink-3)]">{{ __('Credits') }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<div class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Guthaben aufladen') }}</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
@foreach ($creditPacks as $pack)
@php($bonus = $pack['credits'] - intdiv($pack['price_cents'], 100))
<button type="button"
wire:click="selectBooking('credit_pack', '{{ $pack['key'] }}')"
class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-subtle)] p-3 text-center transition-colors hover:border-[color:var(--color-hub)] hover:bg-[color:var(--color-hub-soft)]"
wire:key="pack-{{ $pack['key'] }}">
<div class="text-[16px] font-bold text-[color:var(--color-ink)]">{{ $pack['credits'] }} {{ __('Credits') }}</div>
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $this->formatEuro($pack['price_cents']) }} {{ __('netto') }}</div>
@if ($bonus > 0)
<span class="badge ok mt-1 inline-block">+{{ $bonus }} {{ __('Bonus') }}</span>
@endif
</button>
@endforeach
</div>
</div>
</div>
</article>
{{-- Extra-PM: nur bei vorhandenem Guthaben oder aktivem Abo (sonst Preisliste) --}}
@if ($showExtraPm)
@if ($walletNotice)
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-hub-soft)] border-[color:var(--color-hub-soft-2)] text-[color:var(--color-ink-2)]">
<flux:icon.check-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-hub)]" />
<div class="flex-1">{{ $walletNotice }}</div>
</div>
@endif
@if ($walletError)
<div class="px-4 py-3 rounded-[5px] border text-[13px] flex items-start gap-3
bg-[color:var(--color-bg-subtle)] border-[color:var(--color-bg-rule)] text-[color:var(--color-ink-2)]">
<flux:icon.information-circle class="size-[16px] flex-shrink-0 mt-0.5 text-[color:var(--color-ink-3)]" />
<div class="flex-1">{{ $walletError }}</div>
</div>
@endif
<article class="panel">
<div class="p-5 grid gap-4 md:grid-cols-[1fr_auto] md:items-center">
<div class="flex items-start gap-3">
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
<flux:icon.document-plus class="size-5" />
</div>
<div class="min-w-0">
<h3 class="text-[15px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Extra-Pressemitteilung') }}</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ __('Eine weitere PM, wenn Ihr Monatskontingent voll ist — ohne Tarifwechsel.') }}
</p>
</div>
</div>
<div class="flex items-center gap-4 md:justify-end">
<div class="text-right">
<span class="text-[22px] font-bold text-[color:var(--color-ink)]">{{ $extraPmPrice }}</span>
<span class="text-[12px] text-[color:var(--color-ink-3)]">{{ __('Credits') }}</span>
</div>
<flux:button size="sm" variant="primary" wire:click="buyExtraPm" wire:loading.attr="disabled">
{{ __('Aus Guthaben buchen') }}
</flux:button>
</div>
</div>
</article>
@endif
{{-- Wallet-Verlauf --}}
@if ($recentTransactions->isNotEmpty())
<article class="panel">
<div class="p-5">
<div class="text-[12px] text-[color:var(--color-ink-3)] mb-3">{{ __('Letzte Wallet-Buchungen') }}</div>
<div class="divide-y divide-[color:var(--color-bg-rule)]">
@foreach ($recentTransactions as $tx)
<div class="py-2.5 first:pt-0 last:pb-0 flex items-center justify-between gap-4"
wire:key="tx-{{ $tx->id }}">
<div class="min-w-0">
<div class="text-[13px] text-[color:var(--color-ink)] truncate">{{ $tx->description ?? $tx->type->label() }}</div>
<div class="text-[11px] text-[color:var(--color-ink-3)]">{{ $tx->created_at?->format('d.m.Y H:i') }}</div>
</div>
<div @class([
'text-[14px] font-semibold flex-shrink-0',
'text-[color:var(--color-gain-deep)]' => $tx->amount_credits > 0,
'text-[color:var(--color-ink-2)]' => $tx->amount_credits < 0,
])>
{{ $tx->amount_credits > 0 ? '+' : '' }}{{ $tx->amount_credits }}
</div>
</div>
@endforeach
</div>
</div>
</article>
@endif
</section>
{{-- ============== PREISLISTE (Add-on-Credits) ============== --}}
<section class="space-y-4">
<div>
<span class="section-eyebrow">{{ __('Preisliste') }}</span>
<h2 class="text-[22px] font-bold tracking-[-0.4px] mt-1 mb-1 text-[color:var(--color-ink)]">
{{ __('Was kostet wie viel') }}
</h2>
<p class="text-[12.5px] text-[color:var(--color-ink-3)] max-w-[640px] m-0">
{{ __('Add-ons werden mit Credits bezahlt (1 Credit = 1 €). Der Extra-PM-Preis richtet sich nach Ihrem Tarif — höhere Tarife zahlen weniger.') }}
</p>
</div>
<article class="panel">
<div class="p-5 grid gap-6 md:grid-cols-3 md:divide-x md:divide-[color:var(--color-bg-rule)]">
{{-- Extra-PM nach Tarif --}}
<div class="md:pr-6">
<div class="flex items-center gap-2 mb-3">
<flux:icon.document-plus class="size-4 text-[color:var(--color-hub)]" />
<h3 class="text-[13.5px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Extra-Pressemitteilung') }}</h3>
</div>
<table class="w-full text-[12.5px]">
<tbody class="divide-y divide-[color:var(--color-bg-rule)]">
@foreach ($extraPmStaffel as $row)
<tr @class(['', 'font-semibold' => $row['isCurrent']])>
<td class="py-1.5 text-[color:var(--color-ink-2)]">
{{ $row['label'] }}
@if ($row['isCurrent'])
<span class="badge hub ms-1">{{ __('Ihr Tarif') }}</span>
@endif
</td>
<td class="py-1.5 text-right text-[color:var(--color-ink)]">{{ $row['credits'] }} {{ __('Credits') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Boost nach Laufzeit --}}
<div class="md:px-6">
<div class="flex items-center gap-2 mb-3">
<flux:icon.rocket-launch class="size-4 text-[color:var(--color-hub)]" />
<h3 class="text-[13.5px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Boost — Platzierung') }}</h3>
</div>
<table class="w-full text-[12.5px]">
<tbody class="divide-y divide-[color:var(--color-bg-rule)]">
@foreach ($boostStaffel as $days => $credits)
<tr>
<td class="py-1.5 text-[color:var(--color-ink-2)]">{{ __(':days Tage', ['days' => $days]) }}</td>
<td class="py-1.5 text-right text-[color:var(--color-ink)]">{{ $credits }} {{ __('Credits') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Veröffentlichungsnachweis --}}
<div class="md:pl-6">
<div class="flex items-center gap-2 mb-3">
<flux:icon.document-check class="size-4 text-[color:var(--color-hub)]" />
<h3 class="text-[13.5px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Veröffentlichungsnachweis') }}</h3>
</div>
<div class="flex items-baseline justify-between text-[12.5px] py-1.5">
<span class="text-[color:var(--color-ink-2)]">{{ __('PDF pro PM') }}</span>
<span class="text-[color:var(--color-ink)] font-semibold">{{ $proofPrice }} {{ __('Credits') }}</span>
</div>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] mt-2 m-0">
{{ __('Boost und Nachweis buchen Sie direkt an der jeweiligen veröffentlichten Pressemitteilung.') }}
</p>
</div>
</div>
</article>
</section>
{{-- ============== VERLAUF ============== --}}
{{-- Aktive Buchungen stehen oben im Tarif-Panel hier nur die Historie,
durch Trennlinie und Zwischenüberschrift klar abgesetzt. --}}
@ -707,4 +877,74 @@ new #[Layout('components.layouts.app'), Title('Buchungen & Add-ons')] class exte
</div>
</article>
</section>
{{-- ============== BUCHUNGS-BESTÄTIGUNG (Modal) ============== --}}
<flux:modal name="confirm-booking" class="max-w-lg">
@if ($booking)
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Buchung bestätigen') }}</flux:heading>
<flux:subheading>{{ $booking['title'] }}</flux:subheading>
</div>
@if ($booking['subtitle'])
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0">{{ $booking['subtitle'] }}</p>
@endif
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] divide-y divide-[color:var(--color-bg-rule)] text-[13px]">
<div class="flex items-center justify-between px-4 py-2.5">
<span class="text-[color:var(--color-ink-3)]">{{ __('Netto') }}</span>
<span class="font-medium text-[color:var(--color-ink)]">{{ $booking['net'] }}</span>
</div>
@if ($booking['reverse_charge'])
<div class="flex items-center justify-between px-4 py-2.5">
<span class="text-[color:var(--color-ink-3)]">{{ __('USt.') }}</span>
<span class="text-[color:var(--color-ink-2)]">{{ __('Reverse Charge (0 %)') }}</span>
</div>
@elseif ($booking['tax_exempt'])
<div class="flex items-center justify-between px-4 py-2.5">
<span class="text-[color:var(--color-ink-3)]">{{ __('USt.') }}</span>
<span class="text-[color:var(--color-ink-2)]">{{ __('steuerbefreit') }}</span>
</div>
@else
<div class="flex items-center justify-between px-4 py-2.5">
<span class="text-[color:var(--color-ink-3)]">{{ __('zzgl. :p % USt.', ['p' => $booking['vat_percent']]) }}</span>
<span class="font-medium text-[color:var(--color-ink)]">{{ $booking['vat'] }}</span>
</div>
@endif
<div class="flex items-center justify-between px-4 py-2.5 bg-[color:var(--color-bg-subtle)]">
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Gesamt') }}</span>
<span class="font-bold text-[color:var(--color-ink)]">{{ $booking['gross'] }}</span>
</div>
</div>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Die endgültige Steuer berechnet Stripe anhand Ihrer Rechnungsadresse. Die Abrechnung erfolgt sicher über Stripe.') }}
</p>
@if ($booking['needs_address'])
<div class="px-4 py-3 rounded-[5px] border text-[13px] bg-[color:var(--color-warn-soft)] border-[color:var(--color-warn)]/30 text-[color:var(--color-ink-2)]">
{{ __('Bitte hinterlegen Sie zuerst eine vollständige Rechnungsadresse — sie ist Voraussetzung für jede Buchung.') }}
</div>
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" icon="map-pin" href="{{ route('me.profile') }}" wire:navigate>
{{ __('Rechnungsadresse hinterlegen') }}
</flux:button>
</div>
@else
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" icon="lock-closed" href="{{ $booking['url'] }}">
{{ __('Kostenpflichtig buchen') }}
</flux:button>
</div>
@endif
</div>
@endif
</flux:modal>
</div>