- legal_document_acceptances (insert-only), LegalAcceptanceGate, Pflicht-Schalter im Einreichungs-Modal über den gemeinsamen Trait; API: GET/POST /api/v1/legal/acceptances, submit antwortet 428 mit den geforderten Fassungen; LEGAL_ACCEPTANCE_GATE - ImageService: og-Variante (1200×630) mit halbtransparentem Balken und Inter SemiBold, regenerateOgVariant() für Korrekturen und Bestand; Fokuspunkt/Letterbox getestet - Uploader brennt den Nachweis beim Upload ein, ImageCorrectionService baut die OG-Variante nach einer Korrektur neu; Artikelseite nutzt og für og:image - Teaser: Nachweis in alt/title (Feed, Hero, Kategorie, Newsroom), Unterschrift am Hero-Bild; PressRelease::cardImageCredit() Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
259 lines
16 KiB
PHP
259 lines
16 KiB
PHP
@props([
|
|
'name' => 'confirm-submit-review',
|
|
'action',
|
|
'bookingAction' => null,
|
|
'confirmLabel' => null,
|
|
'quotaTotal' => null,
|
|
'quotaRemaining' => null,
|
|
'hasTitleImage' => true,
|
|
])
|
|
|
|
{{--
|
|
Wiederverwendbares Einreichungs-Modal für Pressemitteilungen.
|
|
Wird in Detailansicht, Bearbeiten und Erstellen eingebunden. Der
|
|
`action`-Prop bestimmt die Livewire-Methode der Eltern-Komponente, die beim
|
|
Bestätigen ausgeführt wird (z. B. `submitForReview`, `saveAndSubmit`,
|
|
`save('review')`). Quota-Block wird nur angezeigt, wenn Werte übergeben
|
|
sind — null bedeutet unbegrenztes Kontingent (Bestandsschutz bzw.
|
|
Launch-Schalter aus) und blendet den Block aus.
|
|
|
|
Submit-Gate (Decision-Update §5.1): Ohne aktive Buchung zeigt das Modal
|
|
statt des Prüf-Flows einen Buchungs-Hinweis — der Button konvertiert,
|
|
er verschwindet nicht. Serverseitig sichert submitForReview() das Gate ab.
|
|
|
|
`booking-action` (optional): Livewire-Methode, die den Entwurf zuerst
|
|
speichert und dann zur Buchungsseite leitet. ZWINGEND in Erstellen/Bearbeiten
|
|
setzen — sonst ginge der erfasste Inhalt beim Sprung verloren. Fehlt sie
|
|
(z. B. Detailansicht, dort ist bereits gespeichert), wird direkt verlinkt.
|
|
|
|
`has-title-image`: Ohne eigenes Titelbild (nur Platzhalter) entfällt die
|
|
Bildrechte-Bestätigung — es gibt nichts zu bestätigen. Default true, damit
|
|
die Checkbox im Zweifel lieber einmal zu viel erscheint.
|
|
--}}
|
|
{{-- Blockform, nicht Inline-Form: die Inline-Variante würde bis zum Ende des nächsten Blocks als Rohblock gelesen (BladeCompiler::storePhpBlocks, Guard: BladePhpBlockGuardTest). --}}
|
|
@php
|
|
$bookingRequired = ! (auth()->user()?->hasActiveBooking() ?? true);
|
|
// Zustimmungsgate (Bildkennzeichnung Block G): offene Fassungen von AGB
|
|
// und Richtlinien, die vor dem Einreichen aktiv angenommen werden müssen.
|
|
$legalGate = app(\App\Services\Legal\LegalAcceptanceGate::class);
|
|
$legalDocumentsToAccept = $legalGate->requiredFor(auth()->user())->map(fn ($document) => $legalGate->describe($document))->values();
|
|
$needsLegalAcceptance = $legalDocumentsToAccept->isNotEmpty();
|
|
@endphp
|
|
|
|
<flux:modal :name="$name" class="w-full max-w-xl">
|
|
@if ($bookingRequired)
|
|
<div class="space-y-5">
|
|
<div>
|
|
<flux:text class="eyebrow muted">{{ __('Veröffentlichung') }}</flux:text>
|
|
<flux:heading size="lg">{{ __('Buchung erforderlich') }}</flux:heading>
|
|
</div>
|
|
|
|
<div class="rounded-[6px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
|
<p class="m-0 mb-2">
|
|
{{ __('Zum Einreichen einer Pressemitteilung wird eine aktive Buchung benötigt. Ihr Entwurf wird dabei gespeichert und bleibt jederzeit bearbeitbar.') }}
|
|
</p>
|
|
<p class="m-0 text-[11.5px] text-[color:var(--color-ink-3)]">
|
|
{{ __('Nach der Buchung reichen Sie die Pressemitteilung mit einem Klick zur Prüfung ein.') }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<flux:modal.close>
|
|
<flux:button variant="filled">{{ __('Später') }}</flux:button>
|
|
</flux:modal.close>
|
|
@if ($bookingAction)
|
|
{{-- Entwurf zuerst speichern, dann zur Buchung — kein Datenverlust. --}}
|
|
<flux:button variant="primary" icon="credit-card"
|
|
wire:click="{{ $bookingAction }}" wire:loading.attr="disabled">
|
|
{{ __('Speichern & Buchung auswählen') }}
|
|
</flux:button>
|
|
@else
|
|
<flux:button variant="primary" :href="route('me.bookings.index')" wire:navigate>
|
|
{{ __('Buchung auswählen') }}
|
|
</flux:button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@else
|
|
{{-- Ohne eigenes Titelbild startet `images` auf true: die Checkbox ist
|
|
ausgeblendet und darf den Bestätigen-Button nicht blockieren. --}}
|
|
<div class="space-y-5" x-data="{ agb: false, images: {{ $hasTitleImage ? 'false' : 'true' }}, contact: false, waiver: false, ai: false, legal: {{ $needsLegalAcceptance ? 'false' : 'true' }} }">
|
|
<div>
|
|
<flux:text class="eyebrow muted">{{ __('Veröffentlichung') }}</flux:text>
|
|
<flux:heading size="lg">{{ __('Pressemitteilung zur Prüfung freigeben') }}</flux:heading>
|
|
</div>
|
|
|
|
{{-- E3 (16.09.2026): unmissverständlich, dass die Freigabe nicht
|
|
„online" heißt — erst die Prüfung, dann die Veröffentlichung. --}}
|
|
@php
|
|
$maxAttempts = max(1, (int) config('scoring.classification.max_automatic_attempts', 3));
|
|
@endphp
|
|
<article class="panel overflow-hidden" style="border-color:var(--color-hub); border-left-width:3px;">
|
|
<div class="p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
|
<p class="m-0 mb-2 font-semibold text-[color:var(--color-ink)]">{{ __('Was nach der Freigabe passiert') }}</p>
|
|
<ol class="m-0 list-decimal space-y-1 ps-5">
|
|
<li>{{ __('Der Text geht zur automatischen Inhaltsprüfung — er ist damit noch nicht online.') }}</li>
|
|
<li>{{ __('Grün oder Gelb: die Mitteilung wird veröffentlicht. Rot: sie kommt mit Begründung zurück, Sie können nachbessern und erneut freigeben.') }}</li>
|
|
<li>{{ __('Nach :max Prüfungen mit Rot sieht sich die Redaktion den Text an und entscheidet.', ['max' => $maxAttempts]) }}</li>
|
|
</ol>
|
|
</div>
|
|
</article>
|
|
|
|
{{-- Rechtliche Hinweise (Platzhalter — vor Go-Live anwaltlich prüfen) --}}
|
|
<article class="panel overflow-hidden">
|
|
<div class="panel-head" style="padding:10px 16px;">
|
|
<span class="section-eyebrow">{{ __('Rechtliche Hinweise') }}</span>
|
|
</div>
|
|
<div class="p-4 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
|
<p class="m-0 mb-2 font-semibold text-[color:var(--color-ink)]">{{ __('Mit der Freigabe versichern Sie:') }}</p>
|
|
<ul class="m-0 list-disc space-y-1 ps-5">
|
|
<li>{{ __('Sie sind befugt, den Inhalt zu veröffentlichen.') }}</li>
|
|
<li>{{ __('Alle verwendeten Bilder, Logos und Zitate liegen in Ihrer Nutzungsbefugnis.') }}</li>
|
|
<li>{{ __('Personenbezogene Daten sind nur im zwingend erforderlichen Umfang enthalten.') }}</li>
|
|
<li>{{ __('Aussagen entsprechen Ihrem Wissensstand und sind sachlich richtig.') }}</li>
|
|
</ul>
|
|
<p class="m-0 mt-3 pt-3 border-t border-[color:var(--color-bg-rule)] text-[11.5px] text-[color:var(--color-ink-3)]">
|
|
{{ __('Sie stellen die Plattform von Ansprüchen Dritter frei, die aus einer unberechtigten Nutzung von Inhalten resultieren. Die endgültige Veröffentlichung erfolgt nach Abschluss der Inhaltsprüfung.') }}
|
|
</p>
|
|
</div>
|
|
</article>
|
|
|
|
{{-- Kontingent (optional) --}}
|
|
@if (! is_null($quotaRemaining) && ! is_null($quotaTotal))
|
|
<article class="panel overflow-hidden">
|
|
<div class="flex items-center justify-between gap-4 px-4 py-3">
|
|
<div class="min-w-0 text-[12.5px] text-[color:var(--color-ink-2)]">
|
|
<div class="font-semibold text-[color:var(--color-ink)]">{{ __('PM-Kontingent diesen Monat') }}</div>
|
|
<div class="text-[color:var(--color-ink-3)]">{{ __('Wird erst bei Veröffentlichung verbraucht — abgelehnte Pressemitteilungen kosten keinen Slot.') }}</div>
|
|
</div>
|
|
{{-- shrink-0 + nowrap: der Flex-Container darf den Badge
|
|
nicht zusammendrücken (QA-Befund 07.07., Umbruch „3 /|3"). --}}
|
|
<span @class(['badge', 'dot', 'shrink-0', 'whitespace-nowrap', $quotaRemaining > 0 ? 'ok' : 'warn'])>
|
|
{{ $quotaRemaining }} / {{ $quotaTotal }}
|
|
</span>
|
|
</div>
|
|
</article>
|
|
@endif
|
|
|
|
@if ($needsLegalAcceptance)
|
|
{{-- Zustimmungsgate (Block G): aktiver Klick je Fassung, kein
|
|
„Weiternutzung gilt als Zustimmung". Wire-gebunden, weil die
|
|
Annahme protokolliert wird (legal_document_acceptances). --}}
|
|
<article class="panel overflow-hidden" data-legal-acceptance>
|
|
<div class="panel-head" style="padding:10px 16px;">
|
|
<span class="section-eyebrow">{{ __('Neue Fassung unserer Bedingungen') }} <span class="text-[color:var(--color-err)]">*</span></span>
|
|
</div>
|
|
<div class="p-4 space-y-3 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
|
<p class="m-0">{{ __('Vor dem Einreichen nehmen Sie bitte die aktuelle Fassung an:') }}</p>
|
|
<ul class="m-0 list-disc space-y-1 ps-5">
|
|
@foreach ($legalDocumentsToAccept as $legalDocument)
|
|
<li><a href="{{ $legalDocument['url'] }}" target="_blank" rel="noopener" class="underline underline-offset-2">{{ $legalDocument['label'] }}</a> — {{ __('Version :version', ['version' => $legalDocument['version']]) }}</li>
|
|
@endforeach
|
|
</ul>
|
|
<flux:switch align="left" wire:model="legalAcceptanceConfirmed" x-on:change="legal = $event.target.checked"
|
|
:label="__('Ich habe die genannten Fassungen gelesen und nehme sie an.')" />
|
|
<flux:error name="legalAcceptanceConfirmed" />
|
|
</div>
|
|
</article>
|
|
@endif
|
|
|
|
{{-- KI-Kennzeichnung (Art. 50 KI-VO, Decision-Update 08.07.2026):
|
|
Pflicht-Selbstdeklaration in jedem Veröffentlichungsprozess.
|
|
Die Eltern-Komponente stellt die Properties über den Trait
|
|
App\Livewire\Concerns\InteractsWithAiDeclaration bereit. --}}
|
|
<article class="panel overflow-hidden">
|
|
<div class="panel-head" style="padding:10px 16px;">
|
|
<span class="section-eyebrow">
|
|
{{ __('KI-Kennzeichnung') }}
|
|
<span class="text-[color:var(--color-err)]">*</span>
|
|
</span>
|
|
</div>
|
|
<div class="p-4 space-y-3">
|
|
<p class="m-0 text-[12.5px] leading-[1.6] text-[color:var(--color-ink-2)]">
|
|
{{ __('Wie wurde der Text dieser Pressemitteilung erstellt? Diese Angabe ist gesetzlich verpflichtend (Art. 50 KI-VO).') }}
|
|
</p>
|
|
{{-- Zwei Gruppen (Entscheidung 10.07.2026): Option 1 = kein
|
|
Label, Option 2 = Hinweis „KI-generiert" am Pressetext. --}}
|
|
<flux:radio.group wire:model="aiTextDeclaration">
|
|
@foreach (\App\Enums\AiTextDeclaration::formCases() as $aiOption)
|
|
<flux:radio :value="$aiOption->value" :label="$aiOption->label()" />
|
|
@endforeach
|
|
</flux:radio.group>
|
|
<flux:error name="aiTextDeclaration" />
|
|
<p class="m-0 pt-2 border-t border-[color:var(--color-bg-rule)] text-[11.5px] leading-[1.5] text-[color:var(--color-ink-3)]">
|
|
{{ __('Bei der ersten Option erscheint kein Label an der Veröffentlichung. Bei der zweiten Option wird der Pressetext mit dem Hinweis „KI-generiert" in Ihrer Stimme gekennzeichnet („Nach Angaben des einreichenden Unternehmens …"). KI-generierte oder KI-bearbeitete Bilder kennzeichnen Sie direkt beim Bild-Upload.') }}
|
|
</p>
|
|
</div>
|
|
</article>
|
|
|
|
{{-- Bestätigungen — Flux-Switches (Einheitlichkeit, 12.06.2026);
|
|
der Alpine-State steuert weiterhin den Bestätigen-Button. --}}
|
|
<article class="panel overflow-hidden">
|
|
<div class="panel-head" style="padding:10px 16px;">
|
|
<span class="section-eyebrow">{{ __('Ihre Bestätigungen') }}</span>
|
|
<span class="shrink-0 whitespace-nowrap text-[11.5px] tabular-nums text-[color:var(--color-ink-3)]"
|
|
x-text="[agb, {{ $hasTitleImage ? 'images, ' : '' }}contact, waiver, ai].filter(Boolean).length + ' / {{ $hasTitleImage ? 5 : 4 }}'"></span>
|
|
</div>
|
|
<div class="divide-y divide-[color:var(--color-bg-rule)]">
|
|
<div class="px-4 py-3">
|
|
<flux:switch
|
|
align="left"
|
|
x-on:change="agb = $event.target.checked"
|
|
:label="__('Der Inhalt entspricht den AGB und gesetzlichen Vorgaben.')"
|
|
/>
|
|
</div>
|
|
@if ($hasTitleImage)
|
|
<div class="px-4 py-3">
|
|
<flux:switch
|
|
align="left"
|
|
x-on:change="images = $event.target.checked"
|
|
:label="__('Alle Bildrechte sind geklärt.')"
|
|
/>
|
|
</div>
|
|
@endif
|
|
<div class="px-4 py-3">
|
|
<flux:switch
|
|
align="left"
|
|
x-on:change="contact = $event.target.checked"
|
|
:label="__('Die Angaben zum Pressekontakt sind korrekt.')"
|
|
/>
|
|
</div>
|
|
{{-- Archiv-Hinweis (AGB § 13 Abs. 1, T2neu): die frühere
|
|
Erlöschens-Checkbox entfiel mit der B2B-Umstellung (E2).
|
|
Der Bestätigungszeitpunkt wird serverseitig protokolliert
|
|
(PressReleaseService). --}}
|
|
<div class="px-4 py-3">
|
|
<flux:switch
|
|
align="left"
|
|
x-on:change="waiver = $event.target.checked"
|
|
:label="__('Mir ist bekannt, dass veröffentlichte Pressemitteilungen dauerhaft archiviert werden; eine spätere Entfernung ist nur als kostenpflichtige Serviceleistung möglich (gesetzliche Ansprüche bleiben unberührt).')"
|
|
/>
|
|
</div>
|
|
{{-- Verbindliche KI-Bestätigung (Art. 50 KI-VO): wire-gebunden,
|
|
weil der Bestätigungszeitpunkt zur Nachweisbarkeit im
|
|
Audit-Trail persistiert wird. --}}
|
|
<div class="px-4 py-3">
|
|
<flux:switch
|
|
align="left"
|
|
wire:model="aiDeclarationConfirmed"
|
|
x-on:change="ai = $event.target.checked"
|
|
:label="__('Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.')"
|
|
/>
|
|
<flux:error name="aiDeclarationConfirmed" />
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<div class="flex justify-end gap-2">
|
|
<flux:modal.close>
|
|
<flux:button variant="filled">{{ __('Abbrechen') }}</flux:button>
|
|
</flux:modal.close>
|
|
<flux:button variant="primary" wire:click="{{ $action }}"
|
|
wire:loading.attr="disabled"
|
|
x-bind:disabled="! (agb && images && contact && waiver && ai && legal)">
|
|
{{ $confirmLabel ?? __('Zur Prüfung freigeben') }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</flux:modal>
|