resetErrorBag('companyId'); } public function updatedCompanyId(): void { if (!$this->companyId) { $this->contactId = null; return; } $contactStillValid = $this->companyContact((int) $this->contactId, (int) $this->companyId); if (!$contactStillValid) { $this->contactId = $this->defaultContactIdFor((int) $this->companyId); } unset($this->presubmitChecks); } public function updatedTitle(): void { $this->resetErrorBag('title'); } /** * Single Source of Truth für die Validierungsregeln. Ermöglicht * Live-Re-Validation via updated()-Hook ohne Duplikation. * * @return array> */ protected function formRules(): array { return [ ...$this->baseFormRules(), 'portal' => ['required', Rule::in(array_map(fn(Portal $p) => $p->value, Portal::cases()))], 'companyId' => ['required', 'integer', Rule::exists('companies', 'id')], ]; } public function save(string $submitStatus = 'draft'): void { $this->syncScheduledAt(); $this->useEmbargo = false; $this->embargoAt = null; try { $this->validate($this->formRules()); } catch (\Illuminate\Validation\ValidationException $e) { $this->notifyValidationError($e); throw $e; } $status = match ($submitStatus) { 'review' => PressReleaseStatus::Review, default => PressReleaseStatus::Draft, }; $slug = new PressRelease()->generateUniqueSlug($this->title, [ 'portal' => $this->portal, 'language' => $this->language, ]); $cleanText = app(PressReleaseHtmlSanitizer::class)->clean($this->text); $pr = new PressRelease([ 'uuid' => (string) Str::uuid(), 'portal' => $this->portal, 'language' => $this->language, 'user_id' => auth()->id(), 'company_id' => (int) $this->companyId, 'category_id' => (int) $this->categoryId, 'title' => $this->title, 'subtitle' => trim($this->subtitle) ?: null, 'slug' => $slug, 'text' => $cleanText, 'boilerplate_override' => $this->useBoilerplateOverride && trim($this->boilerplateOverride) !== '' ? trim($this->boilerplateOverride) : null, 'keywords' => $this->keywords ?: null, 'backlink_url' => $this->backlinkUrl ?: null, 'scheduled_at' => $this->publishMode === 'scheduled' ? $this->resolveScheduledAt() : null, 'embargo_at' => null, 'status' => $status->value, 'no_export' => $this->noExport, ]); $pr->saveWithUniqueSlugRetry($this->title, [ 'portal' => $this->portal, 'language' => $this->language, ]); if ($this->contactId) { $contact = $this->companyContact((int) $this->contactId, (int) $this->companyId); if ($contact) { $pr->contacts()->sync([$contact->id]); } } Flux::toast(heading: $status === PressReleaseStatus::Review ? __('Eingereicht') : __('Entwurf gespeichert'), text: $status === PressReleaseStatus::Review ? __('Pressemitteilung zur Prüfung eingereicht.') : __('Pressemitteilung als Entwurf gespeichert.'), variant: 'success'); $this->redirect(route('admin.press-releases.edit', $pr->id), navigate: true); } public function with(): array { $term = Portal::stripTrailingAbbreviation($this->companySearch); $companies = Company::withoutGlobalScopes() ->searchByTerm($term) ->when(blank($term) && $this->companyId, fn($q) => $q->whereIn('id', [(int) $this->companyId])) ->when(blank($term) && !$this->companyId, fn($q) => $q->whereRaw('0 = 1')) ->orderBy('name') ->limit(50) ->get(['id', 'name']); $selectedCompany = $this->selectedCompany(); return [ 'companies' => $companies, 'categories' => $this->categoryOptions(), 'portalOptions' => array_filter(Portal::cases(), fn(Portal $p) => $p !== Portal::Both), 'selectedCompany' => $selectedCompany, 'selectedCompanyContacts' => $selectedCompany ? $this->companyContacts((int) $selectedCompany->id) : Contact::query()->whereRaw('0 = 1')->get(), 'tagSuggestions' => $this->tagSuggestionsFor($selectedCompany), ]; } #[Computed] public function tags(): array { return $this->tagsArray(); } private function selectedCompany(): ?Company { return $this->companyId ? Company::withoutGlobalScopes()->find((int) $this->companyId) : null; } private function categoryOptions(): Collection { return app(AdminPerformanceCache::class)->remember(AdminPerformanceCache::ActiveCategoryOptions, AdminPerformanceCache::OptionsTtl, fn() => Category::query()->with('translations')->where('is_active', true)->orderBy('id')->get()); } }; ?>
{{-- ============== PAGE HEADER ============== --}} {{ __('Zurück') }} {{-- ============== 2-COLUMN GRID ============== --}}
{{-- =================== LINKS: SCHREIBFLÄCHE =================== --}}
{{-- 1) FIRMA-SELEKTOR --}}
{{ __('Für Firma') }} * @foreach ($companies as $company) {{ $company->name }} @if ($company->portal) ({{ $company->portal->abbreviation() }}) @endif @endforeach @if (blank(trim($companySearch))) {{ __('Mindestens 1 Zeichen eingeben…') }} @else {{ __('Keine Firma gefunden.') }} @endif
{{ __('Boilerplate und Pressekontakt werden bei Wechsel angepasst.') }} @if ($selectedCompany) {{ __('Firmenprofil') }} @endif
{{-- 2) TITEL --}}
{{ __('Titel / Headline') }} *
@php $titleLen = mb_strlen($title); $titleClass = $titleLen >= 40 && $titleLen <= 90 ? 'good' : ($titleLen > 0 ? 'warn' : ''); $titleBar = min(100, max(0, ($titleLen / 100) * 100)); @endphp {{ $titleLen }} / 100 {{ __('KI-Titel · bald') }}

{{ __('40–90 Zeichen empfohlen. Konkret, ohne Marketing-Floskeln.') }}

{{-- 3) SUBTITLE --}}
{{ __('Untertitel') }} — {{ __('optional') }} @php $subLen = mb_strlen($subtitle); $subBar = min(100, max(0, ($subLen / 200) * 100)); @endphp {{ $subLen }} / 200
{{-- 4) FLIESSTEXT --}}
{{ __('Fließtext') }} *
@php $textLen = app( \App\Services\PressRelease\PressReleaseHtmlSanitizer::class, )->plainTextLength($text); $textClass = $textLen >= 600 ? 'good' : ($textLen >= 50 ? 'warn' : ''); $textBar = min(100, max(0, ($textLen / 3500) * 100)); @endphp {{ number_format($textLen, 0, ',', '.') }} / 3.500 Z. {{ __('KI-Lektorat · bald') }}
{{ __('KI-Lektorat') }} {{ __('liest Korrektur, schlägt Kürzungen vor und prüft auf werbliche Sprache. Erscheint hier inline — bald verfügbar.') }}
{{-- 5) BOILERPLATE --}}
{{ __('Über das Unternehmen') }} — {{ __('Boilerplate aus Firma') }}
@if ($selectedCompany?->boilerplate)

{!! nl2br(e($selectedCompany->boilerplate)) !!}

@if ($selectedCompany->website)

Web: {{ $selectedCompany->website }}

@endif
@else
{{ __('Für diese Firma ist noch kein Boilerplate-Text hinterlegt. Du kannst entweder einen Override-Text für diese PM setzen oder das Firmenprofil ergänzen.') }}
@endif @if ($useBoilerplateOverride)
@endif

{{ __('Wird automatisch unter jeder Pressemitteilung dieser Firma angefügt. Pro PM editierbar.') }}

{{-- /Schreibfläche --}} {{-- =================== RECHTS: SETTINGS-SIDEBAR =================== --}}