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:
parent
77a4476fd0
commit
3c6190099f
2 changed files with 463 additions and 167 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue