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>
This commit is contained in:
Kevin Adametz 2026-06-12 08:30:13 +00:00
parent 0efabaf446
commit a000238ca8
141 changed files with 5922 additions and 1001 deletions

View file

@ -1,6 +1,7 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleasePlaceholder;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
@ -8,6 +9,7 @@ use App\Models\Contact;
use App\Models\PressRelease;
use App\Services\Customer\CustomerCompanyContext;
use App\Services\PressRelease\BlacklistViolationException;
use App\Services\PressRelease\PressReleaseCoverImage;
use App\Services\PressRelease\PressReleaseHtmlSanitizer;
use App\Services\PressRelease\PressReleaseService;
use Flux\Flux;
@ -16,6 +18,7 @@ use Illuminate\Validation\Rule;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
@ -30,6 +33,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
public int|string|null $companyId = null;
public string $companySearch = '';
public int|string|null $categoryId = null;
public int|string|null $contactId = null;
@ -52,12 +57,24 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
public ?string $scheduledAt = null;
public ?string $scheduledDate = null;
public ?string $scheduledTime = null;
public bool $useEmbargo = false;
public ?string $embargoAt = null;
public string $currentStatus = '';
public ?int $contentScore = null;
public ?string $contentTier = null;
public string $placeholderVariant = '';
private ?PressRelease $cachedPressRelease = null;
public function mount(int $id): void
{
$this->id = $id;
@ -72,6 +89,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
);
$this->currentStatus = $pr->status->value;
$this->contentScore = $pr->content_score;
$this->contentTier = $pr->content_tier?->value;
$this->portal = $pr->portal->value;
$this->language = $pr->language;
$this->companyId = $pr->company_id;
@ -87,14 +106,27 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
?? $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 = $pr->scheduled_at->format('Y-m-d\TH:i');
$this->scheduledAt = $scheduledAt->format('Y-m-d\TH:i');
$this->scheduledDate = $scheduledAt->format('Y-m-d');
$this->scheduledTime = $scheduledAt->format('H:i');
}
if ($pr->embargo_at) {
$this->useEmbargo = true;
$this->embargoAt = $pr->embargo_at->format('Y-m-d\TH:i');
}
$this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($pr->placeholder_variant?->value)->value;
}
#[On('placeholder-selected')]
public function setPlaceholderVariant(string $variant): void
{
$this->placeholderVariant = PressReleasePlaceholder::fromValueOrDefault($variant)->value;
}
#[On('title-image-changed')]
public function refreshTitleImage(): void
{
unset($this->tags, $this->presubmitChecks);
}
public function updatedCompanyId(): void
@ -118,6 +150,35 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
unset($this->tags, $this->presubmitChecks);
}
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 addTag(string $tag): void
{
$tag = trim($tag);
@ -159,6 +220,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
* liegt, wird es bei jeder Änderung neu geprüft. So verschwindet ein
* roter Hinweis sofort, wenn der User das Feld korrekt ausfüllt und
* der User muss nicht erst auf „Speichern" klicken.
*
* 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
{
@ -214,22 +279,99 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
];
if ($this->publishMode === 'scheduled') {
$rules['scheduledAt'] = ['required', 'date', 'after:'.now()->addMinutes(5)->toIso8601String()];
$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'];
}
if ($this->useEmbargo) {
$rules['embargoAt'] = ['required', 'date', 'after:'.now()->toIso8601String()];
} else {
$rules['embargoAt'] = ['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 — Error-Bag wird automatisch befüllt.
}
}
public function save(bool $submitAfterSave = false): void
{
$this->syncScheduledAt();
$this->useEmbargo = false;
$this->embargoAt = null;
try {
$this->validate($this->formRules());
} catch (\Illuminate\Validation\ValidationException $e) {
@ -278,16 +420,17 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
'boilerplate_override' => $this->useBoilerplateOverride && trim($this->boilerplateOverride) !== ''
? trim($this->boilerplateOverride)
: null,
'placeholder_variant' => $this->placeholderVariant ?: PressReleasePlaceholder::default()->value,
'keywords' => $this->keywords ?: null,
'backlink_url' => $this->backlinkUrl ?: null,
'scheduled_at' => $this->publishMode === 'scheduled' && $this->scheduledAt
? \Carbon\Carbon::parse($this->scheduledAt)
: null,
'embargo_at' => $this->useEmbargo && $this->embargoAt
? \Carbon\Carbon::parse($this->embargoAt)
'scheduled_at' => $this->publishMode === 'scheduled'
? $this->scheduledAtUtc()
: null,
'embargo_at' => null,
]);
$contentChanged = $pr->wasChanged(['title', 'text']);
$pr->contacts()->sync($contact ? [$contact->id] : []);
if ($submitAfterSave) {
@ -314,6 +457,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
variant: 'success',
);
} else {
// Inhaltliche Änderung einer bereits geprüften/bewerteten PM → neu
// prüfen (Re-Check ohne Routing) und neu bewerten. Beim Einreichen
// übernimmt das submitForReview.
if ($contentChanged) {
$service = app(PressReleaseService::class);
$fresh = $pr->fresh();
$service->reclassifyIfClassified($fresh);
$service->rescoreIfScored($fresh);
}
Flux::toast(
heading: __('Gespeichert'),
text: __('Deine Änderungen sind gesichert.'),
@ -333,17 +486,24 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{
$user = auth()->user();
$context = app(CustomerCompanyContext::class);
$myCompanies = $context->companiesFor($user);
$selectedCompany = $this->selectedCompany();
$companyOptions = $context->searchCompaniesFor(
$user,
$this->companySearch,
$this->companyId ? (int) $this->companyId : null,
10,
);
$categories = Category::query()
->with('translations')
->where('is_active', true)
->orderBy('id')
->get();
$pressRelease = $this->getMyPR()->load('images');
$cover = app(PressReleaseCoverImage::class);
return [
'myCompanies' => $myCompanies,
'companyOptions' => $companyOptions,
'categories' => $categories,
'selectedCompany' => $selectedCompany,
'selectedCompanyContacts' => $selectedCompany
@ -351,6 +511,10 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
: Contact::query()->whereRaw('0 = 1')->get(),
'selectedPortalLabel' => $selectedCompany?->portal?->label() ?? __('Wird aus der Firma übernommen'),
'tagSuggestions' => $this->tagSuggestionsFor($selectedCompany),
'coverUrl' => $cover->coverUrl($pressRelease, 'cover'),
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pressRelease),
'quotaTotal' => (int) $user->press_release_quota,
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
];
}
@ -453,7 +617,9 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
private function getMyPR(): PressRelease
{
return PressRelease::withoutGlobalScopes()
// Pro Livewire-Request memoisiert: mount(), with() und save() greifen
// sonst jeweils mit einer eigenen Query auf dieselbe PM zu.
return $this->cachedPressRelease ??= PressRelease::withoutGlobalScopes()
->where('user_id', auth()->id())
->findOrFail($this->id);
}
@ -495,7 +661,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
}
}; ?>
<div class="space-y-8" x-data="{ tagInput: '' }">
<div class="space-y-8 pr-editor-shell" x-data="{ tagInput: '' }">
{{-- ============== PAGE HEADER ============== --}}
<header class="grid items-end gap-8" style="grid-template-columns:1fr auto;">
<div class="min-w-0">
@ -518,17 +684,17 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<flux:button variant="ghost" icon="eye" href="{{ route('me.press-releases.show', $id) }}" wire:navigate>
<flux:button variant="filled" icon="eye" href="{{ route('me.press-releases.show', $id) }}" wire:navigate>
{{ __('Vorschau / Detail') }}
</flux:button>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
<flux:button variant="filled" icon="arrow-left" href="{{ route('me.press-releases.index') }}" wire:navigate>
{{ __('Zur Liste') }}
</flux:button>
</div>
</header>
{{-- ============== 2-COLUMN GRID ============== --}}
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]">
<div class="grid gap-6 pr-editor-layout">
{{-- =================== LINKS: SCHREIBFLÄCHE =================== --}}
<div class="space-y-6 min-w-0">
@ -537,18 +703,42 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
<section class="panel">
<div class="p-4 flex flex-wrap items-center gap-4">
<span class="pr-form-label" style="margin-bottom:0;">{{ __('Für Firma') }}</span>
<flux:select wire:model.live="companyId" class="!w-auto min-w-[260px]">
<option value="">{{ __('Bitte wählen…') }}</option>
@foreach ($myCompanies as $c)
<option value="{{ $c->id }}">{{ $c->name }}</option>
@endforeach
</flux:select>
<div class="min-w-[260px]">
<flux:select
wire:model.live="companyId"
variant="combobox"
:filter="false"
clearable
placeholder="{{ __('Firma suchen…') }}"
>
<x-slot name="input">
<flux:select.input
wire:model.live.debounce.300ms="companySearch"
placeholder="{{ __('Name eingeben…') }}"
/>
</x-slot>
@foreach ($companyOptions as $company)
<flux:select.option :value="$company->id" wire:key="company-option-{{ $company->id }}">
{{ $company->name }}{{ $company->portal ? ' ('.$company->portal->abbreviation().')' : '' }}
</flux:select.option>
@endforeach
<x-slot name="empty">
<flux:select.option.empty>
@if (blank(trim($companySearch)))
{{ __('Keine Firma verfügbar.') }}
@else
{{ __('Keine Firma gefunden.') }}
@endif
</flux:select.option.empty>
</x-slot>
</flux:select>
</div>
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
{{ __('Boilerplate und Pressekontakt werden bei Wechsel angepasst.') }}
</span>
<span class="flex-1"></span>
@if ($selectedCompany)
<flux:button size="sm" variant="ghost" icon="building-office"
<flux:button size="sm" variant="filled" icon="building-office"
href="{{ route('me.press-kits.show', $selectedCompany->id) }}" wire:navigate>
{{ __('Firmenprofil') }}
</flux:button>
@ -655,7 +845,32 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</div>
</section>
{{-- 5) MEDIEN Image-Manager direkt eingebunden --}}
@if ($coverIsPlaceholder)
{{-- 5) TITELBILD-PLATZHALTER --}}
<section class="panel">
<div class="p-5">
<div class="mb-3 flex items-center justify-between gap-3">
<span class="pr-form-label" style="margin-bottom:0;">{{ __('Titelbild-Platzhalter') }}</span>
<flux:modal.trigger name="placeholder-picker">
<flux:button size="xs" variant="filled" icon="swatch">
{{ __('Platzhalter wählen') }}
</flux:button>
</flux:modal.trigger>
</div>
<x-portal.press-release-placeholder
:variant="$placeholderVariant"
:title="$title ?: null"
class="mx-auto aspect-[1280/580] w-full max-w-[1280px] rounded-[6px] border border-[color:var(--color-bg-rule)]" />
<p class="mt-2 text-[11.5px] text-[color:var(--color-ink-3)] m-0">
{{ __('Dieser Platzhalter wird angezeigt, bis ein eigenes Titelbild hochgeladen ist.') }}
</p>
</div>
</section>
<livewire:components.press-release-placeholder-picker :current="$placeholderVariant" />
@endif
{{-- 6) MEDIEN Image-Manager direkt eingebunden --}}
<livewire:components.press-release-images-manager :press-release-id="$id" :wire:key="'pr-images-'.$id" />
{{-- 6) ANHÄNGE TEMPORÄR DEAKTIVIERT
@ -718,7 +933,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{-- /Schreibfläche --}}
{{-- =================== RECHTS: SETTINGS-SIDEBAR =================== --}}
<aside class="space-y-4 lg:sticky lg:top-4 self-start">
<aside class="space-y-4 pr-editor-side self-start">
{{-- Status & Absenden --}}
<article class="panel" style="border-color:var(--color-hub);">
@ -766,17 +981,17 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
@endforeach
</div>
<flux:button
type="button"
variant="primary"
icon="paper-airplane"
class="w-full"
wire:click="saveAndSubmit"
wire:confirm="{{ __('Änderungen speichern und zur Prüfung einreichen?') }}"
wire:loading.attr="disabled"
>
{{ $currentStatus === 'rejected' ? __('Speichern & erneut einreichen') : __('Speichern & zur Prüfung') }}
</flux:button>
<flux:modal.trigger name="confirm-submit-review">
<flux:button
type="button"
variant="primary"
icon="paper-airplane"
class="w-full"
wire:loading.attr="disabled"
>
{{ $currentStatus === 'rejected' ? __('Speichern & erneut einreichen') : __('Speichern & zur Prüfung') }}
</flux:button>
</flux:modal.trigger>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ __('Warnungen blockieren nicht. Pflichtfelder blockieren. Die Redaktion prüft typ. innerhalb von 24h.') }}
</p>
@ -784,7 +999,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
<hr class="border-0 border-t border-[color:var(--color-bg-rule)] my-3" />
<flux:button
type="button"
variant="ghost"
variant="filled"
icon="bookmark"
class="w-full"
wire:click="save"
@ -795,6 +1010,42 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</div>
</article>
{{-- Content-Score (Qualitäts-Feedback während des Schreibens) --}}
@if (! is_null($contentScore))
@php
$tierEnum = $contentTier ? \App\Enums\PressReleaseContentTier::from($contentTier) : null;
$tiers = config('scoring.content_score.tiers');
$nextThreshold = $contentScore < (int) $tiers['gepruft']
? (int) $tiers['gepruft']
: ($contentScore < (int) $tiers['hochwertig'] ? (int) $tiers['hochwertig'] : null);
@endphp
<article class="panel">
<div class="panel-head">
<span class="section-eyebrow">{{ __('Qualität') }}</span>
@if ($tierEnum)
<span class="badge {{ $tierEnum === \App\Enums\PressReleaseContentTier::Hochwertig ? 'ok' : ($tierEnum === \App\Enums\PressReleaseContentTier::Geprueft ? 'hub' : 'muted') }}">{{ $tierEnum->label() }}</span>
@endif
</div>
<div class="p-4">
<div class="text-[26px] font-bold leading-none text-[color:var(--color-ink)]">
{{ $contentScore }}<span class="text-[14px] font-medium text-[color:var(--color-ink-3)]">/100</span>
</div>
@if ($nextThreshold)
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Noch :points Punkte bis zur nächsten Stufe.', ['points' => $nextThreshold - $contentScore]) }}
</p>
@else
<p class="text-[12px] text-[color:var(--color-ink-2)] mt-2 m-0">
{{ __('Höchste Stufe erreicht.') }}
</p>
@endif
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-2 leading-[1.45] m-0">
{{ __('Der Score wird nach dem Speichern automatisch neu berechnet.') }}
</p>
</div>
</article>
@endif
{{-- Kategorie (Pflichtfeld - prominent direkt unter Status) --}}
<article class="panel">
<div class="panel-head">
@ -856,7 +1107,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
{{ __('Diese Firma hat noch keine Pressekontakte.') }}
</p>
@if ($selectedCompany)
<flux:button size="sm" variant="ghost" icon="plus" class="w-full"
<flux:button size="sm" variant="filled" icon="plus" class="w-full"
href="{{ route('me.press-kits.show', $selectedCompany->id) }}" wire:navigate>
{{ __('Kontakt im Firmenprofil anlegen') }}
</flux:button>
@ -986,39 +1237,31 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</label>
@if ($publishMode === 'scheduled')
<flux:field>
<flux:label>{{ __('Veröffentlichungstermin') }}</flux:label>
<flux:input
wire:model.live="scheduledAt"
type="datetime-local"
:min="now()->addMinutes(5)->format('Y-m-d\\TH:i')"
/>
<flux:description>{{ __('Frühestens 5 Min. in der Zukunft.') }}</flux:description>
<flux:error name="scheduledAt" />
</flux:field>
@endif
<div class="border-t pt-3" style="border-color: var(--color-line);">
<flux:switch
wire:model.live="useEmbargo"
:label="__('Sperrfrist (Embargo) setzen')"
/>
<p class="text-[11px] text-[color:var(--color-ink-3)] mt-1 leading-tight">
{{ __('Die PM ist intern frei, aber öffentlich erst ab gewähltem Zeitpunkt sichtbar.') }}
</p>
@if ($useEmbargo)
<flux:field class="mt-3">
<flux:label>{{ __('Sperrfrist bis') }}</flux:label>
<flux:input
wire:model.live="embargoAt"
type="datetime-local"
:min="now()->format('Y-m-d\\TH:i')"
<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="embargoAt" />
<flux:error name="scheduledDate" />
</flux:field>
@endif
</div>
<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>
@ -1061,4 +1304,12 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung bearbeiten')] cl
</aside>
</div>
{{-- Einreichungs-Modal (öffnet über „Speichern & zur Prüfung") --}}
<x-press-release-submit-modal
name="confirm-submit-review"
action="saveAndSubmit"
:confirm-label="$currentStatus === 'rejected' ? __('Speichern & erneut einreichen') : __('Speichern & einreichen')"
:quota-total="$quotaTotal"
:quota-remaining="$quotaRemaining" />
</div>