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
|
|
@ -4,6 +4,7 @@ use App\Enums\PressReleaseStatus;
|
|||
use App\Models\PressRelease;
|
||||
use App\Services\Auth\MagicLinkGenerator;
|
||||
use App\Services\PressRelease\BlacklistViolationException;
|
||||
use App\Services\PressRelease\PressReleaseCoverImage;
|
||||
use App\Services\PressRelease\PressReleaseService;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Layout;
|
||||
|
|
@ -35,6 +36,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
try {
|
||||
app(PressReleaseService::class)->submitForReview($pr);
|
||||
} catch (BlacklistViolationException $e) {
|
||||
Flux::modal('confirm-submit-review')->close();
|
||||
|
||||
Flux::toast(
|
||||
heading: __('Automatisch abgelehnt'),
|
||||
text: __('Unzulässiges Wort gefunden: ":word". Bitte überarbeiten.', ['word' => $e->word]),
|
||||
|
|
@ -45,6 +48,8 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
return;
|
||||
}
|
||||
|
||||
Flux::modal('confirm-submit-review')->close();
|
||||
|
||||
Flux::toast(
|
||||
heading: __('Eingereicht'),
|
||||
text: __('Die Redaktion prüft die Pressemitteilung typischerweise innerhalb von 24 h.'),
|
||||
|
|
@ -78,9 +83,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
|
||||
}
|
||||
|
||||
$cover = app(PressReleaseCoverImage::class);
|
||||
$user = auth()->user();
|
||||
|
||||
return [
|
||||
'pr' => $pr,
|
||||
'categoryName' => $categoryName,
|
||||
'coverUrl' => $cover->coverUrl($pr, 'cover'),
|
||||
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
|
||||
'quotaTotal' => (int) $user->press_release_quota,
|
||||
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
|
||||
'canEdit' => auth()->user()->can('update', $pr)
|
||||
&& in_array($pr->status->value, ['draft', 'rejected']),
|
||||
'latestRejection' => $latestRejection,
|
||||
|
|
@ -133,6 +145,11 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<span class="badge hub dot">{{ __('User Backend') }}</span>
|
||||
<span class="eyebrow muted">{{ __('Mein Bereich · Pressemitteilung') }}</span>
|
||||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||||
@if ($pr->content_tier?->isPubliclyBadged())
|
||||
<span class="badge {{ $pr->content_tier === \App\Enums\PressReleaseContentTier::Hochwertig ? 'ok' : 'hub' }}">
|
||||
{{ $pr->content_tier === \App\Enums\PressReleaseContentTier::Hochwertig ? '★ ' : '✓ ' }}{{ $pr->content_tier->label() }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
|
||||
</div>
|
||||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||||
|
|
@ -154,19 +171,35 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
@if ($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
<flux:button variant="filled" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button variant="ghost" icon="link" wire:click="generateShareLink">
|
||||
<flux:button variant="filled" icon="link" wire:click="generateShareLink">
|
||||
{{ __('Vorschau-Link') }}
|
||||
</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ück') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{-- ============== TITELBILD (Hero) ============== --}}
|
||||
{{-- Harte Obergrenze 1280x580 px: Container deckelt Breite und Seitenverhältnis,
|
||||
damit das Bild auf großen Screens nicht über die Detailgröße hinauswächst. --}}
|
||||
<article class="panel overflow-hidden mx-auto w-full max-w-[1280px]">
|
||||
<div class="relative aspect-[1280/580] w-full">
|
||||
<img src="{{ $coverUrl }}" alt="{{ $pr->title }}"
|
||||
class="absolute inset-0 h-full w-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
@if ($coverIsPlaceholder)
|
||||
<div class="flex items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
|
||||
<flux:icon.photo variant="micro" class="size-3.5" />
|
||||
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</article>
|
||||
|
||||
{{-- ============== SHARE-LINK ERFOLG ============== --}}
|
||||
@if ($shareUrl)
|
||||
<article class="panel" style="border-color:var(--color-ok);">
|
||||
|
|
@ -224,14 +257,15 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</p>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
@if ($canEdit)
|
||||
<flux:button variant="ghost" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
<flux:button variant="filled" icon="pencil" href="{{ route('me.press-releases.edit', $pr->id) }}" wire:navigate>
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button type="button" variant="primary" wire:click="submitForReview"
|
||||
wire:confirm="{{ __('Pressemitteilung zur Prüfung einreichen?') }}">
|
||||
{{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }}
|
||||
</flux:button>
|
||||
<flux:modal.trigger name="confirm-submit-review">
|
||||
<flux:button type="button" variant="primary">
|
||||
{{ $pr->status === PressReleaseStatus::Rejected ? __('Erneut einreichen') : __('Zur Prüfung einreichen') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -261,7 +295,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<div class="panel-head">
|
||||
<span class="section-eyebrow">{{ __('Zugeordnete Pressekontakte') }}</span>
|
||||
@if ($pr->company)
|
||||
<flux:button size="sm" variant="ghost" icon="building-office" href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate>
|
||||
<flux:button size="sm" variant="filled" icon="building-office" href="{{ route('me.press-kits.show', $pr->company->id) }}" wire:navigate>
|
||||
{{ __('Firma') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
|
|
@ -341,7 +375,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Geplante Veröffentlichung') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->scheduled_at->format('d.m.Y H:i') }}
|
||||
{{ $pr->scheduledAtLocal()->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -349,7 +383,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Sperrfrist bis') }}</div>
|
||||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||||
{{ $pr->embargo_at->format('d.m.Y H:i') }}
|
||||
{{ $pr->embargoAtLocal()->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -434,6 +468,16 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
</div>
|
||||
</article>
|
||||
|
||||
{{-- ============== VERÖFFENTLICHUNGS-MODAL ============== --}}
|
||||
@if ($pr->status === PressReleaseStatus::Draft || $pr->status === PressReleaseStatus::Rejected)
|
||||
<x-press-release-submit-modal
|
||||
name="confirm-submit-review"
|
||||
action="submitForReview"
|
||||
:confirm-label="__('Veröffentlichung anfordern')"
|
||||
:quota-total="$quotaTotal"
|
||||
:quota-remaining="$quotaRemaining" />
|
||||
@endif
|
||||
|
||||
{{-- ============== BOILERPLATE-OVERRIDE ============== --}}
|
||||
@if ($pr->boilerplate_override)
|
||||
<article class="panel">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue