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:
parent
0efabaf446
commit
a000238ca8
141 changed files with 5922 additions and 1001 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue