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, isBoosted: bool, activeUntil: \Illuminate\Support\Carbon|null, available: bool, free: int, limit: int, 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, 'free' => 0, 'limit' => $boostService->slotLimit($placement), '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'], 'free' => $availability['free'], 'limit' => $availability['limit'], '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); } }; ?>
@php $statusClass = $pr->status->badgeClass(); @endphp {{-- Flash-Banner ersetzt durch im Layout. --}} {{-- ============== PAGE HEADER ============== --}} {{-- ============== SHARE-LINK ERFOLG ============== --}} @if ($shareUrl)
{{ __('Öffentlicher Vorschau-Link erstellt') }} {{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}
@if ($pr->portal === \App\Enums\Portal::Both) {{-- PM erscheint auf beiden Portalen: Vorschau je Portal öffnen --}} {{ __('Vorschau presseecho') }} {{ __('Vorschau businessportal24') }} @else {{ __('Vorschau öffnen') }} @endif
@endif {{-- ============== REJECTION-HINWEIS ============== --}} @if ($pr->status === PressReleaseStatus::Rejected && $latestRejection)
{{ __('Diese Pressemitteilung wurde abgelehnt') }} {{ __('Handlung erforderlich') }}
@if ($latestRejection->reason) {{ __('Begründung') }}: {{ $latestRejection->reason }} @else {{ __('Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.') }} @endif {{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
@endif {{-- ============== STATUS-WORKFLOW (oben, farblich abgehoben) ============== --}} @if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
{{ __('Status-Workflow') }} status === PressReleaseStatus::Rejected ? 'err' : 'hub'])> {{ $pr->status === PressReleaseStatus::Rejected ? __('Überarbeiten') : __('Entwurf') }}
@unless ($hasActiveBooking) {{-- Veröffentlichungs-Gate: Einreichen zur Prüfung setzt eine bezahlte Option voraus. Speichern als Entwurf bleibt frei. --}}
{{ __('Noch keine Buchung.') }} {{ __('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).') }}
@endunless

{{ $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.') }}

@if ($canEdit) {{ __('Bearbeiten') }} @endif @if ($hasActiveBooking) {{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }} @else {{ __('Buchung wählen') }} @endif
@endif @if ($pr->status === PressReleaseStatus::Published)
{{ __('Status-Workflow') }} {{ __('Live') }}

{{ __('Diese Pressemitteilung ist veröffentlicht (seit :date).', ['date' => $pr->published_at?->format('d.m.Y H:i') ?? '–']) }}

@endif {{-- ============== ADD-ONS (Boost + Veröffentlichungsnachweis) ============== --}} @if ($pr->status === PressReleaseStatus::Published)
{{ __('Add-ons') }} {{ __(':n Credits Guthaben', ['n' => $creditBalance]) }}
{{-- Boost-Stufen: Startseiten-Boost + Kategorie-Boost (13.07.) --}} @foreach ($boostAddOns as $placementValue => $addOn)
@if ($placementValue === 'home') @else @endif

{{ $addOn['label'] }}

{{ $addOn['description'] }}

@if ($addOn['isBoosted'])
{{ __('Aktiv bis :date', ['date' => $addOn['activeUntil']?->format('d.m.Y H:i')]) }}
@endif @if ($canBoost && $addOn['available'])
@foreach ($addOn['options'] as $days => $credits) {{ __(':days Tage für :credits Credits buchen', ['days' => $days, 'credits' => $credits]) }} @endforeach
@if ($addOn['isBoosted'])

{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}

@else {{-- Slot-Transparenz (13.07.): knappe Plätze sichtbar machen. --}}

{{ __('Noch :free von :limit Plätzen frei.', ['free' => $addOn['free'], 'limit' => $addOn['limit']]) }}

@endif @elseif ($canBoost) {{-- Slot-Inventar erschöpft: ehrlich ausweisen statt verkaufen (13.07.). --}}
@if ($addOn['freeAt']) {{ __('Derzeit ausgebucht — der nächste Platz wird am :date frei.', ['date' => $addOn['freeAt']->format('d.m.Y')]) }} @else {{ __('Derzeit ausgebucht.') }} @endif
@else

{{ __('Nur grün geprüfte Pressemitteilungen sind boostbar.') }}

@endif
@endforeach {{-- Veröffentlichungsnachweis --}}

{{ __('Veröffentlichungsnachweis') }}

{{ __('PDF mit Datum, Portal und URL — :credits Credits.', ['credits' => $proofPrice]) }}

@if ($proofPurchased) {{ __('Nachweis herunterladen') }} @else {{ __('Für :credits Credits kaufen', ['credits' => $proofPrice]) }} @endif
@endif @if ($pr->status === PressReleaseStatus::Review)
{{ __('Status-Workflow') }} {{ __('Geduld bitte') }}

{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}

@endif {{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
{{ __('Firma & Pressekontakt') }}
{{-- Firma der Pressemitteilung — gleiche Kachel wie in der Firmenübersicht --}} @if ($pr->company)
@if ($pr->company->is_active) {{ __('Aktiv') }} @else {{ __('Inaktiv') }} @endif

{{ $pr->company->name }}

@if (filled($companyMetaLine))
{{ $companyMetaLine }}
@endif
@if ($pr->company->email) {{ $pr->company->email }} @endif @if ($pr->company->phone) {{ $pr->company->phone }} @endif
@if ($pr->company->portal === \App\Enums\Portal::Both) presseecho businessportal24 @elseif ($pr->company->portal === \App\Enums\Portal::Presseecho) presseecho @elseif ($pr->company->portal === \App\Enums\Portal::Businessportal24) businessportal24 @endif
{{ number_format($pr->company->press_releases_count ?? 0, 0, ',', '.') }} {{ __('PMs') }}
{{ number_format($pr->company->contacts_count ?? 0, 0, ',', '.') }} {{ __('Kontakte') }}
@endif {{-- Pressekontakt(e) direkt darunter --}}
@forelse ($contacts as $contact)
{{ $contact->displayName() }}
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
@if ($contact->email) {{ $contact->email }} @endif @if ($contact->phone) {{ $contact->phone }} @endif
@empty
{{ __('Dieser Pressemitteilung ist noch kein Pressekontakt zugeordnet.') }} @if ($pr->company) {{ __('Kontakte in der Firma prüfen.') }} @endif
@endforelse
{{ __('Status & Verlauf') }} {{ $pr->status->label() }}
{{ __('Aktueller Status') }}
{{ $pr->status->label() }}
{{-- Ergebnis der KI-Inhaltsprüfung (13.07.): erklärt sichtbar, warum die Boost-Platzierung (nur Grün) verfügbar ist oder nicht. --}}
{{ __('Inhaltsprüfung') }}
@if ($pr->classification) {{ $pr->classification->label() }} @else {{ __('Ausstehend') }} @endif
@if ($pr->classification === \App\Enums\PressReleaseClassification::Yellow)
{{ __('Die Boost-Platzierung ist grün geprüften Meldungen vorbehalten.') }}
@endif
{{ __('Erstellt') }}
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
{{ __('Veröffentlicht') }}
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
{{ __('Aufrufe') }}
{{ number_format($pr->hits, 0, ',', '.') }}
{{ __('Geteilt') }}
{{ number_format($shareTotal, 0, ',', '.') }}
@if ($shareTotal > 0)
{{ $shareBreakdown }}
@endif
@if ($pr->scheduled_at)
{{ __('Geplante Veröffentlichung') }}
{{ $pr->scheduledAtLocal()->format('d.m.Y H:i') }}
@endif @if ($pr->embargo_at)
{{ __('Sperrfrist bis') }}
{{ $pr->embargoAtLocal()->format('d.m.Y H:i') }}
@endif
{{ __('Portal') }}
{{ $pr->portal?->label() ?? '–' }} {{ strtoupper($pr->language) }}
{{ __('Kategorie') }}
{{ $categoryName }}
@if (filled($pr->keywords))
{{ __('Themen') }}
@foreach (array_filter(array_map('trim', explode(',', $pr->keywords))) as $keyword) {{ $keyword }} @endforeach
@endif @if ($pr->backlink_url)
{{ __('Backlink') }}
{{ $pr->backlink_url }}
@endif @if ($pr->no_export)
{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}
@endif
@if ($statusLogs->isNotEmpty())
    @foreach ($statusLogs as $log)
  1. @php $logClass = $log->to_status?->badgeClass() ?? 'muted'; @endphp {{ $log->to_status?->label() }} {{ $log->created_at->format('d.m.Y H:i') }} @if ($log->changedBy) {{ __('durch :name', ['name' => $log->changedBy->name]) }} @endif
    @if ($log->reason)

    {{ $log->reason }}

    @endif
  2. @endforeach
@else

{{ __('Noch keine Statusänderungen protokolliert.') }}

@endif
{{-- Content-Score wie im Editor (gemeinsame Komponente, 13.07.). --}} @if (! is_null($pr->content_score)) @endif
{{-- ============== 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. --}}
{{ $pr->title }}
@if ($coverIsPlaceholder)
{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}
@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. --}}
@if ($aiImageBadge = $titleImage->ai_declaration?->publicLabel() ?? ($titleImage->is_ai_generated ? __('KI-generiert') : null)) {{ $aiImageBadge }} @endif @if ($titleImage->copyright) {{ $titleImage->copyright }} @endif
@endif
{{-- ============== INHALT ============== --}}
{{ __('Inhalt') }}
{{-- KI-Label (Art. 50 KI-VO): erscheint so auch in der Veröffentlichung — am Anfang des Inhalts, in Einreicher-Stimme. --}}
{!! $pr->renderedText() !!}
{{-- ============== VERÖFFENTLICHUNGS-MODAL ============== --}} @if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected) @endif {{-- ============== BOILERPLATE-OVERRIDE ============== --}} @if ($pr->boilerplate_override)
{{ __('Eigener Abbinder (Boilerplate)') }} {{ __('Override') }}

{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}

{{ $pr->boilerplate_override }}
@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
{{ __('Depublikation (Service)') }}

{{ __('Aus der öffentlichen Ansicht entfernen') }}

{{ __('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]) }}

@if ($depublicationRequested)
{{ __('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.') }}
@else

{{ __('AGB (Version :version)', ['version' => \App\Models\LegalDocument::currentAgbVersion()]) }} · {{ __('Gesetzliche Ansprüche (z. B. DSGVO) bleiben davon unberührt und sind immer kostenlos.') }}

@if ($depublicationConfirmed) {{ __('Kostenpflichtig depublizieren') }} @else {{ __('Kostenpflichtig depublizieren') }} @endif @endif
@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. --}}
{{ __('Add-on') }} {{ __('Kauf bestätigen') }}
@if ($pendingPurchase) @if ($pendingPurchase['extendsUntil']) {{-- Verlängerungs-Hinweis: für diese PM läuft bereits ein Boost. --}}

{{ __('Diese Buchung verlängert den laufenden Boost.') }}

{{ __('Aktuell aktiv bis :until — die neue Laufzeit schließt daran an und endet am :end.', [ 'until' => $pendingPurchase['extendsUntil'], 'end' => $pendingPurchase['newEndsAt'], ]) }}

@endif
{{ $pendingPurchase['label'] }} {{ __(':n Credits', ['n' => $pendingPurchase['credits']]) }}

@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

@endif
{{ __('Abbrechen') }} {{ __('Kostenpflichtig buchen') }}
{{-- ============== MODAL: NICHT GENÜGEND CREDITS ============== Öffnet sich serverseitig, wenn ein Add-on-Kauf (Boost/Nachweis) am Wallet-Guthaben scheitert — mit Direktlink zum Aufladen. --}}
{{ __('Credit-Wallet') }} {{ __('Nicht genügend Credits') }}
@if ($creditShortfall)

{{ __(':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']), ]) }}

{{ __('Guthaben laden Sie unter „Buchungen & Add-ons" auf — es steht sofort zur Verfügung (1 Credit = 1 €, größere Pakete enthalten Bonus-Credits).') }}

@endif
{{ __('Schließen') }} {{ __('Credits aufladen') }}