presseportale/resources/views/livewire/admin/images/index.blade.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

257 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\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->where(fn ($inner) => $inner->whereNotNull('withheld_at')->orWhereNotNull('precheck_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>