id = $id; $pr = $this->getMyPR(); $this->authorize('view', $pr); } public function submitForReview(): void { $pr = $this->getMyPR(); $this->authorize('submitForReview', $pr); 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 (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 Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'), 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'); } /** * 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(); $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?->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), '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, 'contacts' => $pr->contacts, 'statusLogs' => $pr->statusLogs, 'statusColor' => match($pr->status->value) { 'published' => 'green', 'review' => 'yellow', 'rejected' => 'red', 'archived' => 'blue', default => 'zinc', }, ]; } /** * 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 function getMyPR(): PressRelease { $user = auth()->user(); return PressRelease::withoutGlobalScopes() ->where(function ($q) use ($user): void { $q->where('user_id', $user->id); $companyIds = $user->accessibleCompanyIds(); if ($companyIds !== []) { $q->orWhereIn('company_id', $companyIds); } }) ->with([ 'company:id,name,email,phone,address,portal,logo_path,legacy_portal,is_active,type', '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); } }; ?>
{{ $pr->subtitle }}
@endif{{ $pr->company?->name ?? '–' }} · {{ $categoryName }} · {{ $pr->created_at->format('d.m.Y') }}
{{ $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.') }}
{{ __('Diese Pressemitteilung ist veröffentlicht (seit :date).', ['date' => $pr->published_at?->format('d.m.Y H:i') ?? '–']) }}
{{ __('Hervorhebung auf Start- und Branchenseite.') }}
{{ __('Ein weiterer Kauf verlängert die Laufzeit.') }}
@endif @else{{ __('Nur grün geprüfte Pressemitteilungen sind boostbar.') }}
@endif{{ __('PDF mit Datum, Portal und URL — :credits Credits.', ['credits' => $proofPrice]) }}
{{ __('Ihre Pressemitteilung wird gerade geprüft. Sie werden benachrichtigt, sobald eine Entscheidung vorliegt.') }}
{{ $log->reason }}
@endif{{ __('Noch keine Statusänderungen protokolliert.') }}
@endif{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}