Decision-Update Abschnitt 12 (E15, S24): Sichtung auf offensichtliche Probleme, keine Rechteprüfung, öffentlich weiterhin „automatisierte Prüfung". - ImagePrecheckManager mit openai- und deterministic-Treiber (Muster der Text-Prüfstrecke), Bild auf 1024 px, Metadaten gestrippt, JPEG als Data-URI, Antwortschema 12.4; Fehler → Ausgang `error`, durchlassen mit Vermerk. - Auswertung 12.3: reject (Stock-Wasserzeichen, Nacktheit/Gewalt, Dokument ab 0,85), queue (Screenshot, fremdes Logo, Kennzeichen ab 0,6 sowie Ablehnungskategorien 0,6–0,85), hint (Personen, „wirkt KI" nur bei „ohne KI", technischer Mangel). Stufe 1 wertet nur das Wasserzeichen; die KI setzt nie die Kennzeichnung (E9). Herabstufung per Config (12.8). - Uploader: Check vor jedem Schreiben, Ablehnung speichert nichts und landet als ki_audits `image_precheck` an der PM; Hash-Bindung zwischen Vorprüfung im Formular und Speichern; Spalten precheck*, Snapshot `precheck_snapshot` im Bestätigungsprotokoll, Archiv-Payload erweitert. - Queue: Bild zurückgehalten (withheld_at), Meldung läuft weiter; Admin entscheidet mit Vermerk (Freigabe/Ablehnung → ki_audits `image_precheck_review`); Ansicht „Bild-Sichtung" in der PM-Liste, Sichtungsblock auf der PM-Seite, Systemstatus-Zähler. - Formular v2: Ablehnungsgrund unter dem Dateifeld, Hinweise nach der Dateiauswahl, „wird kurz gesichtet" an der Karte; API 422 mit Grund, Warnung `image_under_review`. - Config images.precheck (IMAGE_PRECHECK aus, STAGE 1); 17 Tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
956 lines
52 KiB
PHP
956 lines
52 KiB
PHP
<?php
|
||
|
||
use App\Models\PressRelease;
|
||
use App\Services\PressRelease\BlacklistViolationException;
|
||
use App\Services\PressRelease\PressReleaseCoverImage;
|
||
use App\Enums\AiImageDeclaration;
|
||
use App\Services\PressRelease\ImageRightsConfirmationRequiredException;
|
||
use App\Enums\ImagePrecheckCategory;
|
||
use App\Services\Image\Precheck\ImagePrecheckReviewService;
|
||
use App\Services\PressRelease\Images\ImageCorrectionService;
|
||
use App\Services\PressRelease\PressReleaseService;
|
||
use Flux\Flux;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Locked;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Volt\Component;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends Component
|
||
{
|
||
use \App\Livewire\Concerns\HandlesAdminPressReleaseActions;
|
||
|
||
#[Locked]
|
||
public int $id;
|
||
|
||
public string $rejectReason = '';
|
||
|
||
public function mount(int $id): void
|
||
{
|
||
$this->id = $id;
|
||
}
|
||
|
||
public function publish(): void
|
||
{
|
||
$pr = $this->findAdminPR($this->id);
|
||
|
||
try {
|
||
app(PressReleaseService::class)->publish($pr);
|
||
} catch (ImageRightsConfirmationRequiredException $e) {
|
||
Flux::toast(heading: __('Titelbild nicht bestätigt'), text: $e->getMessage(), variant: 'danger', duration: 8000);
|
||
Flux::modal('confirm-show-publish')->close();
|
||
|
||
return;
|
||
} catch (BlacklistViolationException $e) {
|
||
Flux::toast(
|
||
heading: __('Automatisch abgelehnt'),
|
||
text: __('Unzulässiges Wort gefunden: ":word".', ['word' => $e->word]),
|
||
variant: 'danger',
|
||
duration: 8000,
|
||
);
|
||
Flux::modal('confirm-show-publish')->close();
|
||
|
||
return;
|
||
}
|
||
|
||
Flux::toast(text: __('Pressemitteilung veröffentlicht. Autor wurde benachrichtigt.'), variant: 'success');
|
||
Flux::modal('confirm-show-publish')->close();
|
||
}
|
||
|
||
public function reject(): void
|
||
{
|
||
$this->validate([
|
||
'rejectReason' => ['required', 'string', 'min:5', 'max:2000'],
|
||
]);
|
||
|
||
$pr = $this->findAdminPR($this->id);
|
||
app(PressReleaseService::class)->reject($pr, trim($this->rejectReason));
|
||
|
||
$this->rejectReason = '';
|
||
|
||
Flux::toast(text: __('Pressemitteilung abgelehnt. Autor wurde benachrichtigt.'), variant: 'warning');
|
||
Flux::modal('confirm-show-reject')->close();
|
||
}
|
||
|
||
public function archive(): void
|
||
{
|
||
$pr = $this->findAdminPR($this->id);
|
||
app(PressReleaseService::class)->archive($pr);
|
||
Flux::toast(text: __('Archiviert.'), variant: 'success');
|
||
Flux::modal('confirm-show-archive')->close();
|
||
}
|
||
|
||
/**
|
||
* Korrekturweg (Bildkennzeichnung E.6): Bildnachweis oder
|
||
* Portalkennzeichnung mit Pflichtgrund ändern, Bild vorläufig zurückhalten.
|
||
*/
|
||
public string $correctionCredit = '';
|
||
|
||
public string $correctionAiDeclaration = '';
|
||
|
||
public string $correctionReason = '';
|
||
|
||
public function correctImage(ImageCorrectionService $corrections): void
|
||
{
|
||
$pr = $this->findAdminPR($this->id);
|
||
$image = $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->first();
|
||
|
||
if ($image === null) {
|
||
return;
|
||
}
|
||
|
||
$this->validate([
|
||
'correctionReason' => ['required', 'string', 'min:5', 'max:1000'],
|
||
'correctionCredit' => ['nullable', 'string', 'max:255'],
|
||
'correctionAiDeclaration' => ['nullable', \Illuminate\Validation\Rule::enum(AiImageDeclaration::class)],
|
||
], [
|
||
'correctionReason.required' => __('Bitte geben Sie den Grund der Korrektur an.'),
|
||
]);
|
||
|
||
$admin = auth()->user();
|
||
$changed = 0;
|
||
|
||
if ($this->correctionCredit !== '' && $this->correctionCredit !== (string) $image->copyright) {
|
||
$corrections->correctCredit($image, $admin, trim($this->correctionCredit), $this->correctionReason);
|
||
$changed++;
|
||
}
|
||
|
||
if ($this->correctionAiDeclaration !== '' && $this->correctionAiDeclaration !== $image->ai_declaration?->value) {
|
||
$corrections->correctAiDeclaration($image, $admin, AiImageDeclaration::from($this->correctionAiDeclaration), $this->correctionReason);
|
||
$changed++;
|
||
}
|
||
|
||
$this->reset(['correctionCredit', 'correctionAiDeclaration', 'correctionReason']);
|
||
|
||
Flux::toast(
|
||
text: $changed > 0 ? __('Korrektur protokolliert und übernommen.') : __('Keine Änderung: Die Angaben entsprechen dem aktuellen Stand.'),
|
||
variant: $changed > 0 ? 'success' : 'warning',
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Bild-Sichtung (Bildkennzeichnung Block K, DU 12.3): Ausgang `queue`
|
||
* des Vorab-Checks braucht eine menschliche Entscheidung mit Vermerk.
|
||
*/
|
||
public string $precheckNote = '';
|
||
|
||
public function approvePrecheck(ImagePrecheckReviewService $reviews): void
|
||
{
|
||
$this->decidePrecheck($reviews, true);
|
||
}
|
||
|
||
public function rejectPrecheck(ImagePrecheckReviewService $reviews): void
|
||
{
|
||
$this->decidePrecheck($reviews, false);
|
||
}
|
||
|
||
private function decidePrecheck(ImagePrecheckReviewService $reviews, bool $approve): void
|
||
{
|
||
$pr = $this->findAdminPR($this->id);
|
||
$image = $pr->images()->awaitingPrecheckReview()->orderByDesc('is_preview')->orderBy('sort_order')->first();
|
||
|
||
if ($image === null) {
|
||
Flux::toast(text: __('Kein Bild wartet auf eine Sichtung.'), variant: 'warning');
|
||
|
||
return;
|
||
}
|
||
|
||
$this->validate(
|
||
['precheckNote' => ['required', 'string', 'min:5', 'max:1000']],
|
||
['precheckNote.required' => __('Bitte geben Sie einen Vermerk zur Entscheidung an.')],
|
||
);
|
||
|
||
$approve
|
||
? $reviews->approve($image, auth()->user(), trim($this->precheckNote))
|
||
: $reviews->reject($image, auth()->user(), trim($this->precheckNote));
|
||
|
||
$this->reset(['precheckNote']);
|
||
|
||
Flux::toast(text: $approve ? __('Bild freigegeben und sichtbar.') : __('Bild abgelehnt und entfernt.'), variant: 'success');
|
||
}
|
||
|
||
public function toggleWithheld(ImageCorrectionService $corrections): void
|
||
{
|
||
$pr = $this->findAdminPR($this->id);
|
||
$image = $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->first();
|
||
|
||
if ($image === null) {
|
||
return;
|
||
}
|
||
|
||
$this->validate(
|
||
['correctionReason' => ['required', 'string', 'min:5', 'max:1000']],
|
||
['correctionReason.required' => __('Bitte geben Sie den Grund an.')],
|
||
);
|
||
|
||
$image->isWithheld()
|
||
? $corrections->release($image, auth()->user(), $this->correctionReason)
|
||
: $corrections->withhold($image, auth()->user(), $this->correctionReason);
|
||
|
||
$this->reset(['correctionReason']);
|
||
|
||
Flux::toast(text: $image->fresh()->isWithheld() ? __('Bild vorläufig zurückgehalten.') : __('Bild wieder freigegeben.'), variant: 'success');
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
$pr = PressRelease::withoutGlobalScopes()
|
||
->with([
|
||
'company:id,name,email,phone,website,address,country_code,slug',
|
||
'company.users:users.id,users.name,users.email',
|
||
'category.translations',
|
||
'user:id,name,email',
|
||
'images',
|
||
'attachments',
|
||
'contacts' => fn ($query) => $query
|
||
->withoutGlobalScopes()
|
||
->orderBy('last_name')
|
||
->orderBy('first_name')
|
||
->select(['contacts.id', 'contacts.company_id', 'contacts.first_name', 'contacts.last_name', 'contacts.responsibility', 'contacts.email', 'contacts.phone']),
|
||
'statusLogs.changedBy:id,name',
|
||
'kiAudits',
|
||
])
|
||
->findOrFail($this->id);
|
||
|
||
$latestClassification = $pr->kiAudits
|
||
->firstWhere('type', \App\Models\KiAudit::TYPE_CLASSIFICATION);
|
||
|
||
$latestRejection = null;
|
||
if ($pr->status->value === 'rejected') {
|
||
$latestRejection = $pr->statusLogs
|
||
->firstWhere(fn ($log) => $log->to_status?->value === 'rejected');
|
||
}
|
||
|
||
$cover = app(PressReleaseCoverImage::class);
|
||
|
||
return [
|
||
'pr' => $pr,
|
||
'statusLogs' => $pr->statusLogs,
|
||
'contacts' => $pr->contacts,
|
||
'latestClassification' => $latestClassification,
|
||
'latestRejection' => $latestRejection,
|
||
'coverUrl' => $cover->coverUrl($pr, 'cover'),
|
||
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
|
||
'categoryName' => $pr->category?->translations->firstWhere('locale', 'de')?->name
|
||
?? $pr->category?->translations->first()?->name
|
||
?? '–',
|
||
];
|
||
}
|
||
}; ?>
|
||
|
||
<div class="space-y-8">
|
||
@php
|
||
$statusClass = $pr->status->badgeClass();
|
||
@endphp
|
||
|
||
{{-- Flash-Banner ersetzt durch <flux:toast /> im Layout. --}}
|
||
|
||
{{-- ============== PAGE HEADER ============== --}}
|
||
<header class="page-header">
|
||
<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 · Pressemitteilung') }}</span>
|
||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||
@if ($pr->classification)
|
||
@php
|
||
$kiBadgeClass = match ($pr->classification) {
|
||
\App\Enums\PressReleaseClassification::Green => 'ok',
|
||
\App\Enums\PressReleaseClassification::Yellow => 'warn',
|
||
\App\Enums\PressReleaseClassification::Red => 'err',
|
||
};
|
||
@endphp
|
||
<span @class(['badge', $kiBadgeClass])>{{ __('KI: :label', ['label' => $pr->classification->label()]) }}</span>
|
||
@endif
|
||
@if (! is_null($pr->content_score) && $pr->content_tier)
|
||
@php
|
||
$tierBadge = match ($pr->content_tier) {
|
||
\App\Enums\PressReleaseContentTier::Hochwertig => 'ok',
|
||
\App\Enums\PressReleaseContentTier::Geprueft => 'hub',
|
||
\App\Enums\PressReleaseContentTier::Standard => 'muted',
|
||
};
|
||
@endphp
|
||
<span @class(['badge', $tierBadge])>{{ __('Score :score · :tier', ['score' => $pr->content_score, 'tier' => $pr->content_tier->label()]) }}</span>
|
||
@endif
|
||
<span class="badge hub">{{ strtoupper($pr->language) }}</span>
|
||
<span class="badge hub">{{ $pr->portal->label() }}</span>
|
||
</div>
|
||
<h1 class="text-[30px] font-bold tracking-[-0.6px] leading-[1.15] m-0 text-[color:var(--color-ink)]">
|
||
{{ $pr->title }}
|
||
</h1>
|
||
@if ($pr->subtitle)
|
||
<p class="text-[18px] font-medium tracking-[-0.2px] leading-[1.35] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||
{{ $pr->subtitle }}
|
||
</p>
|
||
@endif
|
||
<p class="text-[13px] leading-[1.55] mt-2 m-0 max-w-[720px] text-[color:var(--color-ink-2)]">
|
||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Firma') }}:</strong>
|
||
@if ($pr->company)
|
||
<a href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->company->name }}
|
||
</a>
|
||
@else
|
||
–
|
||
@endif
|
||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Kategorie') }}:</strong>
|
||
{{ $categoryName }}
|
||
<span class="text-[color:var(--color-bg-rule)] mx-1">·</span>
|
||
<strong class="font-semibold text-[color:var(--color-ink)]">{{ __('Autor') }}:</strong>
|
||
@if ($pr->user)
|
||
<a href="{{ route('admin.users.show', $pr->user->id) }}" wire:navigate
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->user->name }}
|
||
</a>
|
||
@else
|
||
–
|
||
@endif
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<flux:button variant="filled" icon="pencil" href="{{ route('admin.press-releases.edit', $pr->id) }}" wire:navigate>
|
||
{{ __('Bearbeiten') }}
|
||
</flux:button>
|
||
<flux:button variant="filled" icon="arrow-left" href="{{ route('admin.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 (kein eigenes Bild hochgeladen).') }}</span>
|
||
</div>
|
||
@else
|
||
@php
|
||
$adminTitleImage = $pr->images->sortByDesc('is_preview')->first();
|
||
@endphp
|
||
@if ($adminTitleImage && ($adminCreditInfo = \App\Support\ImageCredit::for($adminTitleImage, $pr->company))->hasDetails())
|
||
{{-- Bildnachweis, KI-Kennzeichnung, Herkunft und Bestätigungsstand
|
||
(Bildkennzeichnung D.1/D.3) --}}
|
||
<div class="flex flex-wrap 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)]">
|
||
@if ($adminCreditInfo->aiLabel)
|
||
<span class="badge hub">{{ $adminCreditInfo->aiLabel }}</span>
|
||
@if ($adminCreditInfo->aiSourceLabel)
|
||
<span class="text-[color:var(--color-ink-4)]">({{ $adminCreditInfo->aiSourceLabel }})</span>
|
||
@endif
|
||
@endif
|
||
@if ($adminCreditInfo->credit)
|
||
<span>{{ $adminCreditInfo->creditLabel }}: {{ $adminCreditInfo->credit }}</span>
|
||
@endif
|
||
@if ($adminCreditInfo->confirmation)
|
||
<span class="text-[color:var(--color-ink-4)]">· {{ __('Rechte bestätigt am :date (:key, AGB :agb, :channel)', ['date' => $adminCreditInfo->confirmation->confirmed_at->format('d.m.Y H:i'), 'key' => $adminCreditInfo->confirmation->legal_text_key, 'agb' => $adminCreditInfo->confirmation->agb_version ?? '–', 'channel' => $adminCreditInfo->confirmation->channel->value]) }}</span>
|
||
@elseif ($adminTitleImage->awaitsCustomerConfirmation())
|
||
<span class="badge">{{ __('wartet auf Kundenbestätigung') }}</span>
|
||
@elseif ($adminCreditInfo->isLegacy)
|
||
<span class="text-[color:var(--color-ink-4)]">· {{ __('Bestand ohne Bestätigungsprotokoll') }}</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
@endif
|
||
</article>
|
||
|
||
{{-- ============== BILD-SICHTUNG (BLOCK K) ============== --}}
|
||
@if (isset($adminTitleImage) && $adminTitleImage && $adminTitleImage->awaitsPrecheckReview())
|
||
<article class="panel" data-image-precheck-review style="border-color:var(--color-warn); border-left-width:3px;">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Bild-Sichtung: Vorab-Check hat etwas gefunden') }}</span>
|
||
<span class="badge warn">{{ __('wartet auf Entscheidung') }}</span>
|
||
</div>
|
||
<div class="p-5 space-y-3 max-w-[760px]">
|
||
<p class="m-0 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Das Bild ist bis zur Entscheidung zurückgehalten; die Meldung läuft normal weiter. Freigabe macht das Bild sichtbar, Ablehnung entfernt es. Beides wird mit Vermerk protokolliert.') }}
|
||
</p>
|
||
<ul class="m-0 list-disc pl-5 text-[13px]">
|
||
@foreach (($adminTitleImage->precheck['evaluation']['triggers'] ?? []) as $precheckTrigger)
|
||
<li>
|
||
{{ ImagePrecheckCategory::tryFrom($precheckTrigger['category'] ?? '')?->label() ?? $precheckTrigger['category'] }}
|
||
<span class="text-[color:var(--color-ink-4)]">({{ number_format((float) ($precheckTrigger['confidence'] ?? 0) * 100, 0) }} %)</span>
|
||
@if (! empty($precheckTrigger['note']))
|
||
— {{ $precheckTrigger['note'] }}
|
||
@endif
|
||
</li>
|
||
@endforeach
|
||
</ul>
|
||
<flux:textarea wire:model="precheckNote" :label="__('Vermerk (Pflicht)')" rows="2" />
|
||
<div class="flex flex-wrap gap-2">
|
||
<flux:button variant="primary" wire:click="approvePrecheck">{{ __('Bild freigeben') }}</flux:button>
|
||
<flux:button variant="danger" wire:click="rejectPrecheck" wire:confirm="{{ __('Bild wirklich ablehnen und entfernen?') }}">{{ __('Bild ablehnen') }}</flux:button>
|
||
<flux:button variant="ghost" size="sm" href="{{ route('admin.press-releases.images.original', ['id' => $pr->id, 'image' => $adminTitleImage->id, 'purpose' => 'Bild-Sichtung nach Vorab-Check']) }}" target="_blank">
|
||
{{ __('Beweisoriginal öffnen') }}
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@elseif (isset($adminTitleImage) && $adminTitleImage && $adminTitleImage->precheck_reviewed_at !== null)
|
||
<p class="m-0 text-[12px] text-[color:var(--color-ink-4)]" data-image-precheck-decided>
|
||
{{ __('Bild-Sichtung am :date: :note', ['date' => $adminTitleImage->precheck_reviewed_at->format('d.m.Y H:i'), 'note' => $adminTitleImage->precheck_review_note]) }}
|
||
</p>
|
||
@endif
|
||
|
||
{{-- ============== KORREKTURWEG BILD (E.6) ============== --}}
|
||
@if (! $coverIsPlaceholder && isset($adminTitleImage) && $adminTitleImage && ! $adminTitleImage->isLegacyRules())
|
||
<article class="panel" data-image-correction>
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Bildnachweis und Kennzeichnung korrigieren') }}</span>
|
||
@if ($adminTitleImage->isWithheld())
|
||
<span class="badge">{{ __('vorläufig zurückgehalten') }}</span>
|
||
@endif
|
||
</div>
|
||
<div class="p-5 space-y-3 max-w-[760px]">
|
||
<p class="m-0 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Kundenangabe und Bestätigung bleiben unverändert; jede Korrektur wird mit Grund und Bearbeiter protokolliert. Kundenangabe: :user · Portalkennzeichnung: :portal (:source).', [
|
||
'user' => $adminTitleImage->ai_declaration_user?->label() ?? '–',
|
||
'portal' => $adminTitleImage->ai_declaration?->label() ?? '–',
|
||
'source' => $adminTitleImage->ai_declaration_source?->publicLabel() ?? '–',
|
||
]) }}
|
||
</p>
|
||
<div class="grid gap-3 sm:grid-cols-2">
|
||
<flux:input wire:model="correctionCredit" :label="__('Neuer Bildnachweis')" :placeholder="$adminTitleImage->copyright" />
|
||
<flux:select wire:model="correctionAiDeclaration" :label="__('Neue Portalkennzeichnung')">
|
||
<flux:select.option value="">{{ __('unverändert') }}</flux:select.option>
|
||
@foreach (AiImageDeclaration::cases() as $aiOption)
|
||
<flux:select.option :value="$aiOption->value">{{ $aiOption->label() }}</flux:select.option>
|
||
@endforeach
|
||
</flux:select>
|
||
</div>
|
||
<flux:textarea wire:model="correctionReason" :label="__('Grund (Pflicht)')" rows="2" />
|
||
<div class="flex flex-wrap gap-2">
|
||
<flux:button variant="primary" wire:click="correctImage">{{ __('Korrektur übernehmen') }}</flux:button>
|
||
<flux:button variant="filled" wire:click="toggleWithheld">
|
||
{{ $adminTitleImage->isWithheld() ? __('Bild wieder freigeben') : __('Bild vorläufig zurückhalten') }}
|
||
</flux:button>
|
||
<flux:button variant="ghost" size="sm" href="{{ route('admin.press-releases.images.original', ['id' => $pr->id, 'image' => $adminTitleImage->id, 'purpose' => 'Sichtprüfung im Admin']) }}" target="_blank">
|
||
{{ __('Beweisoriginal öffnen') }}
|
||
</flux:button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== REJECTION-HINWEIS ============== --}}
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Rejected && $latestRejection)
|
||
<article class="panel" style="border-color:var(--color-err); border-left-width:3px;">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Diese Pressemitteilung wurde abgelehnt') }}</span>
|
||
<span class="badge err dot">{{ __('Handlung erforderlich') }}</span>
|
||
</div>
|
||
<div class="p-5 flex items-start gap-3">
|
||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||
bg-[color:var(--color-err-soft)] border border-[color:var(--color-err)]/30 text-[color:var(--color-loss)]">
|
||
<flux:icon.exclamation-triangle class="size-[18px]" />
|
||
</div>
|
||
<div class="flex-1 text-[13px] text-[color:var(--color-ink-2)]">
|
||
@if ($latestRejection->reason)
|
||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Begründung') }}:</strong>
|
||
<span class="block mt-1 whitespace-pre-line">{{ $latestRejection->reason }}</span>
|
||
@else
|
||
{{ __('Der Autor sollte den Inhalt überarbeiten und erneut einreichen.') }}
|
||
@endif
|
||
<span class="mt-2 block text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Abgelehnt am') }} {{ $latestRejection->created_at->format('d.m.Y H:i') }}
|
||
@if ($latestRejection->changedBy)
|
||
· {{ __('durch :name', ['name' => $latestRejection->changedBy->name]) }}
|
||
@endif
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== STATUS-WORKFLOW ============== --}}
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||
@php
|
||
$manual = $pr->isAwaitingManualReview();
|
||
@endphp
|
||
<article class="panel" style="border-color:var(--color-{{ $manual ? 'err' : 'warn' }}); border-left-width:3px;">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||
@if ($manual)
|
||
<span class="badge err dot">{{ __('Prüfung nötig — Automatik erschöpft') }}</span>
|
||
@else
|
||
<span class="badge warn dot">{{ __('Wartet auf Prüfung') }}</span>
|
||
@endif
|
||
</div>
|
||
<div class="p-5 flex flex-wrap items-start gap-3">
|
||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||
bg-[color:var(--color-warn-soft)] border border-[color:var(--color-warn)]/30 text-[color:var(--color-accent-deep)]">
|
||
<flux:icon.clock class="size-[18px]" />
|
||
</div>
|
||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||
@if ($manual)
|
||
{{-- E3: die automatische Prüfung hat ihre Versuche aufgebraucht.
|
||
Ab hier entscheidet ein Mensch — Freischalten oder mit
|
||
Kommentar zurück; danach bleibt die Automatik für diese
|
||
Meldung aus. --}}
|
||
<p class="m-0 font-semibold text-[color:var(--color-ink)]">
|
||
{{ __(':n automatische Prüfungen, zuletzt Rot. Ihre Entscheidung: veröffentlichen oder mit Kommentar zurück an den Autor.', ['n' => $pr->auto_check_attempts]) }}
|
||
</p>
|
||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Jede weitere Freigabe des Autors landet ohne KI-Lauf wieder hier.') }}
|
||
</p>
|
||
@php
|
||
$kiRejections = $statusLogs->filter(fn ($log) => $log->source === 'ki' && filled($log->reason))->sortBy('created_at')->values();
|
||
@endphp
|
||
@if ($kiRejections->isNotEmpty())
|
||
<ol class="m-0 mt-3 ps-4 space-y-1.5 text-[12px] text-[color:var(--color-ink-2)] list-decimal">
|
||
@foreach ($kiRejections as $log)
|
||
<li><span class="font-mono text-[11px] text-[color:var(--color-ink-4)]">{{ $log->created_at->format('d.m. H:i') }}</span> · {{ $log->reason }}</li>
|
||
@endforeach
|
||
</ol>
|
||
@endif
|
||
@else
|
||
<p class="m-0">{{ __('Diese PM wartet auf eine Prüf-Entscheidung.') }}</p>
|
||
@endif
|
||
@if (! $manual && $latestClassification && $latestClassification->reason)
|
||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||
<strong class="text-[color:var(--color-ink-2)]">{{ __('KI-Hinweis') }}:</strong>
|
||
{{ $latestClassification->reason }}
|
||
</p>
|
||
@endif
|
||
@if ($pr->scheduled_at)
|
||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||
<flux:icon.calendar variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||
{{ __('Geplante Veröffentlichung: :date', ['date' => $pr->scheduledAtLocal()->format('d.m.Y H:i')]) }}
|
||
</p>
|
||
@endif
|
||
</div>
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<flux:modal.trigger name="confirm-show-publish">
|
||
<flux:button type="button" variant="primary">{{ $manual ? __('Freischalten') : __('Veröffentlichen') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
<flux:modal.trigger name="confirm-show-reject">
|
||
<flux:button type="button" variant="danger">{{ $manual ? __('Mit Kommentar zurück') : __('Ablehnen') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
@if ($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||
<article class="panel" style="border-color:var(--color-ok); border-left-width:3px;">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Status-Workflow') }}</span>
|
||
<span class="badge ok dot">{{ __('Live') }}</span>
|
||
</div>
|
||
<div class="p-5 flex flex-wrap items-start gap-3">
|
||
<div class="w-9 h-9 rounded-[5px] flex items-center justify-center flex-shrink-0
|
||
bg-[color:var(--color-ok-soft)] border border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||
<flux:icon.check-circle class="size-[18px]" />
|
||
</div>
|
||
<div class="flex-1 min-w-[220px] text-[13px] text-[color:var(--color-ink-2)]">
|
||
<p class="m-0">
|
||
{{ __('Veröffentlicht am') }}
|
||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}</strong>
|
||
</p>
|
||
@if ($pr->embargo_at && $pr->embargo_at->isFuture())
|
||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||
<flux:icon.lock-closed variant="micro" class="inline-block size-3.5 -mt-0.5 mr-0.5" />
|
||
{{ __('Sperrfrist bis: :date', ['date' => $pr->embargoAtLocal()->format('d.m.Y H:i')]) }}
|
||
</p>
|
||
@endif
|
||
@if ($pr->hits > 0)
|
||
<p class="m-0 mt-1 text-[12px] text-[color:var(--color-ink-3)]">
|
||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ number_format($pr->hits, 0, ',', '.') }}</strong>
|
||
{{ __('Aufrufe seit Veröffentlichung') }}
|
||
</p>
|
||
@endif
|
||
</div>
|
||
<flux:modal.trigger name="confirm-show-archive">
|
||
<flux:button type="button" variant="filled">{{ __('Archivieren') }}</flux:button>
|
||
</flux:modal.trigger>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== KONTAKTE + STATUS/VERLAUF ============== --}}
|
||
<div class="grid gap-6 xl:grid-cols-2">
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Firma & Pressekontakte') }}</span>
|
||
@if ($pr->company)
|
||
<flux:button size="sm" variant="filled" icon="building-office" href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate>
|
||
{{ __('Firma öffnen') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
<div class="p-5">
|
||
@if ($pr->company)
|
||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||
<div class="text-[13px] font-semibold">
|
||
<a href="{{ route('admin.companies.show', $pr->company->id) }}" wire:navigate
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->company->name }}
|
||
</a>
|
||
</div>
|
||
@if ($pr->company->address || $pr->company->country_code)
|
||
<div class="text-[12px] text-[color:var(--color-ink-3)] whitespace-pre-line">{{ trim($pr->company->address.' '.$pr->company->country_code) }}</div>
|
||
@endif
|
||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
@if ($pr->company->email)
|
||
<a href="mailto:{{ $pr->company->email }}"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->company->email }}
|
||
</a>
|
||
@endif
|
||
@if ($pr->company->phone)
|
||
<span>{{ $pr->company->phone }}</span>
|
||
@endif
|
||
@if ($pr->company->website)
|
||
<a href="{{ $pr->company->websiteUrl() }}" target="_blank" rel="noopener"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->company->website }}
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Dieser Pressemitteilung ist keine Firma zugeordnet.') }}
|
||
</div>
|
||
@endif
|
||
|
||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mt-4 mb-2">
|
||
{{ __('Zugeordnete Pressekontakte') }}
|
||
</div>
|
||
<div class="space-y-2">
|
||
@forelse ($contacts as $contact)
|
||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)]">
|
||
{{ $contact->displayName() }}
|
||
</div>
|
||
<div class="text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ $contact->responsibility ?: __('Keine Rolle hinterlegt') }}
|
||
</div>
|
||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
@if ($contact->email)
|
||
<a href="mailto:{{ $contact->email }}"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $contact->email }}
|
||
</a>
|
||
@endif
|
||
@if ($contact->phone)
|
||
<span>{{ $contact->phone }}</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@empty
|
||
<div class="rounded-[5px] border border-dashed border-[color:var(--color-bg-rule)] p-4 text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Dieser Pressemitteilung ist kein Pressekontakt zugeordnet.') }}
|
||
</div>
|
||
@endforelse
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Status & Verlauf') }}</span>
|
||
<span @class(['badge', $statusClass])>{{ $pr->status->label() }}</span>
|
||
</div>
|
||
<div class="p-5">
|
||
<div class="grid gap-2 sm:grid-cols-2">
|
||
<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">{{ __('Autor') }}</div>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1 truncate">
|
||
@if ($pr->user)
|
||
<a href="{{ route('admin.users.show', $pr->user->id) }}" wire:navigate
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->user->name }}
|
||
</a>
|
||
@else
|
||
–
|
||
@endif
|
||
</div>
|
||
</div>
|
||
<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">{{ __('Erstellt') }}</div>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||
{{ $pr->created_at?->format('d.m.Y H:i') ?? '–' }}
|
||
</div>
|
||
</div>
|
||
<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">{{ __('Veröffentlicht') }}</div>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||
{{ $pr->published_at?->format('d.m.Y H:i') ?? '–' }}
|
||
</div>
|
||
</div>
|
||
<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">{{ __('Aufrufe') }}</div>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||
{{ number_format($pr->hits, 0, ',', '.') }}
|
||
</div>
|
||
</div>
|
||
@if ($pr->scheduled_at)
|
||
<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">{{ __('Geplant') }}</div>
|
||
<div class="text-[13px] font-semibold text-[color:var(--color-ink)] mt-1">
|
||
{{ $pr->scheduledAtLocal()->format('d.m.Y H:i') }}
|
||
</div>
|
||
</div>
|
||
@endif
|
||
@if ($pr->embargo_at)
|
||
<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->embargoAtLocal()->format('d.m.Y H:i') }}
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
@php
|
||
// Neben dem Autor können weitere User (Firmen-Mitglieder, z. B.
|
||
// per Magic-Link eingeladen) auf die PM zugreifen.
|
||
$companyUsers = $pr->company?->users ?? collect();
|
||
@endphp
|
||
@if ($companyUsers->isNotEmpty())
|
||
<div class="mt-3">
|
||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">
|
||
{{ __('Benutzer mit Zugriff (über Firma)') }}
|
||
</div>
|
||
<div class="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-[12.5px]">
|
||
@foreach ($companyUsers as $companyUser)
|
||
<span class="inline-flex items-center gap-1.5">
|
||
<a href="{{ route('admin.users.show', $companyUser->id) }}" wire:navigate
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $companyUser->name }}
|
||
</a>
|
||
<span class="text-[11px] text-[color:var(--color-ink-3)]">
|
||
{{ $companyUser->pivot->role === 'owner' ? __('Owner') : __('Member') }}@if ($companyUser->id === $pr->user_id) · {{ __('Autor') }}@endif
|
||
</span>
|
||
</span>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
@if ($pr->no_export)
|
||
<div class="mt-3 flex items-center gap-2 text-[12px] text-[color:var(--color-ink-3)]">
|
||
<flux:icon.no-symbol variant="micro" class="size-3.5" />
|
||
<span>{{ __('Kein Export aktiv (PM wird nicht über Feeds verteilt).') }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="my-4 border-t border-[color:var(--color-bg-rule)]"></div>
|
||
|
||
@if ($statusLogs->isNotEmpty())
|
||
<ol class="space-y-3 border-s border-[color:var(--color-bg-rule)] ps-4">
|
||
@foreach ($statusLogs as $log)
|
||
<li class="text-[12.5px]">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
@php
|
||
$logClass = $log->to_status?->badgeClass() ?? 'muted';
|
||
@endphp
|
||
<span @class(['badge', $logClass])>{{ $log->to_status?->label() ?? $log->to_status }}</span>
|
||
@if ($log->from_status)
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('von') }} {{ $log->from_status->label() }}
|
||
</span>
|
||
@endif
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ $log->created_at->format('d.m.Y H:i') }}
|
||
</span>
|
||
@if ($log->changedBy)
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">·</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ $log->changedBy->name }}</span>
|
||
@endif
|
||
@if ($log->source && $log->source !== 'admin')
|
||
<span class="badge hub">{{ $log->source }}</span>
|
||
@endif
|
||
</div>
|
||
@if ($log->reason)
|
||
<p class="mt-1.5 text-[color:var(--color-ink-2)] m-0 whitespace-pre-line">{{ $log->reason }}</p>
|
||
@endif
|
||
</li>
|
||
@endforeach
|
||
</ol>
|
||
@else
|
||
<p class="text-[12.5px] text-[color:var(--color-ink-3)] m-0">
|
||
{{ __('Noch keine Statusänderungen protokolliert.') }}
|
||
</p>
|
||
@endif
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
{{-- ============== INHALT ============== --}}
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Inhalt') }}</span>
|
||
@if ($pr->ai_text_declaration)
|
||
<span class="text-[11px] text-[color:var(--color-ink-3)]">
|
||
{{ __('KI-Deklaration:') }} {{ $pr->ai_text_declaration->label() }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
<div class="p-5">
|
||
{{-- KI-Label (Art. 50 KI-VO): erscheint so auch in der
|
||
Veröffentlichung — am Anfang des Inhalts, in Einreicher-Stimme. --}}
|
||
<x-portal.ai-public-label :label="$pr->aiTextPublicLabel()" class="mb-4 max-w-[760px]" />
|
||
<div class="pr-content max-w-[760px]">
|
||
{!! $pr->renderedText() !!}
|
||
</div>
|
||
|
||
@if ($pr->keywords || $pr->backlink_url)
|
||
<div class="mt-6 space-y-2 border-t border-[color:var(--color-bg-rule)] pt-4 text-[12.5px] text-[color:var(--color-ink-2)]">
|
||
@if ($pr->keywords)
|
||
<p class="m-0">
|
||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Stichwörter') }}:</strong>
|
||
{{ $pr->keywords }}
|
||
</p>
|
||
@endif
|
||
@if ($pr->backlink_url)
|
||
<p class="m-0">
|
||
<strong class="text-[color:var(--color-ink)] font-semibold">{{ __('Backlink') }}:</strong>
|
||
<a href="{{ $pr->backlink_url }}" target="_blank"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 decoration-[color:var(--color-hub)]/40 hover:decoration-[color:var(--color-hub)]">
|
||
{{ $pr->backlink_url }}
|
||
</a>
|
||
</p>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</article>
|
||
|
||
{{-- ============== BOILERPLATE-OVERRIDE ============== --}}
|
||
@if ($pr->boilerplate_override)
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Eigener Abbinder (Boilerplate)') }}</span>
|
||
<span class="badge hub">{{ __('Override') }}</span>
|
||
</div>
|
||
<div class="p-5">
|
||
<p class="text-[12px] text-[color:var(--color-ink-3)] mt-0 mb-3">
|
||
{{ __('Dieser Text wird für diese Pressemitteilung anstelle des Standard-Abbinders der Firma verwendet.') }}
|
||
</p>
|
||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[13px] leading-[1.6] text-[color:var(--color-ink-2)] whitespace-pre-line">
|
||
{{ $pr->boilerplate_override }}
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== MEDIEN ============== --}}
|
||
@if ($pr->images->isNotEmpty())
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Bilder') }}</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ $pr->images->count() }}
|
||
</span>
|
||
</div>
|
||
<div class="p-5 space-y-2">
|
||
@foreach ($pr->images as $image)
|
||
<div class="flex items-center gap-2 text-[12.5px]">
|
||
<flux:icon.photo class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||
<span class="truncate text-[color:var(--color-ink-2)]">{{ basename($image->path) }}</span>
|
||
@if ($image->is_preview)
|
||
<span class="badge hub">{{ __('Preview') }}</span>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ANHÄNGE-ANZEIGE — TEMPORÄR DEAKTIVIERT
|
||
Datei-Uploads erfordern eine vollständige Sicherheitsprüfung.
|
||
Wird mit dem Anhang-Manager in einer späteren Phase wieder aktiviert.
|
||
@if ($pr->attachments->isNotEmpty())
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Anhänge') }}</span>
|
||
<span class="text-[11.5px] text-[color:var(--color-ink-3)]">
|
||
{{ $pr->attachments->count() }}
|
||
</span>
|
||
</div>
|
||
<div class="p-5 space-y-2">
|
||
@foreach ($pr->attachments as $attachment)
|
||
<div class="flex items-center gap-2 text-[12.5px]">
|
||
<flux:icon.paper-clip class="size-4 flex-shrink-0 text-[color:var(--color-ink-3)]" />
|
||
<span class="truncate text-[color:var(--color-ink-2)] flex-1">
|
||
{{ $attachment->title ?: $attachment->original_name }}
|
||
</span>
|
||
<span class="text-[11px] text-[color:var(--color-ink-3)] flex-shrink-0">
|
||
{{ number_format($attachment->size / 1024, 0, ',', '.') }} KB
|
||
</span>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</article>
|
||
@endif
|
||
--}}
|
||
|
||
@if($pr->status === \App\Enums\PressReleaseStatus::Review)
|
||
<flux:modal name="confirm-show-publish" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung veröffentlichen?') }}</flux:heading>
|
||
<flux:subheading>{{ __('Die Pressemitteilung wird öffentlich sichtbar und der Autor wird benachrichtigt.') }}</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="publish">{{ __('Veröffentlichen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
|
||
<flux:modal name="confirm-show-reject" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung ablehnen?') }}</flux:heading>
|
||
<flux:subheading>{{ __('Die Pressemitteilung wird abgelehnt und der Autor wird benachrichtigt. Bitte begründen Sie die Ablehnung.') }}</flux:subheading>
|
||
</div>
|
||
|
||
<flux:field>
|
||
<flux:label>{{ __('Begründung (an den Autor sichtbar)') }}</flux:label>
|
||
<flux:textarea
|
||
wire:model="rejectReason"
|
||
rows="5"
|
||
placeholder="{{ __('z. B. Werbliche Sprache, fehlende Belege, doppelte Veröffentlichung…') }}"
|
||
/>
|
||
<flux:error name="rejectReason" />
|
||
</flux:field>
|
||
|
||
<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="reject">{{ __('Ablehnen') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
@endif
|
||
|
||
@if($pr->status === \App\Enums\PressReleaseStatus::Published)
|
||
<flux:modal name="confirm-show-archive" class="max-w-lg">
|
||
<div class="space-y-6">
|
||
<div>
|
||
<flux:heading size="lg">{{ __('Pressemitteilung archivieren?') }}</flux:heading>
|
||
<flux:subheading>{{ __('Die Pressemitteilung bleibt intern erhalten, wird aber archiviert.') }}</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="archive">{{ __('Archivieren') }}</flux:button>
|
||
</div>
|
||
</div>
|
||
</flux:modal>
|
||
@endif
|
||
</div>
|