admin: Management ▸ Bilder — Liste aller Titelbilder mit PM-Link, Nachweis, Kennzeichnung, Bestätigungsstand und KI-Vorab-Check, Filter nach Sichtung/Zustand/Ausgang (Bildkennzeichnung)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
bcd02f2e8f
commit
aaeee24412
4 changed files with 333 additions and 0 deletions
|
|
@ -137,6 +137,10 @@
|
|||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('Management')" class="grid mb-4">
|
||||
<flux:navlist.item icon="photo" :href="route('admin.images.index')"
|
||||
:current="request()->routeIs('admin.images.*')" wire:navigate>
|
||||
{{ __('Bilder') }}
|
||||
</flux:navlist.item>
|
||||
@can('compliance:manage')
|
||||
<flux:navlist.item icon="queue-list" :href="route('admin.reports.activity')"
|
||||
:current="request()->routeIs('admin.reports.activity')" wire:navigate>
|
||||
|
|
|
|||
257
resources/views/livewire/admin/images/index.blade.php
Normal file
257
resources/views/livewire/admin/images/index.blade.php
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ImagePrecheckCategory;
|
||||
use App\Enums\ImagePrecheckOutcome;
|
||||
use App\Models\PressReleaseImage;
|
||||
use App\Scopes\PortalScope;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
/**
|
||||
* Admin ▸ Management ▸ Bilder (Bildkennzeichnung, Wunsch vom 25.09.2026).
|
||||
*
|
||||
* Alle eingereichten Titelbilder mit Link zur Meldung, Nachweis,
|
||||
* Kennzeichnung, Herkunft, Bestätigungsstand und dem Ergebnis des
|
||||
* KI-Vorab-Checks (Block K). Entscheidungen fallen weiter auf der PM-Seite.
|
||||
*/
|
||||
new #[Layout('components.layouts.app'), Title('Bilder')] class extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
#[Url]
|
||||
public string $search = '';
|
||||
|
||||
#[Url]
|
||||
public string $precheck = '';
|
||||
|
||||
#[Url]
|
||||
public string $state = '';
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedPrecheck(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedState(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$query = PressReleaseImage::query()
|
||||
->with([
|
||||
'pressRelease' => fn ($q) => $q->withoutGlobalScope(PortalScope::class)->select(['id', 'title', 'status', 'portal', 'company_id', 'published_at'])->with('company:id,name'),
|
||||
'latestRightsConfirmation',
|
||||
'precheckReviewer:id,name',
|
||||
])
|
||||
->orderByDesc('id');
|
||||
|
||||
if ($this->search !== '') {
|
||||
$term = trim($this->search);
|
||||
$query->where(function ($q) use ($term): void {
|
||||
if (ctype_digit($term)) {
|
||||
$q->where('id', (int) $term)->orWhere('press_release_id', (int) $term);
|
||||
}
|
||||
|
||||
$q->orWhereHas('pressRelease', fn ($pr) => $pr->withoutGlobalScope(PortalScope::class)->where('title', 'like', '%'.$term.'%'))
|
||||
->orWhere('copyright', 'like', '%'.$term.'%');
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->precheck !== '') {
|
||||
$this->precheck === 'none'
|
||||
? $query->whereNull('precheck_outcome')
|
||||
: $query->where('precheck_outcome', $this->precheck);
|
||||
}
|
||||
|
||||
match ($this->state) {
|
||||
'review' => $query->awaitingPrecheckReview(),
|
||||
'withheld' => $query->whereNotNull('withheld_at'),
|
||||
'unconfirmed' => $query->whereNotNull('rules_version')->whereNull('rights_confirmed_at'),
|
||||
'legacy' => $query->whereNull('rules_version'),
|
||||
'published' => $query->whereNotNull('published_ever_at'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
'images' => $query->paginate(40),
|
||||
'outcomes' => ImagePrecheckOutcome::cases(),
|
||||
'openReviews' => PressReleaseImage::query()->awaitingPrecheckReview()->count(),
|
||||
'precheckEnabled' => (bool) config('images.precheck.enabled'),
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Bilder') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Alle eingereichten Titelbilder mit Nachweis, Kennzeichnung, Bestätigungsstand und dem Ergebnis des KI-Vorab-Checks. Entscheidungen zur Sichtung und Korrekturen laufen auf der Seite der Meldung.') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:select wire:model.live="state" size="sm">
|
||||
<flux:select.option value="">{{ __('Alle Bilder') }}</flux:select.option>
|
||||
<flux:select.option value="review">{{ __('Sichtung offen') }}</flux:select.option>
|
||||
<flux:select.option value="withheld">{{ __('zurückgehalten') }}</flux:select.option>
|
||||
<flux:select.option value="unconfirmed">{{ __('Bestätigung fehlt') }}</flux:select.option>
|
||||
<flux:select.option value="published">{{ __('je veröffentlicht') }}</flux:select.option>
|
||||
<flux:select.option value="legacy">{{ __('Bestand (ohne Protokoll)') }}</flux:select.option>
|
||||
</flux:select>
|
||||
<flux:select wire:model.live="precheck" size="sm">
|
||||
<flux:select.option value="">{{ __('KI-Check: alle') }}</flux:select.option>
|
||||
@foreach ($outcomes as $outcome)
|
||||
<flux:select.option :value="$outcome->value">{{ __('KI-Check: :label', ['label' => $outcome->label()]) }}</flux:select.option>
|
||||
@endforeach
|
||||
<flux:select.option value="none">{{ __('KI-Check: nicht geprüft') }}</flux:select.option>
|
||||
</flux:select>
|
||||
<flux:input wire:model.live.debounce.400ms="search" size="sm" :placeholder="__('PM-Titel, Nachweis, PM- oder Bild-ID')" class="w-56" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@unless ($precheckEnabled)
|
||||
<flux:callout variant="warning" icon="information-circle">
|
||||
{{ __('Der KI-Vorab-Check ist aus (IMAGE_PRECHECK). Neue Bilder werden nicht gesichtet; die Spalte zeigt nur Altbefunde.') }}
|
||||
</flux:callout>
|
||||
@endunless
|
||||
|
||||
@if ($openReviews > 0)
|
||||
<flux:callout variant="warning" icon="eye" data-open-reviews>
|
||||
{{ trans_choice(':count Bild wartet auf eine Sichtung.|:count Bilder warten auf eine Sichtung.', $openReviews, ['count' => $openReviews]) }}
|
||||
<flux:button size="xs" variant="ghost" wire:click="$set('state', 'review')">{{ __('Anzeigen') }}</flux:button>
|
||||
</flux:callout>
|
||||
@endif
|
||||
|
||||
<flux:card>
|
||||
<table class="w-full text-[13px]">
|
||||
<thead>
|
||||
<tr class="text-left text-zinc-500">
|
||||
<th class="py-2 pr-3 font-medium">{{ __('Bild') }}</th>
|
||||
<th class="py-2 pr-3 font-medium">{{ __('Pressemitteilung') }}</th>
|
||||
<th class="py-2 pr-3 font-medium">{{ __('Nachweis und Kennzeichnung') }}</th>
|
||||
<th class="py-2 pr-3 font-medium">{{ __('Bestätigung') }}</th>
|
||||
<th class="py-2 pr-3 font-medium">{{ __('KI-Vorab-Check') }}</th>
|
||||
<th class="py-2 font-medium">{{ __('Hochgeladen') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700 align-top">
|
||||
@forelse ($images as $image)
|
||||
@php
|
||||
$thumb = $image->variantUrl('thumb') ?? $image->variantUrl('card') ?? $image->url();
|
||||
$confirmation = $image->latestRightsConfirmation;
|
||||
$triggers = $image->precheck['evaluation']['triggers'] ?? [];
|
||||
@endphp
|
||||
<tr wire:key="image-{{ $image->id }}" data-image-row="{{ $image->id }}">
|
||||
<td class="py-2 pr-3">
|
||||
<div class="flex items-start gap-2">
|
||||
@if ($thumb)
|
||||
<img src="{{ $thumb }}" alt="" class="size-14 shrink-0 rounded object-cover bg-zinc-100 dark:bg-zinc-800" loading="lazy">
|
||||
@else
|
||||
<div class="size-14 shrink-0 rounded bg-zinc-100 dark:bg-zinc-800"></div>
|
||||
@endif
|
||||
<div class="text-xs text-zinc-500">
|
||||
<div class="font-mono">#{{ $image->id }}</div>
|
||||
@if ($image->width && $image->height)
|
||||
<div>{{ $image->width }}×{{ $image->height }}</div>
|
||||
@endif
|
||||
@if ($image->isWithheld())
|
||||
<flux:badge size="sm" color="amber">{{ __('zurückgehalten') }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 pr-3 max-w-[280px]">
|
||||
@if ($image->pressRelease)
|
||||
<a class="font-medium underline underline-offset-2" href="{{ route('admin.press-releases.show', ['id' => $image->press_release_id]) }}" wire:navigate>
|
||||
{{ \Illuminate\Support\Str::limit($image->pressRelease->title, 70) }}
|
||||
</a>
|
||||
<div class="text-xs text-zinc-500">
|
||||
PM #{{ $image->press_release_id }} · {{ $image->pressRelease->status->label() }}
|
||||
@if ($image->pressRelease->company)
|
||||
· {{ $image->pressRelease->company->name }}
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-zinc-500">PM #{{ $image->press_release_id }} ({{ __('gelöscht') }})</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3 max-w-[260px]">
|
||||
<div>{{ $image->copyright ?: '–' }}</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
@if ($image->ai_declaration)
|
||||
{{ $image->ai_declaration->label() }}
|
||||
@if ($image->ai_declaration_source)
|
||||
({{ $image->ai_declaration_source->publicLabel() }})
|
||||
@endif
|
||||
@if ($image->ai_declaration_locked)
|
||||
· {{ __('gesperrt') }}
|
||||
@endif
|
||||
@else
|
||||
{{ __('keine Kennzeichnung') }}
|
||||
@endif
|
||||
</div>
|
||||
@if ($image->provenance_status)
|
||||
<div class="text-xs text-zinc-500">{{ __('Herkunft: :status', ['status' => $image->provenance_status->value]) }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3 whitespace-nowrap">
|
||||
@if ($confirmation)
|
||||
<flux:badge size="sm" color="green">{{ __('bestätigt') }}</flux:badge>
|
||||
<div class="text-xs text-zinc-500">{{ $confirmation->confirmed_at->format('d.m.Y H:i') }} · {{ $confirmation->legal_text_key }} · {{ $confirmation->channel->value }}</div>
|
||||
@elseif ($image->awaitsCustomerConfirmation())
|
||||
<flux:badge size="sm" color="amber">{{ __('wartet auf Kunde') }}</flux:badge>
|
||||
@elseif ($image->isLegacyRules())
|
||||
<flux:badge size="sm" color="zinc">{{ __('Bestand') }}</flux:badge>
|
||||
@else
|
||||
<flux:badge size="sm" color="red">{{ __('fehlt') }}</flux:badge>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3 max-w-[300px]">
|
||||
@if ($image->precheck_outcome)
|
||||
<flux:badge size="sm" color="{{ match ($image->precheck_outcome) { \App\Enums\ImagePrecheckOutcome::Queue => 'amber', \App\Enums\ImagePrecheckOutcome::Hint => 'sky', \App\Enums\ImagePrecheckOutcome::Error => 'zinc', default => 'green' } }}">
|
||||
{{ $image->precheck_outcome->label() }}
|
||||
</flux:badge>
|
||||
@if ($triggers !== [])
|
||||
<ul class="mt-1 text-xs text-zinc-500 space-y-0.5">
|
||||
@foreach ($triggers as $trigger)
|
||||
<li>{{ ImagePrecheckCategory::tryFrom($trigger['category'] ?? '')?->label() ?? $trigger['category'] }} ({{ number_format((float) ($trigger['confidence'] ?? 0) * 100, 0) }} %)</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@if ($image->awaitsPrecheckReview())
|
||||
<div class="mt-1 text-xs font-medium text-amber-700 dark:text-amber-300">{{ __('Sichtung offen') }}</div>
|
||||
@elseif ($image->precheck_reviewed_at)
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
{{ __('Entschieden :date von :name: :note', ['date' => $image->precheck_reviewed_at->format('d.m.Y H:i'), 'name' => $image->precheckReviewer?->name ?? '–', 'note' => $image->precheck_review_note]) }}
|
||||
</div>
|
||||
@endif
|
||||
@if (! empty($image->precheck['model']))
|
||||
<div class="mt-1 text-[11px] text-zinc-400">{{ $image->precheck['model'] }}</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-xs text-zinc-500">{{ __('nicht geprüft') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 whitespace-nowrap text-xs text-zinc-500">
|
||||
{{ $image->created_at?->format('d.m.Y H:i') }}
|
||||
@if ($image->published_ever_at)
|
||||
<div>{{ __('veröffentlicht') }}</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="py-6 text-center text-zinc-500">{{ __('Keine Bilder gefunden.') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">{{ $images->links() }}</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue