presseportale/resources/views/livewire/customer/press-releases/show.blade.php
Kevin Adametz 0474d22347 Boost-Slot-Grenzen (5/3) + Kategorie-Boost zum halben Satz
Entscheidung 13.07.: verkauft wird nur, was sichtbar ist — max. 5
gleichzeitige Boosts je Startseite, 3 je Branchenseite (= die sichtbaren
Plaetze; Hero-Slider damit vertagt). Neue Stufe Kategorie-Boost (nur
Branchenseite, 6/10/18 Credits = 50 % aufgerundet); der Startseiten-Boost
(12/20/35) erscheint weiter auf Start- UND Branchenseite.

- boosts.placement (home/category, Bestand = home) + BoostPlacement-Enum;
  Preise/Slots in config/credits.php (boost_category, boost_slots)
- BoostService::slotAvailability() je Ausgabe (Portal x Sprache, Kategorie
  zusaetzlich je Rubrik); ausgebucht -> soldOut-Exception mit fruehestem
  freien Termin; Verlaengerungen halten ihren Slot, Stufen verlaengern
  unabhaengig; Kategorie-Boost invalidiert nur Kategorie-Caches
- Startseite zeigt nur Home-Boosts; Branchenseite: Kategorie-Boosts mit
  Vorrang (ihre 3 Slots sind die Garantie), Home-Boosts fuellen den Rest
- Add-on-Panel der PM-Seite: zwei Boost-Angebote, bei vollem Inventar
  "Derzeit ausgebucht — frei ab" statt Kauf-Buttons (serverseitig gegated);
  Buchungsseiten-Preisliste um die Kategorie-Staffel ergaenzt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:28:03 +00:00

1269 lines
65 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\BoostPlacement;
use App\Enums\PressReleaseEventType;
use App\Enums\PressReleaseShareChannel;
use App\Enums\PressReleaseStatus;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
use App\Exceptions\BoostNotAllowedException;
use App\Exceptions\InsufficientCreditsException;
use App\Models\BookingConfirmation;
use App\Models\LegalDocument;
use App\Models\PressRelease;
use App\Models\SinglePurchase;
use App\Services\Auth\MagicLinkGenerator;
use App\Services\Billing\CreditPricingService;
use App\Services\Billing\ProofPdfService;
use App\Services\PressRelease\AiDeclarationRequiredException;
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;
use Flux\Flux;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component
{
use \App\Livewire\Concerns\InteractsWithAiDeclaration;
#[Locked]
public int $id;
public ?string $shareUrl = null;
public ?string $shareExpiresAt = null;
public bool $depublicationConfirmed = false;
/**
* Kontext fürs „Nicht genügend Credits"-Modal (QA-Befund 07.07.):
* ['label' => Add-on-Name, 'required' => int, 'available' => int].
*
* @var array{label: string, required: int, available: int}|null
*/
public ?array $creditShortfall = null;
/**
* Kontext fürs Kauf-Bestätigungs-Modal (Entscheidung 10.07.): Add-ons
* buchen erst nach explizitem „Kostenpflichtig buchen", damit ein Klick
* auf eine Laufzeit nicht sofort Credits abbucht. Läuft bereits ein
* Boost derselben Stufe, tragen extendsUntil/newEndsAt den
* Verlängerungs-Hinweis.
*
* @var array{type: 'boost'|'proof', placement: string|null, label: string, credits: int, days: int|null, extendsUntil: string|null, newEndsAt: string|null}|null
*/
public ?array $pendingPurchase = null;
/**
* Pflicht-Schalter im Kauf-Modal: ohne aktive Bestätigung führt
* buyPendingAddOn keine Abbuchung aus.
*/
public bool $purchaseConfirmed = false;
public function mount(int $id): void
{
$this->id = $id;
$pr = $this->getMyPR();
$this->authorize('view', $pr);
// KI-Grad vorbelegen (bereits deklariert), gemappt auf die zwei
// Formular-Optionen; die verbindliche Bestätigung muss bei jedem
// Submit erneut gesetzt werden.
$this->aiTextDeclaration = $pr->ai_text_declaration?->formOption()->value;
}
public function submitForReview(): void
{
$pr = $this->getMyPR();
$this->authorize('submitForReview', $pr);
// KI-Kennzeichnung (Art. 50 KI-VO): Pflicht-Deklaration aus dem
// Submit-Modal validieren und vor dem Einreichen persistieren.
$this->validateAiDeclaration();
$pr->update($this->aiDeclarationAttributes());
try {
app(PressReleaseService::class)->submitForReview($pr);
} catch (BlacklistViolationException $e) {
Flux::modal('confirm-submit-review')->close();
Flux::toast(
heading: __('Automatisch abgelehnt'),
text: __('Unzulässiges Wort gefunden: ":word". Bitte überarbeiten.', ['word' => $e->word]),
variant: 'danger',
duration: 8000,
);
return;
} catch (AiDeclarationRequiredException|BookingRequiredException|QuotaExceededException $e) {
Flux::modal('confirm-submit-review')->close();
Flux::toast(
heading: __('Nicht eingereicht'),
text: $e->getMessage(),
variant: 'warning',
duration: 8000,
);
return;
}
Flux::modal('confirm-submit-review')->close();
Flux::toast(
heading: __('Eingereicht'),
text: __('Die automatische Inhaltsprüfung ist in der Regel innerhalb weniger Minuten abgeschlossen.'),
variant: 'success',
);
}
public function generateShareLink(MagicLinkGenerator $generator): void
{
$pr = $this->getMyPR();
$this->authorize('view', $pr);
$share = $generator->createPressReleaseShareLink($pr, auth()->user());
$this->shareUrl = $share['url'];
$this->shareExpiresAt = $share['expires_at']->format('d.m.Y H:i');
Flux::toast(text: __('Vorschau-Link wurde erzeugt.'), variant: 'success');
}
/**
* Kauf-Bestätigung für einen Boost öffnen (Abbuchung erst in
* buyPendingAddOn). Stufe: Startseiten-Boost oder Kategorie-Boost.
*/
public function confirmBoost(int $days, string $placement = 'home'): void
{
$placement = BoostPlacement::tryFrom($placement);
if (! $placement) {
return;
}
$options = app(CreditPricingService::class)->boostOptions($placement);
if (! isset($options[$days])) {
return;
}
// Läuft bereits ein Boost dieser Stufe, verlängert die Buchung dessen
// Laufzeit — das Modal weist ausdrücklich darauf hin (10.07.).
$activeUntil = app(BoostService::class)->activeUntil($this->getMyPR(), $placement);
$this->purchaseConfirmed = false;
$this->pendingPurchase = [
'type' => 'boost',
'placement' => $placement->value,
'label' => __(':placement — :days Tage', ['placement' => $placement->label(), 'days' => $days]),
'credits' => $options[$days],
'days' => $days,
'extendsUntil' => $activeUntil?->format('d.m.Y H:i'),
'newEndsAt' => $activeUntil?->copy()->addDays($days)->format('d.m.Y H:i'),
];
Flux::modal('confirm-addon-purchase')->show();
}
/**
* Kauf-Bestätigung für den Veröffentlichungsnachweis öffnen.
*/
public function confirmProof(): void
{
$this->purchaseConfirmed = false;
$this->pendingPurchase = [
'type' => 'proof',
'placement' => null,
'label' => __('Veröffentlichungsnachweis (PDF)'),
'credits' => app(ProofPdfService::class)->priceCredits(),
'days' => null,
'extendsUntil' => null,
'newEndsAt' => null,
];
Flux::modal('confirm-addon-purchase')->show();
}
/**
* Bestätigten Add-on-Kauf ausführen — der einzige Weg, auf dem die
* Buttons der Add-on-Sektion tatsächlich Credits abbuchen.
*/
public function buyPendingAddOn(): void
{
// Serverseitige Absicherung des Pflicht-Schalters (der Button ist
// clientseitig zusätzlich deaktiviert).
if ($this->pendingPurchase !== null && ! $this->purchaseConfirmed) {
Flux::toast(
text: __('Bitte bestätigen Sie die kostenpflichtige Buchung über den Schalter.'),
variant: 'warning',
);
return;
}
$pending = $this->pendingPurchase;
$this->pendingPurchase = null;
$this->purchaseConfirmed = false;
Flux::modal('confirm-addon-purchase')->close();
if ($pending === null) {
return;
}
if ($pending['type'] === 'boost') {
$this->buyBoost($pending['days'], $pending['placement'] ?? 'home');
return;
}
$this->buyProof();
}
/**
* Boost (Platzierung) aus der Wallet buchen — nur veröffentlichte, grüne
* PMs sind boostbar; ausgebuchte Slots lehnt der BoostService ab (Gate).
*/
public function buyBoost(int $days, string $placement = 'home'): void
{
$placement = BoostPlacement::tryFrom($placement);
if (! $placement) {
return;
}
$pr = $this->getMyPR();
$this->authorize('view', $pr);
try {
app(BoostService::class)->boost(auth()->user(), $pr, $days, $placement);
Flux::toast(
heading: __(':placement gebucht', ['placement' => $placement->label()]),
text: $placement === BoostPlacement::Home
? __('Ihre Pressemitteilung wird jetzt auf Start- und Branchenseite hervorgehoben.')
: __('Ihre Pressemitteilung wird jetzt auf der Branchenseite ihrer Rubrik hervorgehoben.'),
variant: 'success',
);
} catch (InsufficientCreditsException $e) {
$this->showCreditShortfallModal(__('Der Boost'), $e);
} 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) {
$this->showCreditShortfallModal(__('Der Veröffentlichungsnachweis'), $e);
}
}
/**
* Statt eines flüchtigen Toasts ein Modal mit Aufladen-Link öffnen
* (QA-Befund 07.07.): der Kunde soll direkt zur Wallet finden.
*/
private function showCreditShortfallModal(string $label, InsufficientCreditsException $e): void
{
$this->creditShortfall = [
'label' => $label,
'required' => $e->required,
'available' => $e->available,
];
Flux::modal('insufficient-credits')->show();
}
/**
* Unternehmer-Pflicht-Checkbox vor dem Depublikations-Checkout (T1neu):
* protokolliert wie im Buchungs-Modal — der Checkout-Controller verlangt
* eine zeitnahe Bestätigung zusätzlich serverseitig.
*/
public function updatedDepublicationConfirmed(bool $value): void
{
if (! $value) {
return;
}
$agb = LegalDocument::current(LegalDocument::TYPE_AGB);
BookingConfirmation::query()->create([
'user_id' => auth()->id(),
'booking_type' => 'depublication',
'booking_ref' => (string) $this->id,
'legal_document_id' => $agb?->id,
'agb_version' => $agb?->version ?? (string) config('legal.agb_version'),
'ip' => request()->ip(),
'confirmed_at' => now(),
]);
}
/**
* Anzeige-Daten der beiden Boost-Stufen (Startseiten-/Kategorie-Boost)
* fürs Add-on-Panel: Preise, laufende Platzierung, Slot-Verfügbarkeit
* („Derzeit ausgebucht — frei ab …", Entscheidung 13.07.).
*
* @return array<string, array{label: string, description: string, options: array<int, int>, isBoosted: bool, activeUntil: \Illuminate\Support\Carbon|null, available: bool, freeAt: \Illuminate\Support\Carbon|null}>
*/
private function boostAddOns(PressRelease $pr, BoostService $boostService): array
{
$pricing = app(CreditPricingService::class);
$canBoost = $boostService->canBoost($pr);
$addOns = [];
foreach (BoostPlacement::cases() as $placement) {
// Slot-Abfrage nur, wenn die PM überhaupt boostbar ist.
$availability = $canBoost
? $boostService->slotAvailability($pr, $placement)
: ['available' => false, 'freeAt' => null];
$addOns[$placement->value] = [
'label' => $placement->label(),
'description' => $placement === BoostPlacement::Home
? __('Hervorhebung auf Start- und Branchenseite.')
: __('Hervorhebung auf der Branchenseite Ihrer Rubrik.'),
'options' => $pricing->boostOptions($placement),
'isBoosted' => $boostService->isBoosted($pr, $placement),
'activeUntil' => $boostService->activeUntil($pr, $placement),
'available' => $availability['available'],
'freeAt' => $availability['freeAt'],
];
}
return $addOns;
}
public function with(): array
{
$pr = $this->getMyPR();
$this->authorize('view', $pr);
$categoryName = $pr->category?->translations->firstWhere('locale', 'de')?->name ?? '–';
$latestRejection = null;
if ($pr->status->value === 'rejected') {
$latestRejection = $pr->statusLogs
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
}
$cover = app(PressReleaseCoverImage::class);
$user = auth()->user();
$company = $pr->company;
$boostService = app(BoostService::class);
$proofService = app(ProofPdfService::class);
// Share-Klicks der öffentlichen Detailseite, je Kanal aggregiert.
$shareCounts = $pr->events()
->where('type', PressReleaseEventType::Share)
->selectRaw('channel, count(*) as clicks')
->groupBy('channel')
->pluck('clicks', 'channel');
return [
'pr' => $pr,
'publicLinks' => $pr->status === PressReleaseStatus::Published
? $pr->publicPortalLinks()
: [],
'creditBalance' => $user->creditBalance(),
'canBoost' => $boostService->canBoost($pr),
'boostAddOns' => $this->boostAddOns($pr, $boostService),
'proofPrice' => $proofService->priceCredits(),
'proofPurchased' => $proofService->hasPurchased($user, $pr),
'companyLogoUrl' => $company?->logoUrl(),
'companyInitials' => $this->companyInitials($company?->name),
'companyMetaLine' => $this->companyMetaLine($company),
'categoryName' => $categoryName,
'coverUrl' => $cover->coverUrl($pr, 'cover'),
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
'titleImage' => $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first(),
'quotaTotal' => $user->pressReleaseQuotaTotal(),
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
'canEdit' => auth()->user()->can('update', $pr)
&& in_array($pr->status->value, ['draft', 'rejected']),
// Einreichen zur Prüfung ist hinter eine bezahlte Buchung gegated
// (Decision-Update §5.1, Schalter billing.enforce_booking).
'hasActiveBooking' => $user->hasActiveBooking(),
'latestRejection' => $latestRejection,
'shareTotal' => (int) $shareCounts->sum(),
'shareBreakdown' => $shareCounts
->sortDesc()
->map(fn (int $clicks, string $channel) => PressReleaseShareChannel::from($channel)->label().' '.number_format($clicks, 0, ',', '.'))
->implode(' · '),
'contacts' => $pr->contacts,
'statusLogs' => $pr->statusLogs,
// T12: Depublikation ist config-gated (ohne Preis kein Angebot).
'depublicationPriceCents' => (int) config('billing.depublication_price_cents'),
// Nur ein BEZAHLTER Vorgang blendet das Angebot aus — ein
// abgebrochener Checkout (Pending) wird beim nächsten Klick
// wiederverwendet (CheckoutController::depublication).
'depublicationRequested' => SinglePurchase::query()
->where('press_release_id', $pr->id)
->where('type', SinglePurchaseType::Depublication->value)
->where('status', SinglePurchaseStatus::Paid->value)
->exists(),
];
}
/**
* Initialen für die Logo-Kachel (analog zur Firmenübersicht).
*/
private function companyInitials(?string $name): string
{
$name = trim((string) $name);
if ($name === '') {
return '–';
}
$letters = collect(preg_split('/\s+/u', $name) ?: [])
->map(fn (string $word): string => mb_substr($word, 0, 1))
->filter()
->take(2)
->implode('');
return mb_strtoupper($letters ?: mb_substr($name, 0, 2));
}
/**
* Kompakte Meta-Zeile: Ort (letzte Adresszeile) · Firmentyp.
*/
private function companyMetaLine(?\App\Models\Company $company): string
{
if (! $company) {
return '';
}
$parts = [];
$lastAddressLine = collect(preg_split('/\r?\n/', trim((string) $company->address)) ?: [])
->map(fn ($line) => trim((string) $line))
->filter()
->last();
if (is_string($lastAddressLine) && $lastAddressLine !== '') {
$parts[] = $lastAddressLine;
}
if ($company->type?->label()) {
$parts[] = $company->type->label();
}
return implode(' · ', $parts);
}
private ?PressRelease $cachedPressRelease = null;
private function getMyPR(): PressRelease
{
// Pro Livewire-Request memoisiert: mount(), with() und die Add-on-
// Aktionen rufen getMyPR() sonst jeweils mit der vollen Eager-Load-
// Query erneut auf. Firmen-Kennzahlen direkt mit eager laden, statt
// sie nachträglich per loadCount() nachzuziehen.
return $this->cachedPressRelease ??= PressRelease::accessibleBy(auth()->user())
->with([
'company' => fn ($query) => $query
->select(['id', 'name', 'email', 'phone', 'address', 'portal', 'logo_path', 'legacy_portal', 'is_active', 'type'])
->withCount(['pressReleases', 'contacts']),
'category.translations',
'contacts' => fn ($query) => $query
->withoutGlobalScopes()
->orderBy('last_name')
->orderBy('first_name')
->select(['contacts.id', 'contacts.company_id', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']),
'statusLogs.changedBy:id,name,email',
])
->findOrFail($this->id);
}
}; ?>
<div class="space-y-8">
@php
$statusClass = $pr->status->badgeClass();
@endphp
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
{{-- ============== PAGE HEADER ============== --}}
<header class="page-header">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('User Backend') }}</span>
<span class="eyebrow muted">{{ __('Mein Bereich · Pressemitteilung') }}</span>
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
@if ($pr->content_tier?->isPubliclyBadged())
<span class="badge {{ $pr->content_tier === \App\Enums\PressReleaseContentTier::Hochwertig ? 'ok' : 'hub' }}">
{{ $pr->content_tier === \App\Enums\PressReleaseContentTier::Hochwertig ? '★ ' : '✓ ' }}{{ $pr->content_tier->label() }}
</span>
@endif
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ $pr->title }}
</h1>
@if ($pr->subtitle)
<p class="text-[18px] font-medium tracking-[-0.2px] leading-[1.35] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ $pr->subtitle }}
</p>
@endif
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
{{ $pr->company?->name ?? '–' }}
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
{{ $categoryName }}
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
{{ $pr->created_at->format('d.m.Y') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0 flex-wrap justify-end">
{{-- Direktlinks zur öffentlichen Seite (QA-Befund 07.07.):
bei Portal::Both je Portal ein eigener Button. --}}
@foreach ($publicLinks as $link)
<flux:button variant="primary" icon="arrow-top-right-on-square"
href="{{ $link['url'] }}" target="_blank" rel="noopener">
{{ count($publicLinks) > 1
? __('Auf :portal ansehen', ['portal' => $link['label']])
: __('Öffentlich ansehen') }}
</flux:button>
@endforeach
@if ($canEdit)
<flux:button variant="filled" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@endif
<flux:button variant="filled" icon="link" wire:click="generateShareLink">
{{ __('Vorschau-Link') }}
</flux:button>
<flux:button variant="filled" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
{{ __('Zurück') }}
</flux:button>
</div>
</header>
{{-- ============== SHARE-LINK ERFOLG ============== --}}
@if ($shareUrl)
<article class="panel" style="border-color:var(--color-ok);">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Öffentlicher Vorschau-Link erstellt') }}</span>
<span class="badge ok dot">{{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}</span>
</div>
<div class="p-5 space-y-3">
<flux:input readonly copyable :value="$shareUrl" />
<div class="flex flex-wrap items-center gap-2">
@if ($pr->portal === \App\Enums\Portal::Both)
{{-- PM erscheint auf beiden Portalen: Vorschau je Portal öffnen --}}
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}?portal=presseecho" target="_blank" rel="noopener">
{{ __('Vorschau presseecho') }}
</flux:button>
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}?portal=businessportal24" target="_blank" rel="noopener">
{{ __('Vorschau businessportal24') }}
</flux:button>
@else
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}" target="_blank" rel="noopener">
{{ __('Vorschau öffnen') }}
</flux:button>
@endif
</div>
</div>
</article>
@endif
{{-- ============== REJECTION-HINWEIS ============== --}}
@if ($pr->status === PressReleaseStatus::Rejected && $latestRejection)
<article class="panel" style="border-color:var(--color-err); border-left-width:3px;">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Diese Pressemitteilung wurde abgelehnt') }}</span>
<span class="badge err dot">{{ __('Handlung erforderlich') }}</span>
</div>
<div class="p-5 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-err-soft)] border border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
<flux:icon.exclamation-triangle class="size-[18px]" />
</div>
<div class="flex-1 text-[13px] text-[color:var(--color-ink-2)]">
@if ($latestRejection->reason)
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Begründung') }}:</strong>
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
@else
{{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }}
@endif
<span class="mt-2 block text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
</span>
</div>
</div>
</article>
@endif
{{-- ============== STATUS-WORKFLOW (oben, farblich abgehoben) ============== --}}
@if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
<article class="panel"
style="border-color:var(--color-{{ $pr->status === PressReleaseStatus::Rejected ? 'err' : 'hub' }}); border-left-width:3px;
background:var(--color-{{ $pr->status === PressReleaseStatus::Rejected ? 'err' : 'hub' }}-soft);">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
<span @class(['badge', 'dot', $pr->status === PressReleaseStatus::Rejected ? 'err' : 'hub'])>
{{ $pr->status === PressReleaseStatus::Rejected ? __('Überarbeiten') : __('Entwurf') }}
</span>
</div>
<div class="p-5 space-y-4">
@unless ($hasActiveBooking)
{{-- Veröffentlichungs-Gate: Einreichen zur Prüfung setzt eine
bezahlte Option voraus. Speichern als Entwurf bleibt frei. --}}
<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.lock-closed 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)]">{{ __('Noch keine Buchung.') }}</span>
{{ __('Den Entwurf können Sie frei speichern. Zum Einreichen und Veröffentlichen benötigen Sie eine bezahlte Option (Tarif, Einzel-PM oder Extra-PM aus Guthaben).') }}
</div>
</div>
@endunless
<div class="flex flex-wrap items-center gap-3">
<p class="text-[13px] text-[color:var(--color-ink-2)] m-0 flex-1 min-w-[220px]">
{{ $pr->status === PressReleaseStatus::Rejected
? __('Sie können den Text bearbeiten und erneut zur Prüfung einreichen.')
: __('Reichen Sie den Entwurf ein, sobald er vollständig ist.') }}
</p>
<div class="flex items-center gap-2 flex-shrink-0 flex-wrap">
@if ($canEdit)
<flux:button variant="filled" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
{{ __('Bearbeiten') }}
</flux:button>
@endif
@if ($hasActiveBooking)
<flux:modal.trigger name="confirm-submit-review">
<flux:button type="button" variant="primary">
{{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }}
</flux:button>
</flux:modal.trigger>
@else
<flux:button variant="primary" icon="credit-card" href="{{ route('me.bookings.index') }}" wire:navigate>
{{ __('Buchung wählen') }}
</flux:button>
@endif
</div>
</div>
</div>
</article>
@endif
@if ($pr->status === PressReleaseStatus::Published)
<article class="panel" style="border-color:var(--color-ok); border-left-width:3px; background:var(--color-ok-soft);">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
<span class="badge ok dot">{{ __('Live') }}</span>
</div>
<div class="p-5 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-ok-soft)] border border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
<flux:icon.check-circle class="size-[18px]" />
</div>
<p class="flex-1 text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Diese Pressemitteilung ist veröffentlicht (seit :date).', ['date' => $pr->published_at?->format('d.m.Y H:i') ?? '–']) }}
</p>
</div>
</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-3">
{{-- Boost-Stufen: Startseiten-Boost + Kategorie-Boost (13.07.) --}}
@foreach ($boostAddOns as $placementValue => $addOn)
<div class="space-y-3 @if (! $loop->first) md:border-l md:border-[color:var(--color-bg-rule)] md:pl-5 @endif">
<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)]">
@if ($placementValue === 'home')
<flux:icon.rocket-launch class="size-[18px]" />
@else
<flux:icon.tag class="size-[18px]" />
@endif
</div>
<div class="min-w-0">
<h3 class="text-[14px] font-bold text-[color:var(--color-ink)] m-0">{{ $addOn['label'] }}</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ $addOn['description'] }}
</p>
</div>
</div>
@if ($addOn['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' => $addOn['activeUntil']?->format('d.m.Y H:i')]) }}
</div>
@endif
@if ($canBoost && $addOn['available'])
<div class="flex flex-wrap gap-2">
@foreach ($addOn['options'] as $days => $credits)
<flux:button size="sm" variant="primary"
wire:click="confirmBoost({{ $days }}, '{{ $placementValue }}')" wire:loading.attr="disabled">
{{ __(':days Tage für :credits Credits buchen', ['days' => $days, 'credits' => $credits]) }}
</flux:button>
@endforeach
</div>
@if ($addOn['isBoosted'])
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}</p>
@endif
@elseif ($canBoost)
{{-- Slot-Inventar erschöpft: ehrlich ausweisen statt verkaufen (13.07.). --}}
<div class="rounded-[6px] border border-[color:var(--color-warn)]/40 bg-[color:var(--color-warn-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-ink)]">
@if ($addOn['freeAt'])
{{ __('Derzeit ausgebucht — der nächste Platz wird am :date frei.', ['date' => $addOn['freeAt']->format('d.m.Y')]) }}
@else
{{ __('Derzeit ausgebucht.') }}
@endif
</div>
@else
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Nur grün geprüfte Pressemitteilungen sind boostbar.') }}
</p>
@endif
</div>
@endforeach
{{-- 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="confirmProof" 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">
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
<span class="badge warn dot">{{ __('Geduld bitte') }}</span>
</div>
<div class="p-5 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-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
<flux:icon.clock class="size-[18px]" />
</div>
<p class="flex-1 text-[13px] text-[color:var(--color-ink-2)] m-0">
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
</p>
</div>
</article>
@endif
{{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
<div class="grid gap-6 xl:grid-cols-2">
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Firma & Pressekontakt') }}</span>
</div>
<div class="p-5 space-y-4">
{{-- Firma der Pressemitteilung — gleiche Kachel wie in der Firmenübersicht --}}
@if ($pr->company)
<div class="firm-card">
<div class="flex items-start justify-between gap-3">
<div class="logo {{ $companyLogoUrl ? '' : 'lg-'.['brew', 'mv', 'soft', 'warm'][$pr->company->id % 4] }}">
@if ($companyLogoUrl)
<img src="{{ $companyLogoUrl }}" alt="{{ $pr->company->name }}" loading="lazy" />
@else
{{ $companyInitials }}
@endif
</div>
@if ($pr->company->is_active)
<span class="badge ok dot">{{ __('Aktiv') }}</span>
@else
<span class="badge err dot">{{ __('Inaktiv') }}</span>
@endif
</div>
<div class="min-w-0">
<h3 class="name">{{ $pr->company->name }}</h3>
@if (filled($companyMetaLine))
<div class="meta-line">{{ $companyMetaLine }}</div>
@endif
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($pr->company->email)
<span>{{ $pr->company->email }}</span>
@endif
@if ($pr->company->phone)
<span>{{ $pr->company->phone }}</span>
@endif
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
@if ($pr->company->portal === \App\Enums\Portal::Both)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@elseif ($pr->company->portal === \App\Enums\Portal::Presseecho)
<span class="portal-pill pe"><span class="pdot"></span>presseecho</span>
@elseif ($pr->company->portal === \App\Enums\Portal::Businessportal24)
<span class="portal-pill bp"><span class="pdot"></span>businessportal24</span>
@endif
</div>
<div class="kpis">
<div class="kpi">
<span class="k">{{ number_format($pr->company->press_releases_count ?? 0, 0, ',', '.') }}</span>
<span class="l">{{ __('PMs') }}</span>
</div>
<div class="kpi">
<span class="k">{{ number_format($pr->company->contacts_count ?? 0, 0, ',', '.') }}</span>
<span class="l">{{ __('Kontakte') }}</span>
</div>
</div>
<div class="flex items-center gap-2 pt-1">
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate class="card-action primary" style="flex:1;">
<flux:icon.arrow-right class="size-3" />
{{ __('Firma öffnen') }}
</a>
</div>
</div>
@endif
{{-- Pressekontakt(e) direkt darunter --}}
<div class="space-y-2">
@forelse ($contacts as $contact)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
{{ $contact->displayName() }}
</div>
<div class="text-[12px] text-[color:var(--color-ink-3)]">
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
</div>
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($contact->email)
<a href="mailto:{{ $contact->email }}"
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $contact->email }}
</a>
@endif
@if ($contact->phone)
<span>{{ $contact->phone }}</span>
@endif
</div>
</div>
@empty
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }}
@if ($pr->company)
<a href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate
class="font-medium text-[color:var(--color-hub)] hover:underline">
{{ __('Kontakte in der Firma prüfen.') }}
</a>
@endif
</div>
@endforelse
</div>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status & Verlauf') }}</span>
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
</div>
<div class="p-5">
<div class="grid gap-2 sm:grid-cols-2">
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aktueller Status') }}</div>
<div class="mt-1.5">
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Erstellt') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Veröffentlicht') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Aufrufe') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ number_format($pr->hits, 0, ',', '.') }}
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Geteilt') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ number_format($shareTotal, 0, ',', '.') }}
</div>
@if ($shareTotal > 0)
<div class="text-[11px] text-[color:var(--color-ink-3)] mt-0.5">{{ $shareBreakdown }}</div>
@endif
</div>
@if ($pr->scheduled_at)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Geplante Veröffentlichung') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ $pr->scheduledAtLocal()->format('d.m.Y H:i') }}
</div>
</div>
@endif
@if ($pr->embargo_at)
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Sperrfrist bis') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ $pr->embargoAtLocal()->format('d.m.Y H:i') }}
</div>
</div>
@endif
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Portal') }}</div>
<div class="mt-1 flex items-center gap-2">
<span class="text-[13px] font-semibold text-[color:var(--color-ink)]">{{ $pr->portal?->label() ?? '–' }}</span>
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
</div>
</div>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Kategorie') }}</div>
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
{{ $categoryName }}
</div>
</div>
</div>
@if (filled($pr->keywords))
<div class="mt-3 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-2">{{ __('Themen') }}</div>
<div class="flex flex-wrap gap-1.5">
@foreach (array_filter(array_map('trim', explode(',', $pr->keywords))) as $keyword)
<span class="badge hub">{{ $keyword }}</span>
@endforeach
</div>
</div>
@endif
@if ($pr->backlink_url)
<div class="mt-3 rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Backlink') }}</div>
<a href="{{ $pr->backlink_url }}" target="_blank"
class="text-[12.5px] break-all text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
{{ $pr->backlink_url }}
</a>
</div>
@endif
@if ($pr->no_export)
<div class="mt-3 flex items-center gap-2 text-[12px] text-[color:var(--color-ink-3)]">
<flux:icon.no-symbol variant="micro" class="size-3.5" />
<span>{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}</span>
</div>
@endif
<div class="my-4 border-t border-[color:var(--color-bg-rule)]"></div>
@if ($statusLogs->isNotEmpty())
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
@foreach ($statusLogs as $log)
<li class="text-[12.5px]">
<div class="flex flex-wrap items-center gap-2">
@php
$logClass = $log->to_status?->badgeClass() ?? 'muted';
@endphp
<span @class(['badge', $logClass])>{{ $log->to_status?->label() }}</span>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ $log->created_at->format('d.m.Y H:i') }}
</span>
@if ($log->changedBy)
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('durch :name', ['name' => $log->changedBy->name]) }}
</span>
@endif
</div>
@if ($log->reason)
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0">{{ $log->reason }}</p>
@endif
</li>
@endforeach
</ol>
@else
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Noch keine Statusänderungen protokolliert.') }}
</p>
@endif
</div>
</article>
</div>
{{-- ============== TITELBILD (Hero) ============== --}}
{{-- Harte Obergrenze 1280x580 px: Container deckelt Breite und Seitenverhältnis,
damit das Bild auf großen Screens nicht über die Detailgröße hinauswächst. --}}
<article class="panel overflow-hidden mx-auto w-full max-w-[1280px]">
<div class="relative aspect-[1280/580] w-full">
<img src="{{ $coverUrl }}" alt="{{ $pr->title }}"
class="absolute inset-0 h-full w-full object-cover" loading="lazy" />
</div>
@if ($coverIsPlaceholder)
<div class="flex items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
<flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
</div>
@elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated || $titleImage->ai_declaration?->publicLabel()))
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act): kurzes
Badge („KI-generiert" / „KI-bearbeitet"), kein Satz-Wortlaut. --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($aiImageBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null))
<span class="badge hub">{{ $aiImageBadge }}</span>
@endif
@if ($titleImage->copyright)
<span>{{ $titleImage->copyright }}</span>
@endif
</div>
@endif
</article>
{{-- ============== INHALT ============== --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
</div>
<div class="p-5">
{{-- KI-Label (Art. 50 KI-VO): erscheint so auch in der
Veröffentlichung — am Anfang des Inhalts, in Einreicher-Stimme. --}}
<x-portal.ai-public-label :label="$pr->aiTextPublicLabel()" class="mb-4 max-w-[760px]" />
<div class="pr-content max-w-[760px]">
{!! $pr->renderedText() !!}
</div>
</div>
</article>
{{-- ============== VERÖFFENTLICHUNGS-MODAL ============== --}}
@if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
<x-press-release-submit-modal
name="confirm-submit-review"
action="submitForReview"
:confirm-label="__('Veröffentlichung anfordern')"
:quota-total="$quotaTotal"
:quota-remaining="$quotaRemaining"
:has-title-image="! $coverIsPlaceholder" />
@endif
{{-- ============== BOILERPLATE-OVERRIDE ============== --}}
@if ($pr->boilerplate_override)
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Eigener Abbinder (Boilerplate)') }}</span>
<span class="badge hub">{{ __('Override') }}</span>
</div>
<div class="p-5">
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-3">
{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}
</p>
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[13px] leading-[1.6] text-[color:var(--color-ink-2)] whitespace-pre-line">
{{ $pr->boilerplate_override }}
</div>
</div>
</article>
@endif
{{-- ============== DEPUBLIKATION (T12, Meldeverfahren Weg 3) ==============
Bewusst ganz unten (QA-Befund 07.07.): seltener Ausnahme-Service,
soll nicht zwischen Add-ons und Inhalt um Aufmerksamkeit konkurrieren. --}}
@if ($pr->status === PressReleaseStatus::Published && $depublicationPriceCents > 0)
@php
$depubNet = number_format($depublicationPriceCents / 100, 2, ',', '.');
$depubGross = number_format($depublicationPriceCents * (1 + (float) config('billing.vat_rate')) / 100, 2, ',', '.');
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Depublikation (Service)') }}</span>
</div>
<div class="p-5 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.eye-slash class="size-[18px]" />
</div>
<div class="min-w-0">
<h3 class="text-[14px] font-bold text-[color:var(--color-ink)] m-0">{{ __('Aus der öffentlichen Ansicht entfernen') }}</h3>
<p class="text-[12.5px] text-[color:var(--color-ink-2)] mt-1 mb-0">
{{ __('Freiwillige Entfernung dieser veröffentlichten Mitteilung — die öffentliche URL zeigt danach dauerhaft einen Entfernt-Hinweis. Kopien in Suchmaschinen oder Drittarchiven können zeitweise weiter verfügbar sein. Preis: :net € zzgl. USt. (:gross € inkl. 19 % USt.)', ['net' => $depubNet, 'gross' => $depubGross]) }}
</p>
</div>
</div>
@if ($depublicationRequested)
<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)]">
{{ __('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.') }}
</div>
@else
<div class="space-y-2">
<flux:switch
align="left"
wire:model.live="depublicationConfirmed"
:label="__('Ich bestelle in Ausübung meiner gewerblichen oder selbständigen beruflichen Tätigkeit (§ 14 BGB) und akzeptiere die AGB.')"
/>
<p class="text-[11.5px] text-[color:var(--color-ink-3)] m-0">
<a href="{{ route('agb', ['edition' => 'de']) }}" target="_blank" rel="noopener" class="underline underline-offset-2 hover:text-[color:var(--color-ink)]">
{{ __('AGB (Version :version)', ['version' => \App\Models\LegalDocument::currentAgbVersion()]) }}
</a>
· {{ __('Gesetzliche Ansprüche (z. B. DSGVO) bleiben davon unberührt und sind immer kostenlos.') }}
</p>
</div>
@if ($depublicationConfirmed)
<flux:button size="sm" variant="primary" icon="lock-closed" href="{{ route('me.checkout.depublication', $pr->id) }}">
{{ __('Kostenpflichtig depublizieren') }}
</flux:button>
@else
<flux:button size="sm" variant="primary" icon="lock-closed" disabled>
{{ __('Kostenpflichtig depublizieren') }}
</flux:button>
@endif
@endif
</div>
</article>
@endif
{{-- ============== MODAL: ADD-ON-KAUF BESTÄTIGEN ==============
Vorgeschaltete Bestätigung (Entscheidung 10.07.): kein Add-on-Klick
bucht direkt Credits ab — erst „Kostenpflichtig buchen" kauft. --}}
<flux:modal name="confirm-addon-purchase" class="w-full max-w-md">
<div class="space-y-5">
<div>
<flux:text class="eyebrow muted">{{ __('Add-on') }}</flux:text>
<flux:heading size="lg">{{ __('Kauf bestätigen') }}</flux:heading>
</div>
@if ($pendingPurchase)
@if ($pendingPurchase['extendsUntil'])
{{-- Verlängerungs-Hinweis: für diese PM läuft bereits ein Boost. --}}
<div class="rounded-[6px] border-l-[3px] border border-[color:var(--color-warn)] bg-[color:var(--color-warn-soft)] p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink)]">
<p class="m-0 font-bold">{{ __('Diese Buchung verlängert den laufenden Boost.') }}</p>
<p class="m-0 mt-1 text-[color:var(--color-ink-2)]">
{{ __('Aktuell aktiv bis :until — die neue Laufzeit schließt daran an und endet am :end.', [
'until' => $pendingPurchase['extendsUntil'],
'end' => $pendingPurchase['newEndsAt'],
]) }}
</p>
</div>
@endif
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
<div class="flex items-center justify-between gap-3">
<span>{{ $pendingPurchase['label'] }}</span>
<span class="font-bold whitespace-nowrap text-[color:var(--color-ink)]">{{ __(':n Credits', ['n' => $pendingPurchase['credits']]) }}</span>
</div>
<p class="m-0 mt-3 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-3)]">
@if ($creditBalance >= $pendingPurchase['credits'])
{{ __('Der Betrag wird sofort von Ihrem Guthaben abgebucht: :available Credits → :rest Credits.', [
'available' => $creditBalance,
'rest' => $creditBalance - $pendingPurchase['credits'],
]) }}
@else
{{ __('Ihr Guthaben (:available Credits) reicht dafür nicht aus — es fehlen :missing Credits.', [
'available' => $creditBalance,
'missing' => $pendingPurchase['credits'] - $creditBalance,
]) }}
@endif
</p>
</div>
<flux:switch wire:model.live="purchaseConfirmed"
label="{{ __('Ich bestätige die kostenpflichtige Buchung über :n Credits.', ['n' => $pendingPurchase['credits']]) }}" />
@endif
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="buyPendingAddOn" wire:loading.attr="disabled"
:disabled="! $purchaseConfirmed">
{{ __('Kostenpflichtig buchen') }}
</flux:button>
</div>
</div>
</flux:modal>
{{-- ============== MODAL: NICHT GENÜGEND CREDITS ==============
Öffnet sich serverseitig, wenn ein Add-on-Kauf (Boost/Nachweis)
am Wallet-Guthaben scheitert — mit Direktlink zum Aufladen. --}}
<flux:modal name="insufficient-credits" class="w-full max-w-md">
<div class="space-y-5">
<div>
<flux:text class="eyebrow muted">{{ __('Credit-Wallet') }}</flux:text>
<flux:heading size="lg">{{ __('Nicht genügend Credits') }}</flux:heading>
</div>
@if ($creditShortfall)
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
<p class="m-0">
{{ __(':label kostet :required Credits — Ihr Guthaben beträgt :available Credits. Es fehlen :missing Credits.', [
'label' => $creditShortfall['label'],
'required' => $creditShortfall['required'],
'available' => $creditShortfall['available'],
'missing' => max(0, $creditShortfall['required'] - $creditShortfall['available']),
]) }}
</p>
<p class="m-0 mt-3 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Guthaben laden Sie unter „Buchungen & Add-ons" auf — es steht sofort zur Verfügung (1 Credit = 1 €, größere Pakete enthalten Bonus-Credits).') }}
</p>
</div>
@endif
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Schließen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" icon="wallet" href="{{ route('me.bookings.index') }}#credits" wire:navigate>
{{ __('Credits aufladen') }}
</flux:button>
</div>
</div>
</flux:modal>
</div>