presseportale/resources/views/livewire/admin/press-releases/edit.blade.php
Kevin Adametz a000238ca8 User Panel: Phase-8-Abschluss, Titelbild/Lizenzen/Zeitzonen und KI-Pruef-Pipeline
Phase 8 (Rest) + Umbauten vom 10./11.06.:
- Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker,
  PressReleaseCoverImage-Resolver
- Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen,
  Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise)
- Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt),
  geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE)
- Quota-Stub (users.press_release_quota) + monatlicher Reset-Command
- Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf
  filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout)

KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans):
- API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route
  durch denselben Funnel (Blacklist, Quota, Status-Log)
- Klassifikation Rot/Gelb/Gruen asynchron (Queue classification,
  OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log
- Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen ->
  Auto-Publish; Scheduler publiziert nur gruene faellige PMs
- Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl.
  Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung
- Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override

Suite: 442 passed, 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:30:13 +00:00

1395 lines
61 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\Portal;
use App\Enums\PressReleaseStatus;
use App\Jobs\ClassifyPressRelease;
use App\Jobs\ScorePressRelease;
use App\Models\Category;
use App\Models\Company;
use App\Models\Contact;
use App\Models\PressRelease;
use App\Services\Admin\AdminPerformanceCache;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] class extends Component {
#[Locked]
public int $id;
public string $portal = '';
public string $language = 'de';
public int|string|null $companyId = null;
public string $companySearch = '';
public int|string|null $categoryId = null;
public int|string|null $contactId = null;
public string $title = '';
public string $subtitle = '';
public string $text = '';
public string $keywords = '';
public string $backlinkUrl = '';
public string $boilerplateOverride = '';
public bool $useBoilerplateOverride = false;
public bool $noExport = false;
public string $publishMode = 'now';
public ?string $scheduledAt = null;
public ?string $scheduledDate = null;
public ?string $scheduledTime = null;
public bool $useEmbargo = false;
public ?string $embargoAt = null;
public string $currentStatus = '';
public string $targetStatus = '';
/**
* Treiber für den manuellen KI-Re-Check: 'default' nutzt den konfigurierten
* Anbieter, sonst expliziter Override (z. B. 'openai'|'deterministic').
*/
public string $kiProvider = 'default';
public bool $kiRunClassification = true;
public bool $kiRunContentScore = false;
public function mount(int $id): void
{
$this->id = $id;
$pr = PressRelease::withoutGlobalScopes()->findOrFail($id);
$this->portal = $pr->portal->value;
$this->language = $pr->language;
$this->companyId = $pr->company_id;
$this->categoryId = $pr->category_id;
$this->title = $pr->title;
$this->subtitle = $pr->subtitle ?? '';
$this->text = $pr->text;
$this->keywords = $pr->keywords ?? '';
$this->backlinkUrl = $pr->backlink_url ?? '';
$this->boilerplateOverride = $pr->boilerplate_override ?? '';
$this->useBoilerplateOverride = filled($pr->boilerplate_override);
$this->noExport = $pr->no_export;
$this->currentStatus = $pr->status->value;
$this->targetStatus = $this->currentStatus;
$this->contactId = $pr->contacts()->withoutGlobalScopes()->first()?->id ?? $this->defaultContactIdFor((int) $pr->company_id);
if ($pr->scheduled_at) {
// DB-Wert ist UTC; für die Eingabefelder nach Europe/Berlin wandeln.
$scheduledAt = $pr->scheduled_at->copy()->setTimezone(PressRelease::DISPLAY_TIMEZONE);
$this->publishMode = 'scheduled';
$this->scheduledAt = $scheduledAt->format('Y-m-d\TH:i');
$this->scheduledDate = $scheduledAt->format('Y-m-d');
$this->scheduledTime = $scheduledAt->format('H:i');
}
}
public function updatedCompanySearch(): void
{
$this->resetErrorBag('companyId');
}
public function updatedPublishMode(): void
{
$this->syncScheduledAt();
if ($this->publishMode === 'now') {
$this->scheduledDate = null;
$this->scheduledTime = null;
$this->scheduledAt = null;
$this->resetErrorBag(['scheduledDate', 'scheduledTime', 'scheduledAt']);
}
}
public function updatedScheduledDate(): void
{
$this->syncScheduledAt();
$this->validateScheduledAtWhenReady();
}
public function updatedScheduledTime(): void
{
$this->syncScheduledAt();
$this->validateScheduledAtWhenReady();
}
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 addTag(string $tag): void
{
$tag = trim($tag);
if ($tag === '') {
return;
}
$existing = $this->tagsArray();
if (count($existing) >= 5) {
return;
}
if (in_array($tag, $existing, true)) {
return;
}
$existing[] = $tag;
$this->keywords = implode(', ', $existing);
unset($this->tags, $this->presubmitChecks);
}
public function removeTag(string $tag): void
{
$existing = array_values(array_filter($this->tagsArray(), fn(string $existingTag): bool => $existingTag !== $tag));
$this->keywords = implode(', ', $existing);
unset($this->tags, $this->presubmitChecks);
}
/**
* @return array<string, array<int, mixed>>
*/
protected function formRules(): array
{
$rules = [
'portal' => ['required', Rule::in(array_map(fn(Portal $p) => $p->value, Portal::cases()))],
'language' => ['required', Rule::in(['de', 'en'])],
'companyId' => ['required', 'integer', Rule::exists('companies', 'id')],
'categoryId' => ['required', 'integer', Rule::exists('categories', 'id')],
'contactId' => ['nullable', 'integer'],
'title' => ['required', 'string', 'min:5', 'max:255'],
'subtitle' => ['nullable', 'string', 'max:255'],
'text' => ['required', 'string', 'min:50'],
'keywords' => ['nullable', 'string', 'max:255'],
'backlinkUrl' => ['nullable', 'url', 'max:255'],
'boilerplateOverride' => ['nullable', 'string', 'max:5000'],
'publishMode' => ['required', Rule::in(['now', 'scheduled'])],
];
if ($this->publishMode === 'scheduled') {
$rules['scheduledDate'] = ['required', 'date'];
$rules['scheduledTime'] = ['required', 'date_format:H:i'];
$rules['scheduledAt'] = [
'required',
'date',
// Termin wird in Europe/Berlin erfasst; deshalb hier zeitzonen-
// bewusst prüfen statt über die naive `after:`-Regel.
function (string $attribute, mixed $value, \Closure $fail): void {
$scheduledAt = $this->scheduledAtUtc();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$fail(__('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
}
},
];
} else {
$rules['scheduledDate'] = ['nullable'];
$rules['scheduledTime'] = ['nullable'];
$rules['scheduledAt'] = ['nullable'];
}
$rules['embargoAt'] = ['nullable'];
return $rules;
}
protected function syncScheduledAt(): void
{
if ($this->publishMode !== 'scheduled') {
$this->scheduledAt = null;
return;
}
if (blank($this->scheduledDate) && blank($this->scheduledTime) && filled($this->scheduledAt)) {
$scheduledAt = \Carbon\Carbon::parse($this->scheduledAt);
$this->scheduledDate = $scheduledAt->format('Y-m-d');
$this->scheduledTime = $scheduledAt->format('H:i');
return;
}
if (blank($this->scheduledDate) || blank($this->scheduledTime)) {
$this->scheduledAt = null;
return;
}
$this->scheduledAt = "{$this->scheduledDate}T{$this->scheduledTime}";
}
/**
* Wandelt den in Europe/Berlin erfassten Termin in den UTC-Zeitpunkt,
* wie er in der Datenbank gespeichert wird. Null, wenn kein Termin gesetzt.
*/
protected function scheduledAtUtc(): ?\Carbon\Carbon
{
if (blank($this->scheduledAt)) {
return null;
}
return \Carbon\Carbon::parse($this->scheduledAt, PressRelease::DISPLAY_TIMEZONE)->utc();
}
protected function validateScheduledAtWhenReady(): void
{
if (blank($this->scheduledAt)) {
return;
}
$this->resetErrorBag('scheduledAt');
$scheduledAt = $this->scheduledAtUtc();
if ($scheduledAt === null || $scheduledAt->lessThanOrEqualTo(now()->addMinutes(5))) {
$this->addError('scheduledAt', __('Der Veröffentlichungstermin muss mindestens 5 Minuten in der Zukunft liegen.'));
return;
}
try {
$this->validateOnly('scheduledAt', $this->formRules());
} catch (\Illuminate\Validation\ValidationException) {
// Termin bleibt invalid.
}
}
/**
* Die Termin-Synchronisierung liegt vollständig in den spezifischen
* `updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks; hier bleibt
* nur die generische Re-Validierung bereits fehlerhafter Felder.
*/
public function updated(string $property): void
{
if (!$this->getErrorBag()->has($property)) {
return;
}
try {
$this->validateOnly($property, $this->formRules());
} catch (\Illuminate\Validation\ValidationException) {
// Field bleibt invalid.
}
}
protected function notifyValidationError(?\Illuminate\Validation\ValidationException $exception = null): void
{
$count = $exception ? array_sum(array_map('count', $exception->errors())) : count($this->getErrorBag()->all());
Flux::toast(heading: __('Bitte Eingaben prüfen'), text: $count > 1 ? __(':count Felder benötigen deine Aufmerksamkeit.', ['count' => $count]) : __('Ein Feld benötigt deine Aufmerksamkeit.'), variant: 'danger', duration: 6000);
}
public function save(): void
{
$this->syncScheduledAt();
$this->useEmbargo = false;
$this->embargoAt = null;
try {
$this->validate($this->formRules());
} catch (\Illuminate\Validation\ValidationException $e) {
$this->notifyValidationError($e);
throw $e;
}
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
if ($pr->title !== $this->title || $pr->portal !== $this->portal || $pr->language !== $this->language) {
$slug = $pr->generateUniqueSlug($this->title, [
'portal' => $this->portal,
'language' => $this->language,
]);
} else {
$slug = $pr->slug;
}
$cleanText = app(PressReleaseHtmlSanitizer::class)->clean($this->text);
$pr->update([
'portal' => $this->portal,
'language' => $this->language,
'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->scheduledAtUtc() : null,
'embargo_at' => null,
'no_export' => $this->noExport,
]);
$contentChanged = $pr->wasChanged(['title', 'text']);
if ($this->contactId) {
$contact = $this->companyContact((int) $this->contactId, (int) $this->companyId);
if ($contact) {
$pr->contacts()->sync([$contact->id]);
}
}
// Inhaltliche Änderung einer bereits geprüften/bewerteten PM → neu
// prüfen (Re-Check ohne Routing) und neu bewerten.
if ($contentChanged) {
$service = app(PressReleaseService::class);
$fresh = $pr->fresh();
$service->reclassifyIfClassified($fresh);
$service->rescoreIfScored($fresh);
}
Flux::toast(text: __('Pressemitteilung gespeichert.'), variant: 'success');
}
public function submitForReview(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
try {
app(PressReleaseService::class)->submitForReview($pr);
} catch (BlacklistViolationException $e) {
$this->currentStatus = PressReleaseStatus::Rejected->value;
Flux::toast(heading: __('Automatisch abgelehnt'), text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]), variant: 'danger', duration: 8000);
return;
}
$this->currentStatus = PressReleaseStatus::Review->value;
Flux::toast(text: __('Zur Prüfung eingereicht.'), variant: 'success');
}
/**
* Stößt eine manuelle KI-Prüfung im Hintergrund an (Re-Check).
*
* Anders als beim Einreichen wird hier NICHT geroutet (Status bleibt
* unverändert): Das Ergebnis aktualisiert nur Klassifikation + Audit, die
* Entscheidung trifft der Admin weiterhin selbst.
*/
public function runKiCheck(): void
{
if (! $this->kiRunClassification && ! $this->kiRunContentScore) {
Flux::toast(text: __('Es wurde keine Prüfung ausgewählt.'), variant: 'warning');
return;
}
$provider = $this->kiProvider === 'default' ? null : $this->kiProvider;
if ($this->kiRunClassification) {
ClassifyPressRelease::dispatch($this->id, route: false, providerOverride: $provider)
->onQueue('classification');
}
if ($this->kiRunContentScore) {
ScorePressRelease::dispatch($this->id, providerOverride: $provider)
->onQueue('classification');
}
Flux::modal('admin-ki-check')->close();
Flux::toast(
heading: __('KI-Prüfung gestartet'),
text: __('Die Prüfung läuft im Hintergrund. Das Ergebnis erscheint nach Abschluss in der Detailansicht.'),
variant: 'success',
duration: 7000,
);
}
public function publish(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
try {
app(PressReleaseService::class)->publish($pr);
} catch (BlacklistViolationException $e) {
$this->currentStatus = PressReleaseStatus::Rejected->value;
Flux::toast(heading: __('Automatisch abgelehnt'), text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]), variant: 'danger', duration: 8000);
return;
}
$this->currentStatus = PressReleaseStatus::Published->value;
Flux::toast(text: __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'), variant: 'success');
}
public function reject(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->reject($pr);
$this->currentStatus = PressReleaseStatus::Rejected->value;
Flux::toast(text: __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'), variant: 'warning');
}
public function backToDraft(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->backToDraft($pr);
$this->currentStatus = PressReleaseStatus::Draft->value;
Flux::toast(text: __('Zurück auf Entwurf gesetzt.'), variant: 'success');
}
public function archive(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->archive($pr);
$this->currentStatus = PressReleaseStatus::Archived->value;
$this->targetStatus = $this->currentStatus;
Flux::toast(text: __('Pressemitteilung archiviert.'), variant: 'success');
}
public function changeStatus(): void
{
$this->validate([
'targetStatus' => ['required', Rule::in(array_map(fn(PressReleaseStatus $status) => $status->value, PressReleaseStatus::cases()))],
]);
if ($this->targetStatus === $this->currentStatus) {
$this->addError('targetStatus', __('Bitte wähle einen anderen Status aus.'));
return;
}
$status = PressReleaseStatus::from($this->targetStatus);
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
app(PressReleaseService::class)->changeStatusFromAdmin($pr, $status);
$this->currentStatus = $status->value;
$this->targetStatus = $status->value;
Flux::toast(text: __('Status wurde auf ":status" geändert.', ['status' => $status->label()]), variant: 'success');
Flux::modal('confirm-status-change')->close();
}
public function deletePressRelease(): void
{
$pr = PressRelease::withoutGlobalScopes()->findOrFail($this->id);
$wasPublished = $pr->status === PressReleaseStatus::Published;
app(PressReleaseService::class)->deleteFromAdmin($pr);
Flux::toast(text: $wasPublished ? __('Pressemitteilung wurde archiviert und der Inhalt durch den voreingestellten Ersatztext ersetzt.') : __('Pressemitteilung wurde gelöscht.'), variant: 'success');
$this->redirect(route('admin.press-releases.index'), navigate: true);
}
public function with(): array
{
$term = Portal::stripTrailingAbbreviation($this->companySearch);
$companies = Company::withoutGlobalScopes()
->when(filled($term), function ($q) use ($term): void {
if ($this->supportsFullTextSearch($term)) {
$q->whereFullText(['name', 'email', 'slug'], $term);
return;
}
$q->where('name', 'like', '%' . $term . '%')->orWhere('slug', 'like', '%' . $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']);
$statusEnum = PressReleaseStatus::tryFrom($this->currentStatus);
$selectedCompany = $this->companyId ? Company::withoutGlobalScopes()->find((int) $this->companyId) : null;
return [
'companies' => $companies,
'categories' => $this->categoryOptions(),
'portalOptions' => array_filter(Portal::cases(), fn(Portal $p) => $p !== Portal::Both),
'statusOptions' => PressReleaseStatus::cases(),
'statusEnum' => $statusEnum,
'targetStatusEnum' => PressReleaseStatus::tryFrom($this->targetStatus),
'selectedCompany' => $selectedCompany,
'selectedCompanyContacts' => $selectedCompany ? $this->companyContacts((int) $selectedCompany->id) : Contact::query()->whereRaw('0 = 1')->get(),
'tagSuggestions' => $this->tagSuggestionsFor($selectedCompany),
'statusColor' => match ($this->currentStatus) {
'published' => 'green',
'review' => 'yellow',
'rejected' => 'red',
'archived' => 'blue',
default => 'zinc',
},
];
}
#[Computed]
public function tags(): array
{
return $this->tagsArray();
}
#[Computed]
public function presubmitChecks(): array
{
$titleLen = mb_strlen(trim($this->title));
$textLen = app(PressReleaseHtmlSanitizer::class)->plainTextLength($this->text);
$tagsCount = count($this->tagsArray());
return [
[
'key' => 'title',
'status' => $titleLen >= 5 ? 'ok' : 'err',
'label' => __('Titel vorhanden'),
'sub' => $titleLen > 0 ? __(':n Zeichen', ['n' => $titleLen]) : __('Noch leer'),
],
[
'key' => 'text',
'status' => $textLen >= 600 ? 'ok' : ($textLen >= 50 ? 'warn' : 'err'),
'label' => __('Mindestlänge Fließtext erreicht'),
'sub' => __(':n / 600 Zeichen empfohlen', ['n' => number_format($textLen, 0, ',', '.')]),
],
[
'key' => 'company',
'status' => $this->companyId ? 'ok' : 'err',
'label' => __('Firma zugeordnet'),
'sub' => $this->companyId ? '' : __('Keine Firma gewählt'),
],
[
'key' => 'category',
'status' => $this->categoryId ? 'ok' : 'err',
'label' => __('Kategorie gewählt'),
'sub' => $this->categoryId ? '' : __('Kategorie ist Pflicht'),
],
[
'key' => 'contact',
'status' => $this->contactId ? 'ok' : 'warn',
'label' => __('Pressekontakt zugeordnet'),
'sub' => $this->contactId ? '' : __('empfohlen — Journalisten greifen direkt zum Hörer'),
],
[
'key' => 'tags',
'status' => $tagsCount >= 1 ? 'ok' : 'warn',
'label' => __('Themen-Tags vergeben'),
'sub' => $tagsCount >= 1 ? trans_choice('{1}:n Tag|[2,*]:n Tags', $tagsCount, ['n' => $tagsCount]) : __('empfohlen für SEO & Auffindbarkeit'),
],
];
}
/**
* @return list<string>
*/
private function tagsArray(): array
{
if (trim($this->keywords) === '') {
return [];
}
return collect(explode(',', $this->keywords))
->map(fn(string $tag): string => trim($tag))
->filter()
->unique()
->values()
->all();
}
private function defaultContactIdFor(int $companyId): ?int
{
if ($companyId <= 0) {
return null;
}
return $this->companyContacts($companyId)->first()?->id;
}
/**
* @return Collection<int, Contact>
*/
private function companyContacts(int $companyId): Collection
{
return Contact::withoutGlobalScopes()
->where('company_id', $companyId)
->orderBy('last_name')
->orderBy('first_name')
->get(['id', 'company_id', 'first_name', 'last_name', 'responsibility', 'phone', 'email']);
}
private function companyContact(int $contactId, int $companyId): ?Contact
{
if ($contactId <= 0 || $companyId <= 0) {
return null;
}
return Contact::withoutGlobalScopes()->where('company_id', $companyId)->whereKey($contactId)->first();
}
/**
* @return list<string>
*/
private function tagSuggestionsFor(?Company $company): array
{
$defaults = [__('Mittelstand'), __('Unternehmen'), __('Eröffnung'), __('Innovation'), __('Nachhaltigkeit')];
if (!$company) {
return $defaults;
}
return array_values(array_unique(array_filter([$company->portal?->label(), $company->country_code === 'DE' ? __('Deutschland') : null, ...$defaults])));
}
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());
}
private function supportsFullTextSearch(string $term): bool
{
return mb_strlen($term) >= 3 && in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
}; ?>
<div class="space-y-8 pr-editor-shell" x-data="{ tagInput: '' }">
@php
$statusClass = match ($currentStatus) {
'published' => 'ok',
'review' => 'warn',
'rejected' => 'err',
default => 'hub',
};
@endphp
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
<div class="flex items-center gap-3 mb-3 flex-wrap">
<span class="badge hub dot">{{ __('Admin Backend') }}</span>
<span class="eyebrow muted">{{ __('Content · Bearbeiten') }}</span>
<span @class(['badge', $statusClass])>{{ $statusEnum?->label() ?? $currentStatus }}</span>
<span class="badge hub">ID {{ $id }}</span>
</div>
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
{{ __('Pressemitteilung bearbeiten') }}
</h1>
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[640px] text-[color:var(--color-ink-2)]">
{{ __('Schreibfläche links, Steuerung rechts. Status-Aktionen erscheinen oben in der Sidebar.') }}
</p>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:modal.trigger name="admin-ki-check">
<flux:button variant="filled" icon="sparkles">
{{ __('Prüfung') }}
</flux:button>
</flux:modal.trigger>
<flux:button variant="filled" icon="eye" href="{{ route('admin.press-releases.show', $id) }}"
wire:navigate>
{{ __('Vorschau / Detail') }}
</flux:button>
<flux:button variant="filled" icon="arrow-left" href="{{ route('admin.press-releases.index') }}"
wire:navigate>
{{ __('Zur Liste') }}
</flux:button>
</div>
</header>
{{-- ============== KI-PRÜFUNG (On-Demand) ============== --}}
<flux:modal name="admin-ki-check" class="w-full max-w-lg">
<div class="space-y-5">
<div>
<flux:text class="eyebrow muted">{{ __('KI-Prüfung') }}</flux:text>
<flux:heading size="lg">{{ __('Prüfung im Hintergrund starten') }}</flux:heading>
<flux:text class="mt-1">
{{ __('Startet eine erneute KI-Prüfung. Das Ergebnis aktualisiert nur die Bewertung und das Audit-Log der Status bleibt unverändert.') }}
</flux:text>
</div>
<div class="space-y-3">
<label class="flex items-start gap-2 text-[13px] text-[color:var(--color-ink-2)]">
<flux:checkbox wire:model="kiRunClassification" />
<span>
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Klassifikation (Red Flag)') }}</span><br>
{{ __('Prüft auf unzulässige Inhalte (grün/gelb/rot).') }}
</span>
</label>
<label class="flex items-start gap-2 text-[13px] text-[color:var(--color-ink-2)]">
<flux:checkbox wire:model="kiRunContentScore" />
<span>
<span class="font-semibold text-[color:var(--color-ink)]">{{ __('Content-Score') }}</span><br>
{{ __('Bewertet die handwerkliche Qualität (0100) und leitet die Stufe ab.') }}
</span>
</label>
<flux:select wire:model="kiProvider" :label="__('Anbieter')">
<option value="default">{{ __('Konfigurierter Anbieter') }}</option>
<option value="openai">OpenAI</option>
<option value="deterministic">{{ __('Deterministisch (Blacklist)') }}</option>
</flux:select>
</div>
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" icon="sparkles" wire:click="runKiCheck">
{{ __('Prüfung starten') }}
</flux:button>
</div>
</div>
</flux:modal>
{{-- ============== 2-COLUMN GRID ============== --}}
<div class="grid gap-6 pr-editor-layout">
{{-- =================== LINKS: SCHREIBFLÄCHE =================== --}}
<div class="space-y-6 min-w-0">
{{-- 1) FIRMA-SELEKTOR --}}
<section class="panel">
<div class="p-4 space-y-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<span class="pr-form-label shrink-0" style="margin-bottom:0;">
{{ __('Für Firma') }} <span class="req">*</span>
</span>
<flux:select wire:model.live="companyId" variant="combobox" :filter="false" clearable
placeholder="{{ __('Firma suchen…') }}" class="w-full sm:flex-1">
<x-slot name="input">
<flux:select.input wire:model.live.debounce.300ms="companySearch"
placeholder="{{ __('Name eingeben…') }}" />
</x-slot>
@foreach ($companies as $company)
<flux:select.option :value="$company->id" wire:key="{{ $company->id }}">
{{ $company->name }} @if ($company->portal)
({{ $company->portal->abbreviation() }})
@endif
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if (blank(trim($companySearch)))
{{ __('Mindestens 1 Zeichen eingeben…') }}
@else
{{ __('Keine Firma gefunden.') }}
@endif
</flux:select.option.empty>
</x-slot>
</flux:select>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Boilerplate und Pressekontakt werden bei Wechsel angepasst.') }}
</span>
@if ($selectedCompany)
<flux:button size="sm" variant="filled" icon="building-office"
href="{{ route('admin.companies.show', $selectedCompany->id) }}" wire:navigate>
{{ __('Firmenprofil') }}
</flux:button>
@endif
</div>
</div>
<flux:error name="companyId" />
</section>
{{-- 2) TITEL --}}
<section class="panel">
<div class="p-5 pb-4">
<div class="flex items-center justify-between mb-2 gap-4">
<span class="pr-form-label" style="margin-bottom:0;">
{{ __('Titel / Headline') }} <span class="req">*</span>
</span>
<div class="flex items-center gap-3">
@php
$titleLen = mb_strlen($title);
$titleClass =
$titleLen >= 40 && $titleLen <= 90 ? 'good' : ($titleLen > 0 ? 'warn' : '');
$titleBar = min(100, max(0, ($titleLen / 100) * 100));
@endphp
<span class="pr-meter {{ $titleClass }}">
<span class="bar"><i style="width: {{ $titleBar }}%;"></i></span>
{{ $titleLen }} / 100
</span>
<span class="pr-bald-badge">{{ __('KI-Titel · bald') }}</span>
</div>
</div>
<flux:input wire:model.live.debounce.300ms="title"
placeholder="{{ __('Aussagekräftiger Titel — wer, was, wo?') }}" size="lg" />
<p class="pr-form-help">
{{ __('4090 Zeichen empfohlen. Konkret, ohne Marketing-Floskeln.') }}
</p>
<flux:error name="title" />
</div>
</section>
{{-- 3) SUBTITLE --}}
<section class="panel">
<div class="p-5 pb-4">
<div class="flex items-center justify-between mb-2 gap-4">
<span class="pr-form-label" style="margin-bottom:0;">
{{ __('Untertitel') }}
<span class="text-[color:var(--color-ink-4)] font-normal"
style="letter-spacing:0;text-transform:none;">
— {{ __('optional') }}
</span>
</span>
@php
$subLen = mb_strlen($subtitle);
$subBar = min(100, max(0, ($subLen / 200) * 100));
@endphp
<span class="pr-meter">
<span class="bar"><i style="width: {{ $subBar }}%;"></i></span>
{{ $subLen }} / 200
</span>
</div>
<flux:input wire:model.live.debounce.300ms="subtitle"
placeholder="{{ __('Kurzer Zusatztext / Dachzeile…') }}" />
<flux:error name="subtitle" />
</div>
</section>
{{-- 4) FLIESSTEXT --}}
<section class="panel">
<div class="p-5 pb-4">
<div class="flex items-center justify-between mb-2 gap-4">
<span class="pr-form-label" style="margin-bottom:0;">
{{ __('Fließtext') }} <span class="req">*</span>
</span>
<div class="flex items-center gap-3">
@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
<span class="pr-meter {{ $textClass }}">
<span class="bar"><i style="width: {{ $textBar }}%;"></i></span>
{{ number_format($textLen, 0, ',', '.') }} / 3.500 Z.
</span>
<span class="pr-bald-badge">{{ __('KI-Lektorat · bald') }}</span>
</div>
</div>
<flux:editor wire:model.live.debounce.500ms="text"
toolbar="heading | bold italic | bullet ordered blockquote | link | undo redo"
placeholder="{{ __('Hier weiterschreiben…') }}" />
<flux:error name="text" />
<div class="pr-ai-hint mt-4">
<span class="ico">
<flux:icon name="sparkles" variant="micro" />
</span>
<span>
<strong class="font-semibold">{{ __('KI-Lektorat') }}</strong>
{{ __('liest Korrektur, schlägt Kürzungen vor und prüft auf werbliche Sprache. Erscheint hier inline — bald verfügbar.') }}
</span>
</div>
</div>
</section>
{{-- 5) MEDIEN --}}
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
{{-- 6) ANHÄNGE — TEMPORÄR DEAKTIVIERT
Datei-Uploads erfordern eine vollständige Sicherheitsprüfung
(Virus-/Malware-Scan, MIME-Validierung, Storage-Quoten).
Wird in einer späteren Phase aktiviert.
<livewire:components.press-release-attachments-manager :press-release-id="$id" :wire:key="'pr-attachments-'.$id" />
--}}
{{-- 7) BOILERPLATE --}}
<section class="panel">
<div class="p-5">
<div class="flex items-center justify-between mb-3 gap-4">
<span class="pr-form-label" style="margin-bottom:0;">
{{ __('Über das Unternehmen') }}
<span class="text-[color:var(--color-ink-4)] font-normal"
style="letter-spacing:0;text-transform:none;">
— {{ __('Boilerplate aus Firma') }}
</span>
</span>
<flux:checkbox wire:model.live="useBoilerplateOverride"
:label="__('Für diese PM überschreiben')" />
</div>
@if ($selectedCompany?->boilerplate)
<div class="pr-boiler">
<p class="m-0">{!! nl2br(e($selectedCompany->boilerplate)) !!}</p>
@if ($selectedCompany->website)
<p class="m-0 text-[12px] text-[color:var(--color-ink-3)] mt-3">
<span
class="font-semibold text-[color:var(--color-ink-2)]">{{ __('Web') }}:</span>
{{ $selectedCompany->website }}
</p>
@endif
</div>
@else
<div class="pr-boiler text-[color:var(--color-ink-3)]">
{{ __('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.') }}
</div>
@endif
@if ($useBoilerplateOverride)
<div class="mt-3">
<flux:textarea wire:model.live.debounce.500ms="boilerplateOverride" rows="5"
placeholder="{{ __('Boilerplate-Text speziell für diese PM…') }}" />
<flux:error name="boilerplateOverride" />
</div>
@endif
<p class="pr-form-help">
{{ __('Wird automatisch unter jeder Pressemitteilung dieser Firma angefügt. Pro PM editierbar.') }}
</p>
</div>
</section>
</div>
{{-- /Schreibfläche --}}
{{-- =================== RECHTS: SETTINGS-SIDEBAR =================== --}}
<aside class="space-y-4 pr-editor-side self-start">
{{-- Status-Aktionen + Pre-Submit-Check --}}
<article class="panel" style="border-color:var(--color-hub);">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Status-Aktionen') }}</span>
<span @class(['badge', $statusClass])>{{ $statusEnum?->label() ?? $currentStatus }}</span>
</div>
<div class="p-5">
<div
class="rounded-[4px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3 mb-3">
@php
$okCount = collect($this->presubmitChecks)->where('status', 'ok')->count();
$totalCount = count($this->presubmitChecks);
@endphp
<div class="flex items-center justify-between mb-1">
<span class="eyebrow muted" style="font-size:9.5px;letter-spacing:0.16em;">
{{ __('Pre-Submit-Check') }}
</span>
<span class="text-[10.5px] font-mono font-semibold text-[color:var(--color-ok)]">
{{ $okCount }} / {{ $totalCount }} ok
</span>
</div>
@foreach ($this->presubmitChecks as $check)
<div class="pr-check-row {{ $check['status'] }}">
<span class="ic">
@if ($check['status'] === 'ok')
<flux:icon name="check" variant="micro" class="size-3" />
@elseif ($check['status'] === 'warn')
<flux:icon name="exclamation-triangle" variant="micro" class="size-3" />
@else
<flux:icon name="x-mark" variant="micro" class="size-3" />
@endif
</span>
<span class="lbl">
{{ $check['label'] }}
@if (!empty($check['sub']))
<span class="sub">{{ $check['sub'] }}</span>
@endif
</span>
</div>
@endforeach
</div>
<flux:field>
<flux:label>{{ __('Neuer Status') }}</flux:label>
<flux:select wire:model.live="targetStatus">
@foreach ($statusOptions as $statusOption)
<option value="{{ $statusOption->value }}">
{{ $statusOption->label() }}{{ $statusOption->value === $currentStatus ? ' (aktuell)' : '' }}
</option>
@endforeach
</flux:select>
<flux:error name="targetStatus" />
</flux:field>
<flux:modal.trigger name="confirm-status-change">
<flux:button type="button" variant="primary" class="w-full mt-3">
{{ __('Status wechseln') }}
</flux:button>
</flux:modal.trigger>
</div>
</article>
{{-- Kategorie (Pflichtfeld - prominent direkt unter Status) --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">
{{ __('Kategorie') }}
<span class="text-[color:var(--color-err)]">*</span>
</span>
</div>
<div class="p-5">
<flux:field>
<flux:select wire:model.live="categoryId">
<option value="">{{ __('Bitte wählen…') }}</option>
@foreach ($categories as $cat)
<option value="{{ $cat->id }}">
{{ $cat->translations->firstWhere('locale', 'de')?->name ?? $cat->id }}</option>
@endforeach
</flux:select>
<flux:error name="categoryId" />
<flux:description>{{ __('Pflichtfeld — steuert, in welcher Rubrik die PM erscheint.') }}
</flux:description>
</flux:field>
</div>
</article>
{{-- Portal (Badge in Portal-Farbe, Select für Admin-Override) --}}
@php
$portalPillClass = 'portal-pill';
if ($portal === 'presseecho') {
$portalPillClass = 'portal-pill pe';
} elseif ($portal === 'businessportal24') {
$portalPillClass = 'portal-pill bp';
}
$portalLabelDisplay = $portal;
foreach ($portalOptions as $p) {
if ($p->value === $portal) {
$portalLabelDisplay = $p->label();
break;
}
}
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Portal') }}</span>
<span class="{{ $portalPillClass }}">
<span class="pdot"></span>{{ $portalLabelDisplay }}
</span>
</div>
<div class="p-5">
<flux:field>
<flux:label>{{ __('Portal-Override') }} <span class="text-[color:var(--color-err)]">*</span>
</flux:label>
<flux:select wire:model.live="portal">
@foreach ($portalOptions as $p)
<option value="{{ $p->value }}">{{ $p->label() }}</option>
@endforeach
</flux:select>
<flux:error name="portal" />
<flux:description>
{{ __('Admin-Befugnis: Portal kann unabhängig von der Firma geändert werden.') }}
</flux:description>
</flux:field>
</div>
</article>
{{-- Pressekontakt --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Pressekontakt') }}</span>
</div>
<div class="p-5 space-y-3">
@if ($selectedCompanyContacts->isEmpty())
<p class="text-[12px] text-[color:var(--color-ink-3)] m-0">
{{ __('Diese Firma hat noch keine Pressekontakte.') }}
</p>
@if ($selectedCompany)
<flux:button size="sm" variant="filled" icon="plus" class="w-full"
href="{{ route('admin.companies.show', $selectedCompany->id) }}" wire:navigate>
{{ __('Kontakt im Firmenprofil anlegen') }}
</flux:button>
@endif
@else
<flux:field>
<flux:label>{{ __('Kontakt für diese PM') }}</flux:label>
<flux:select wire:model.live="contactId">
<option value="">{{ __('Bitte wählen…') }}</option>
@foreach ($selectedCompanyContacts as $contact)
@php
$contactName =
trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')) ?:
__('Kontakt #:n', ['n' => $contact->id]);
$contactRole = $contact->responsibility ?: __('Kontakt');
@endphp
<option value="{{ $contact->id }}">
{{ $contactName }} — {{ $contactRole }}
</option>
@endforeach
</flux:select>
<flux:error name="contactId" />
</flux:field>
@if (!$contactId)
<div class="flex items-start gap-2 text-[11.5px] text-[color:var(--color-warn)]">
<flux:icon name="exclamation-triangle" variant="mini"
class="size-4 flex-shrink-0 mt-0.5" />
<span>{{ __('Noch kein Pressekontakt ausgewählt. Empfohlen, aber nicht zwingend.') }}</span>
</div>
@else
@php($activeContact = $selectedCompanyContacts->firstWhere('id', (int) $contactId))
@if ($activeContact && empty($activeContact->phone))
<div class="flex items-start gap-2 text-[11.5px] text-[color:var(--color-warn)]">
<flux:icon name="exclamation-triangle" variant="mini"
class="size-4 flex-shrink-0 mt-0.5" />
<span>{{ __('Kein Telefon hinterlegt — Journalisten greifen oft direkt zum Hörer.') }}</span>
</div>
@endif
@endif
@endif
</div>
</article>
{{-- Themen-Tags --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Themen-Tags') }}</span>
<span class="text-[10.5px] text-[color:var(--color-ink-4)]">
<strong class="font-mono text-[color:var(--color-ink-2)]">{{ count($this->tags) }}</strong> /
5
</span>
</div>
<div class="p-5 space-y-3">
<div
class="border border-[color:var(--color-bg-rule)] rounded-[4px] bg-[color:var(--color-bg-card)] px-2 py-2 min-h-[58px] flex flex-wrap items-center gap-1.5">
@forelse ($this->tags as $tag)
<span class="pr-tag-chip" wire:key="tag-{{ $tag }}">
{{ $tag }}
<button type="button" class="x"
wire:click="removeTag(@js($tag))"
title="{{ __('Entfernen') }}">×</button>
</span>
@empty
@if (count($this->tags) === 0)
<span class="text-[11.5px] text-[color:var(--color-ink-4)] italic px-1.5">
{{ __('Tag tippen + Enter…') }}
</span>
@endif
@endforelse
<input type="text" x-model="tagInput"
@keydown.enter.prevent="if (tagInput.trim()) { $wire.addTag(tagInput.trim()); tagInput = ''; }"
@keydown.comma.prevent="if (tagInput.trim()) { $wire.addTag(tagInput.trim()); tagInput = ''; }"
class="flex-1 min-w-[80px] border-0 bg-transparent text-[12px] text-[color:var(--color-ink)] focus:outline-none p-1"
placeholder="{{ count($this->tags) === 0 ? '' : '+ Tag' }}"
@disabled(count($this->tags) >= 5) />
</div>
@if (!empty($tagSuggestions))
<div>
<div class="eyebrow muted mb-1.5" style="font-size:9.5px;">{{ __('Vorschläge') }}</div>
<div class="flex flex-wrap gap-1.5">
@foreach ($tagSuggestions as $suggestion)
@if (!in_array($suggestion, $this->tags, true))
<button type="button" class="pr-tag-suggest"
wire:click="addTag(@js($suggestion))">+
{{ $suggestion }}</button>
@endif
@endforeach
</div>
</div>
@endif
<p class="text-[10.5px] text-[color:var(--color-ink-4)] m-0 leading-[1.45]">
{{ __('Tags helfen bei SEO und Auffindbarkeit.') }}
</p>
</div>
</article>
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Veröffentlichung') }}</span>
</div>
<div class="p-5 space-y-3">
<label class="pr-pub-opt {{ $publishMode === 'now' ? 'is-checked' : '' }}">
<input type="radio" wire:model.live="publishMode" value="now" class="sr-only" />
<span class="dot-out"></span>
<span>
<span
class="text-[12.5px] font-semibold text-[color:var(--color-hub)] block leading-tight">
{{ __('Sofort nach Freigabe') }}
</span>
<span class="text-[11px] text-[color:var(--color-ink-3)] block mt-0.5 leading-tight">
{{ __('geht live, sobald sie veröffentlicht wird') }}
</span>
</span>
</label>
<label class="pr-pub-opt {{ $publishMode === 'scheduled' ? 'is-checked' : '' }}">
<input type="radio" wire:model.live="publishMode" value="scheduled" class="sr-only" />
<span class="dot-out"></span>
<span class="flex-1">
<span class="text-[12.5px] font-semibold text-[color:var(--color-hub)] leading-tight">
{{ __('Geplanter Termin') }}
</span>
<span class="text-[11px] text-[color:var(--color-ink-3)] block mt-0.5 leading-tight">
{{ __('Datum + Uhrzeit — wird automatisch zum Termin veröffentlicht') }}
</span>
</span>
</label>
@if ($publishMode === 'scheduled')
<div class="grid gap-3 sm:grid-cols-2">
<flux:field>
<flux:label>{{ __('Datum') }}</flux:label>
<flux:date-picker
wire:model.live="scheduledDate"
type="input"
:placeholder="__('Datum wählen')"
with-today
/>
<flux:error name="scheduledDate" />
</flux:field>
<flux:field>
<flux:label>{{ __('Uhrzeit') }}</flux:label>
<flux:time-picker
wire:model.live="scheduledTime"
type="input"
:placeholder="__('Uhrzeit wählen')"
/>
<flux:error name="scheduledTime" />
</flux:field>
</div>
<flux:error name="scheduledAt" />
<p class="text-[11px] text-[color:var(--color-ink-3)] leading-tight">{{ __('Frühestens 5 Min. in der Zukunft.') }}</p>
@endif
</div>
</article>
{{-- Weitere Felder --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Weitere Felder') }}</span>
</div>
<div class="p-5 space-y-3">
<flux:field>
<flux:label>{{ __('Backlink-URL') }}</flux:label>
<flux:input wire:model="backlinkUrl" type="url" placeholder="https://…" />
<flux:error name="backlinkUrl" />
</flux:field>
<flux:field>
<flux:label>{{ __('Sprache') }}</flux:label>
<flux:select wire:model="language">
<option value="de">Deutsch</option>
<option value="en">English</option>
</flux:select>
</flux:field>
<flux:checkbox wire:model="noExport" label="{{ __('Kein Export') }}" />
</div>
</article>
{{-- Aktionen --}}
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Aktionen') }}</span>
</div>
<div class="p-5 space-y-2">
<flux:button type="button" variant="primary" icon="bookmark" class="w-full" wire:click="save">
{{ __('Änderungen speichern') }}
</flux:button>
<flux:modal.trigger name="confirm-delete-press-release">
<flux:button type="button" variant="danger" icon="trash" class="w-full">
{{ __('Pressemitteilung löschen') }}
</flux:button>
</flux:modal.trigger>
</div>
</article>
{{-- Phase-2-Footer --}}
<div class="rounded-[5px] border p-3.5"
style="background:var(--color-accent-soft);border-color:color-mix(in srgb, var(--color-accent) 50%, transparent);">
<div class="flex items-center gap-2 mb-2">
<flux:icon name="sparkles" variant="micro"
class="size-3.5 text-[color:var(--color-accent-deep)]" />
<span class="eyebrow accent">{{ __('Phase 2 — bald') }}</span>
</div>
<ul
class="text-[11.5px] text-[color:var(--color-accent-deep)] leading-[1.55] list-none p-0 m-0 space-y-1">
<li>· {{ __('KI-Titel-Optimierung & KI-Lektorat live') }}</li>
<li>· {{ __('Versionshistorie & Kommentare') }}</li>
<li>· {{ __('Portal-Vorschau (presseecho vs. BP24)') }}</li>
</ul>
</div>
</aside>
</div>
<flux:modal name="confirm-status-change" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Status wirklich wechseln?') }}</flux:heading>
<flux:subheading>
{{ __('Aktuell: :current. Neuer Status: :target.', [
'current' => $statusEnum?->label() ?? $currentStatus,
'target' => $targetStatusEnum?->label() ?? $targetStatus,
]) }}
</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="primary" wire:click="changeStatus">{{ __('Status ändern') }}</flux:button>
</div>
</div>
</flux:modal>
<flux:modal name="confirm-delete-press-release" class="max-w-lg">
<div class="space-y-6">
<div>
<flux:heading size="lg">{{ __('Pressemitteilung löschen?') }}</flux:heading>
<flux:subheading>
@if ($currentStatus === 'published')
{{ __('Diese Pressemitteilung ist veröffentlicht. Sie wird nicht entfernt, sondern archiviert und der Inhalt wird durch den voreingestellten Ersatztext ersetzt, damit die URL keinen 404-Fehler erzeugt.') }}
@else
{{ __('Diese Pressemitteilung wird per Soft Delete aus den Standardlisten entfernt.') }}
@endif
</flux:subheading>
</div>
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
<flux:modal.close>
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
</flux:modal.close>
<flux:button variant="danger" wire:click="deletePressRelease">{{ __('Löschung bestätigen') }}
</flux:button>
</div>
</div>
</flux:modal>
</div>