UI: Credit-Wallet-Hub + Per-PM Add-ons im Backend-Pressekonto
Wallet-Oekonomie im Customer-Bereich sichtbar gemacht: - Buchungen & Add-ons: Guthaben-Anzeige, Credit-Paket-Topup (Bonus-Staffel), Extra-PM-Kauf aus der Wallet mit kontextuellem Mini-Checkout-Hinweis, Pruefkontingent-Status (frei/Monat, heute/Tageslimit), Wallet-Verlauf - PM-Detailseite: Add-ons-Panel fuer veroeffentlichte PMs -- Boost buchen (7/14/30 Tage, Gate nur gruen, Verlaengerung), Veroeffentlichungsnachweis kaufen + Download. Aktionen ueber BoostService/ProofPdfService Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
344aac0740
commit
77a4476fd0
4 changed files with 482 additions and 2 deletions
|
|
@ -1,10 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Exceptions\BoostNotAllowedException;
|
||||
use App\Exceptions\InsufficientCreditsException;
|
||||
use App\Models\PressRelease;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use App\Services\Billing\CreditPricingService;
|
||||
use App\Services\Billing\ProofPdfService;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\BookingRequiredException;
|
||||
use App\Services\PressRelease\BoostService;
|
||||
use App\Services\PressRelease\PressReleaseCoverImage;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use App\Services\PressRelease\QuotaExceededException;
|
||||
|
|
@ -83,6 +88,60 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
Flux::toast(text: __('Vorschau-Link wurde erzeugt.'), variant: 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Boost (Platzierung) aus der Wallet buchen — nur veröffentlichte, grüne
|
||||
* PMs sind boostbar (Gate im BoostService).
|
||||
*/
|
||||
public function buyBoost(int $days): void
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('view', $pr);
|
||||
|
||||
try {
|
||||
app(BoostService::class)->boost(auth()->user(), $pr, $days);
|
||||
Flux::toast(
|
||||
heading: __('Boost gebucht'),
|
||||
text: __('Ihre Pressemitteilung wird jetzt auf Start- und Branchenseite hervorgehoben.'),
|
||||
variant: 'success',
|
||||
);
|
||||
} catch (InsufficientCreditsException $e) {
|
||||
Flux::toast(
|
||||
heading: __('Nicht genügend Credits'),
|
||||
text: __('Der Boost kostet :need Credits, Ihr Guthaben beträgt :have. Laden Sie unter „Buchungen & Add-ons" auf.', ['need' => $e->required, 'have' => $e->available]),
|
||||
variant: 'warning',
|
||||
duration: 8000,
|
||||
);
|
||||
} catch (BoostNotAllowedException $e) {
|
||||
Flux::toast(text: $e->getMessage(), variant: 'warning', duration: 8000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Veröffentlichungsnachweis-PDF kaufen (3 Credits, einmalig je PM). Danach
|
||||
* steht der Download über die Nachweis-Route bereit.
|
||||
*/
|
||||
public function buyProof(): void
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
$this->authorize('downloadProof', $pr);
|
||||
|
||||
try {
|
||||
app(ProofPdfService::class)->purchase(auth()->user(), $pr);
|
||||
Flux::toast(
|
||||
heading: __('Nachweis gekauft'),
|
||||
text: __('Der Veröffentlichungsnachweis steht jetzt zum Download bereit.'),
|
||||
variant: 'success',
|
||||
);
|
||||
} catch (InsufficientCreditsException $e) {
|
||||
Flux::toast(
|
||||
heading: __('Nicht genügend Credits'),
|
||||
text: __('Der Nachweis kostet :need Credits, Ihr Guthaben beträgt :have.', ['need' => $e->required, 'have' => $e->available]),
|
||||
variant: 'warning',
|
||||
duration: 8000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$pr = $this->getMyPR();
|
||||
|
|
@ -101,8 +160,18 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
|
||||
$company = $pr->company?->loadCount(['pressReleases', 'contacts']);
|
||||
|
||||
$boostService = app(BoostService::class);
|
||||
$proofService = app(ProofPdfService::class);
|
||||
|
||||
return [
|
||||
'pr' => $pr,
|
||||
'creditBalance' => $user->creditBalance(),
|
||||
'canBoost' => $boostService->canBoost($pr),
|
||||
'isBoosted' => $boostService->isBoosted($pr),
|
||||
'boostedUntil' => $boostService->activeUntil($pr),
|
||||
'boostOptions' => app(CreditPricingService::class)->boostOptions(),
|
||||
'proofPrice' => $proofService->priceCredits(),
|
||||
'proofPurchased' => $proofService->hasPurchased($user, $pr),
|
||||
'companyLogoUrl' => $company?->logoUrl(),
|
||||
'companyInitials' => $this->companyInitials($company?->name),
|
||||
'companyMetaLine' => $this->companyMetaLine($company),
|
||||
|
|
@ -349,6 +418,82 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</article>
|
||||
@endif
|
||||
|
||||
{{-- ============== ADD-ONS (Boost + Veröffentlichungsnachweis) ============== --}}
|
||||
@if ($pr->status === PressReleaseStatus::Published)
|
||||
<article class="panel">
|
||||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Add-ons') }}</span>
|
||||
<span class="badge">{{ __(':n Credits Guthaben', ['n' => $creditBalance]) }}</span>
|
||||
</div>
|
||||
<div class="p-5 grid gap-5 md:grid-cols-2">
|
||||
{{-- Boost --}}
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.rocket-launch class="size-[18px]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-[14px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Boost — Platzierung') }}</h3>
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
|
||||
{{ __('Hervorhebung auf Start- und Branchenseite.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($isBoosted)
|
||||
<div class="rounded-[6px] border border-[color:var(--color-ok)]/30 bg-[color:var(--color-ok-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-gain-deep)]">
|
||||
{{ __('Aktiv bis :date', ['date' => $boostedUntil?->copy()->setTimezone(\App\Models\PressRelease::DISPLAY_TIMEZONE)->format('d.m.Y H:i')]) }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($canBoost)
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($boostOptions as $days => $credits)
|
||||
<flux:button size="sm" variant="filled"
|
||||
wire:click="buyBoost({{ $days }})" wire:loading.attr="disabled">
|
||||
{{ __(':days Tage · :credits Credits', ['days' => $days, 'credits' => $credits]) }}
|
||||
</flux:button>
|
||||
@endforeach
|
||||
</div>
|
||||
@if ($isBoosted)
|
||||
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}</p>
|
||||
@endif
|
||||
@else
|
||||
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
|
||||
{{ __('Nur grün geprüfte Pressemitteilungen sind boostbar.') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Veröffentlichungsnachweis --}}
|
||||
<div class="space-y-3 md:border-l md:border-[color:var(--color-bg-rule)] md:pl-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0 bg-[color:var(--color-hub-soft)] text-[color:var(--color-hub)]">
|
||||
<flux:icon.document-check class="size-[18px]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-[14px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Veröffentlichungsnachweis') }}</h3>
|
||||
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
|
||||
{{ __('PDF mit Datum, Portal und URL — :credits Credits.', ['credits' => $proofPrice]) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($proofPurchased)
|
||||
<flux:button size="sm" variant="primary" icon="arrow-down-tray"
|
||||
href="{{ route('me.press-releases.proof', $pr->id) }}">
|
||||
{{ __('Nachweis herunterladen') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button size="sm" variant="primary" wire:click="buyProof" wire:loading.attr="disabled">
|
||||
{{ __('Für :credits Credits kaufen', ['credits' => $proofPrice]) }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if ($pr->status === PressReleaseStatus::Review)
|
||||
<article class="panel" style="border-color:var(--color-warn); border-left-width:3px; background:var(--color-warn-soft);">
|
||||
<div class="panel-head">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue