- 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>
569 lines
26 KiB
PHP
569 lines
26 KiB
PHP
<?php
|
||
|
||
use App\Enums\LegalRequestStatus;
|
||
use App\Enums\LegalRequestType;
|
||
use App\Enums\PressReleaseStatus;
|
||
use App\Mail\LegalRequestExecuted;
|
||
use App\Models\LegalRequest;
|
||
use App\Services\PressRelease\AnonymizationService;
|
||
use App\Services\PressRelease\PressReleaseService;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Locked;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Component;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Anfrage · Recht & Compliance')] class extends Component
|
||
{
|
||
#[Locked]
|
||
public int $id;
|
||
|
||
public string $adminNote = '';
|
||
|
||
public string $notification = '';
|
||
|
||
/** Schwärzbegriffe inkl. Varianten, ein Begriff pro Zeile. */
|
||
public string $redactionTerms = '';
|
||
|
||
public string $redactionReplacement = '';
|
||
|
||
public bool $detachContacts = true;
|
||
|
||
/** @var list<int> */
|
||
public array $removeImageIds = [];
|
||
|
||
/** @var array{fields: array<string, array{hits: int, after: string}>, total_hits: int}|null */
|
||
public ?array $redactionPreview = null;
|
||
|
||
public function mount(int $id): void
|
||
{
|
||
$this->id = $id;
|
||
$this->adminNote = (string) ($this->request()->admin_note ?? '');
|
||
$this->redactionReplacement = AnonymizationService::DEFAULT_REPLACEMENT;
|
||
|
||
// Spur B (Phase 5): strukturierter Schwärzungsauftrag aus dem
|
||
// verifizierten Web-Formular befüllt den Editor vor — der Admin
|
||
// prüft, ergänzt und führt aus.
|
||
$redaction = (array) ($this->request()->payload['redaction'] ?? []);
|
||
|
||
if ($redaction !== []) {
|
||
$this->redactionTerms = implode("\n", (array) ($redaction['terms'] ?? []));
|
||
$this->detachContacts = (bool) ($redaction['detach_contacts'] ?? true);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Pflicht-Vorschau (Konzept §Spur B): Ohne Diff keine Ausführung.
|
||
*/
|
||
public function previewAnonymization(AnonymizationService $service): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$pressRelease = $this->request()->pressRelease;
|
||
abort_unless($pressRelease !== null, 404);
|
||
|
||
if ($this->redactionTermList() === [] && ! $this->detachContacts && $this->removeImageIds === []) {
|
||
$this->addError('redactionTerms', __('Mindestens einen Schwärzbegriff eingeben oder Kontakte/Bilder auswählen.'));
|
||
|
||
return;
|
||
}
|
||
|
||
$this->redactionPreview = $service->preview(
|
||
$pressRelease,
|
||
$this->redactionTermList(),
|
||
$this->replacementValue(),
|
||
);
|
||
}
|
||
|
||
public function executeAnonymization(AnonymizationService $service): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$request = $this->request();
|
||
$pressRelease = $request->pressRelease;
|
||
abort_unless($pressRelease !== null, 404);
|
||
|
||
if ($this->redactionPreview === null) {
|
||
$this->addError('redactionTerms', __('Bitte zuerst die Vorschau prüfen.'));
|
||
|
||
return;
|
||
}
|
||
|
||
$service->execute(
|
||
$pressRelease,
|
||
$this->redactionTermList(),
|
||
$this->replacementValue(),
|
||
$this->detachContacts,
|
||
$this->removeImageIds,
|
||
legalRequest: $request,
|
||
);
|
||
|
||
$request->update([
|
||
'status' => LegalRequestStatus::Resolved->value,
|
||
'admin_note' => $this->adminNote !== '' ? $this->adminNote : null,
|
||
'resolved_by_user_id' => auth()->id(),
|
||
'resolved_at' => now(),
|
||
'executed_at' => now(),
|
||
]);
|
||
|
||
$this->sendExecutedMail($request, 'anonymized');
|
||
|
||
$this->redactionPreview = null;
|
||
$this->notification = __('Anonymisierung ausgeführt — PM geschwärzt, Re-Sync-Sperre gesetzt.');
|
||
}
|
||
|
||
/**
|
||
* Alternative zur Schwärzung (Matrix-Test §1/§2): Bei Vanity-/Personen-PMs
|
||
* bleibt nach Schwärzung nur ein Rumpf — dann ist Depublizierung das
|
||
* mildere Restmittel (im DSGVO-Fall kostenlos, umkehrbar).
|
||
*/
|
||
public function depublishRelease(PressReleaseService $service): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$request = $this->request();
|
||
$pressRelease = $request->pressRelease;
|
||
abort_unless($pressRelease !== null, 404);
|
||
|
||
if ($pressRelease->status !== PressReleaseStatus::Published) {
|
||
$this->addError('redactionTerms', __('Nur veröffentlichte PMs können depubliziert werden.'));
|
||
|
||
return;
|
||
}
|
||
|
||
$service->depublish($pressRelease, 'Depublizierung im Meldeverfahren '.$request->reference(), $request);
|
||
|
||
$request->update([
|
||
'status' => LegalRequestStatus::Resolved->value,
|
||
'admin_note' => $this->adminNote !== '' ? $this->adminNote : null,
|
||
'resolved_by_user_id' => auth()->id(),
|
||
'resolved_at' => now(),
|
||
'executed_at' => now(),
|
||
]);
|
||
|
||
$this->sendExecutedMail($request, 'depublished');
|
||
|
||
$this->notification = __('PM depubliziert (Tombstone, umkehrbar) — Re-Sync-Sperre gesetzt.');
|
||
}
|
||
|
||
/**
|
||
* Umsetzungs-Bestätigung an die meldende Person (Phase 5) — nur wenn
|
||
* eine Antragsteller-Adresse vorliegt (interne Vorgänge haben keine).
|
||
*
|
||
* @param 'anonymized'|'depublished' $action
|
||
*/
|
||
private function sendExecutedMail(LegalRequest $request, string $action): void
|
||
{
|
||
if (! $request->requester_email) {
|
||
return;
|
||
}
|
||
|
||
Mail::to($request->requester_email)->queue(new LegalRequestExecuted($request->fresh(), $action));
|
||
}
|
||
|
||
/**
|
||
* Rückgängig machen: spielt den verschlüsselten Vorher-Snapshot einer
|
||
* ausgeführten Korrektur/Anonymisierung zurück (bis zur Löschfrist).
|
||
*/
|
||
public function restoreSnapshot(\App\Services\PressRelease\CorrectionService $correctionService): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$request = $this->request();
|
||
$snapshot = $request->redactionSnapshots()->latest('id')->first();
|
||
abort_unless($snapshot !== null, 404);
|
||
|
||
$correctionService->restoreSnapshot($snapshot, auth()->user());
|
||
|
||
$request->update([
|
||
'status' => LegalRequestStatus::InProgress->value,
|
||
'executed_at' => null,
|
||
]);
|
||
|
||
$this->notification = __('Vorher-Stand wiederhergestellt — Anfrage wieder in Bearbeitung.');
|
||
}
|
||
|
||
/**
|
||
* @return list<string>
|
||
*/
|
||
private function redactionTermList(): array
|
||
{
|
||
return collect(preg_split('/\r\n|\r|\n/', $this->redactionTerms) ?: [])
|
||
->map(fn (string $term): string => trim($term))
|
||
->filter()
|
||
->unique()
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
private function replacementValue(): string
|
||
{
|
||
return trim($this->redactionReplacement) !== ''
|
||
? trim($this->redactionReplacement)
|
||
: AnonymizationService::DEFAULT_REPLACEMENT;
|
||
}
|
||
|
||
public function canRedact(LegalRequest $request): bool
|
||
{
|
||
return $request->pressRelease !== null
|
||
&& in_array($request->type, [LegalRequestType::Dsgvo, LegalRequestType::PersonalRights], true);
|
||
}
|
||
|
||
/**
|
||
* HTML-Inhalt lesbar und kopierbar machen: Block-Enden zu Zeilenumbrüchen,
|
||
* Tags raus — der Admin kopiert Begriffe direkt aus dieser Ansicht.
|
||
*/
|
||
public function displayValue(?string $value): string
|
||
{
|
||
// Ohne "?"+">" im Pattern — der Volt-Compiler würde die Sequenz als
|
||
// PHP-Schließtag lesen und die Klasse zerschneiden.
|
||
$withBreaks = preg_replace('#<br[^>]*>|</(p|div|h[1-6]|li|blockquote)>#i', "\n", (string) $value) ?? '';
|
||
|
||
return trim(strip_tags($withBreaks));
|
||
}
|
||
|
||
/**
|
||
* @return array<string, string>
|
||
*/
|
||
public function redactableFieldLabels(): array
|
||
{
|
||
return [
|
||
'title' => __('Titel'),
|
||
'subtitle' => __('Untertitel'),
|
||
'text' => __('Text'),
|
||
'boilerplate_override' => __('Boilerplate'),
|
||
'keywords' => __('Keywords'),
|
||
];
|
||
}
|
||
|
||
public function markInProgress(): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$this->request()->update(['status' => LegalRequestStatus::InProgress->value]);
|
||
$this->notification = __('Status auf „In Bearbeitung" gesetzt.');
|
||
}
|
||
|
||
public function resolve(): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$this->finish(LegalRequestStatus::Resolved, __('Anfrage als erledigt markiert.'));
|
||
}
|
||
|
||
public function reject(): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$this->finish(LegalRequestStatus::Rejected, __('Anfrage abgelehnt.'));
|
||
}
|
||
|
||
public function reopen(): void
|
||
{
|
||
$this->authorize('compliance:manage');
|
||
|
||
$this->request()->update([
|
||
'status' => LegalRequestStatus::Open->value,
|
||
'resolved_by_user_id' => null,
|
||
'resolved_at' => null,
|
||
]);
|
||
$this->notification = __('Anfrage wieder geöffnet.');
|
||
}
|
||
|
||
private function finish(LegalRequestStatus $status, string $message): void
|
||
{
|
||
$this->request()->update([
|
||
'status' => $status->value,
|
||
'admin_note' => $this->adminNote !== '' ? $this->adminNote : null,
|
||
'resolved_by_user_id' => auth()->id(),
|
||
'resolved_at' => now(),
|
||
]);
|
||
|
||
$this->notification = $message;
|
||
}
|
||
|
||
private function request(): LegalRequest
|
||
{
|
||
return LegalRequest::query()->with(['pressRelease.images', 'resolver'])->findOrFail($this->id);
|
||
}
|
||
|
||
public function with(): array
|
||
{
|
||
return ['request' => $this->request()];
|
||
}
|
||
|
||
public function statusBadgeClass(LegalRequestStatus $status): string
|
||
{
|
||
return match ($status) {
|
||
LegalRequestStatus::Open => 'warn',
|
||
LegalRequestStatus::InProgress => 'hub',
|
||
LegalRequestStatus::Resolved => 'ok',
|
||
LegalRequestStatus::Rejected => 'err',
|
||
};
|
||
}
|
||
}; ?>
|
||
|
||
<div class="space-y-8">
|
||
{{-- ============== PAGE HEADER ============== --}}
|
||
<x-portal.page-header
|
||
size="sm"
|
||
:badge="__('Admin Backend')"
|
||
:eyebrow="__('Recht & Compliance · Anfrage')"
|
||
:title="$request->type->label()"
|
||
>
|
||
<x-slot:meta>
|
||
<span class="badge dot {{ $this->statusBadgeClass($request->status) }}">{{ $request->status->label() }}</span>
|
||
</x-slot:meta>
|
||
<x-slot:actions>
|
||
<flux:button icon="arrow-left" variant="filled" href="{{ route('admin.legal-requests.index') }}" wire:navigate>
|
||
{{ __('Zurück') }}
|
||
</flux:button>
|
||
</x-slot:actions>
|
||
</x-portal.page-header>
|
||
|
||
@if ($notification)
|
||
<div x-data="{ show: true }" x-init="setTimeout(() => show = false, 3000)" x-show="show" x-transition
|
||
class="px-4 py-3 rounded-[5px] border text-[12.5px] flex items-center gap-2
|
||
bg-[color:var(--color-ok-soft)] border-[color:var(--color-ok)]/30 text-[color:var(--color-gain-deep)]">
|
||
<flux:icon.check-circle class="size-[16px] flex-shrink-0" />
|
||
{{ $notification }}
|
||
</div>
|
||
@endif
|
||
|
||
<div class="grid gap-6 xl:grid-cols-3">
|
||
{{-- ============== ANFRAGE ============== --}}
|
||
<article class="panel xl:col-span-2">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Anliegen') }}</span>
|
||
</div>
|
||
<div class="p-5 space-y-4">
|
||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 text-[13px] text-[color:var(--color-ink)] whitespace-pre-line">
|
||
{{ $request->message ?: __('Keine Nachricht hinterlegt.') }}
|
||
</div>
|
||
|
||
<dl class="grid gap-3 sm:grid-cols-2 text-[12.5px]">
|
||
<div>
|
||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Vorgangsnummer') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)] font-mono">{{ $request->reference() }}</dd>
|
||
</div>
|
||
@if ($request->requesterName())
|
||
<div>
|
||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Name') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)]">{{ $request->requesterName() }}</dd>
|
||
</div>
|
||
@endif
|
||
<div>
|
||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Absender') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)] break-all">
|
||
@if ($request->requester_email)
|
||
<a href="mailto:{{ $request->requester_email }}"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2">{{ $request->requester_email }}</a>
|
||
@else
|
||
{{ __('anonym') }}
|
||
@endif
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Eingegangen') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)]">{{ $request->created_at?->format('d.m.Y H:i') ?? '-' }}</dd>
|
||
</div>
|
||
<div class="sm:col-span-2">
|
||
<dt class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold mb-1">{{ __('Pressemitteilung') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)]">
|
||
@if ($request->pressRelease)
|
||
{{ $request->pressRelease->title }}
|
||
@if (\Illuminate\Support\Facades\Route::has('admin.press-releases.show'))
|
||
<a href="{{ route('admin.press-releases.show', $request->pressRelease->id) }}" wire:navigate
|
||
class="ml-2 text-[12px] text-[color:var(--color-hub)] underline underline-offset-2">{{ __('öffnen') }}</a>
|
||
@endif
|
||
@elseif ($request->reportedUrl())
|
||
{{ __('Keine PM zugeordnet — gemeldete URL:') }}
|
||
<a href="{{ $request->reportedUrl() }}" target="_blank" rel="noopener"
|
||
class="text-[color:var(--color-hub)] underline underline-offset-2 break-all">{{ $request->reportedUrl() }}</a>
|
||
@else
|
||
{{ __('PM wurde gelöscht') }}
|
||
@endif
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
</article>
|
||
|
||
{{-- ============== ANONYMISIERUNG (Spur B) ============== --}}
|
||
@if ($this->canRedact($request) && $request->status->isOpen())
|
||
<article class="panel xl:col-span-2">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Anonymisierung (Schwärzung)') }}</span>
|
||
</div>
|
||
<div class="p-5 space-y-4">
|
||
<p class="text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Schwärzt personenbezogene Daten über Wortgrenzen (inkl. Varianten wie „Herr Teplitsky", „Teplitskys"). Kostenlos, ohne KI-Gate; Alt-Werte werden verschlüsselt gesichert und die Re-Sync-Sperre gesetzt.') }}
|
||
</p>
|
||
|
||
@if ((array) ($request->payload['redaction'] ?? []) !== [])
|
||
<div class="rounded-[6px] border border-[color:var(--color-warn)]/40 bg-[color:var(--color-warn-soft)] px-3 py-2 text-[12.5px] text-[color:var(--color-ink)]">
|
||
{{ __('Strukturierter Schwärzungsauftrag aus dem Web-Formular — Begriffe und Optionen sind unten vorbefüllt (:hits Treffer laut Antragsteller-Vorschau).', ['hits' => (int) ($request->payload['redaction']['preview_total_hits'] ?? 0)]) }}
|
||
@if ($request->payload['redaction']['remove_images_requested'] ?? false)
|
||
<span class="font-semibold">{{ __('Antragsteller bittet zusätzlich um Entfernung von Bildern mit Personenbezug — unten auswählen.') }}</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
|
||
<flux:field>
|
||
<flux:textarea wire:model="redactionTerms" rows="4"
|
||
:label="__('Schwärzbegriffe (ein Begriff pro Zeile, Varianten einzeln)')"
|
||
:placeholder="'Matvey Teplitsky Herr Teplitsky Teplitskys'" />
|
||
<flux:error name="redactionTerms" />
|
||
</flux:field>
|
||
|
||
<div class="grid gap-3 sm:grid-cols-2">
|
||
<flux:field>
|
||
<flux:input wire:model="redactionReplacement"
|
||
:label="__('Ersetzungswert (Rolle/Platzhalter, nie leer)')"
|
||
:placeholder="\App\Services\PressRelease\AnonymizationService::DEFAULT_REPLACEMENT" />
|
||
</flux:field>
|
||
<div class="pt-6">
|
||
<flux:checkbox wire:model="detachContacts"
|
||
:label="__('Kontakt-Zuordnungen der PM lösen (Ansprechpartner raus)')" />
|
||
</div>
|
||
</div>
|
||
|
||
@if ($request->pressRelease->images->isNotEmpty())
|
||
<flux:field>
|
||
<flux:label>{{ __('Bilder entfernen (Klarname in Pressebild!)') }}</flux:label>
|
||
<div class="space-y-1">
|
||
@foreach ($request->pressRelease->images as $image)
|
||
<flux:checkbox wire:model="removeImageIds" value="{{ $image->id }}"
|
||
:label="($image->title ?: $image->path)" />
|
||
@endforeach
|
||
</div>
|
||
</flux:field>
|
||
@endif
|
||
|
||
<div class="flex flex-wrap gap-2 pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||
<flux:button variant="filled" icon="eye" wire:click="previewAnonymization">
|
||
{{ $redactionPreview === null ? __('Vorschau erstellen (Pflicht)') : __('Vorschau aktualisieren') }}
|
||
</flux:button>
|
||
@if ($redactionPreview !== null)
|
||
<flux:button variant="primary" icon="shield-check" wire:click="executeAnonymization"
|
||
wire:confirm="{{ __('Schwärzung jetzt ausführen? Alt-Werte werden verschlüsselt gesichert, die Re-Sync-Sperre wird gesetzt.') }}">
|
||
{{ __('Anonymisieren ausführen') }}
|
||
</flux:button>
|
||
@endif
|
||
@if ($request->pressRelease->status === \App\Enums\PressReleaseStatus::Published)
|
||
<flux:button variant="danger" icon="archive-box-arrow-down" wire:click="depublishRelease"
|
||
wire:confirm="{{ __('PM stattdessen depublizieren (Tombstone, umkehrbar)? Richtig, wenn nach Schwärzung nur ein Rumpf bliebe.') }}">
|
||
{{ __('Stattdessen depublizieren') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- Kompletter PM-Inhalt: vor der Vorschau das Original (Begriffe
|
||
von hier kopieren), danach der Stand nach Schwärzung. --}}
|
||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-4 space-y-3">
|
||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||
<p class="text-[12.5px] font-semibold text-[color:var(--color-ink)]">
|
||
{{ $redactionPreview === null
|
||
? __('Aktueller Inhalt — Begriffe von hier in das Feld oben kopieren')
|
||
: __('Vorschau nach Schwärzung') }}
|
||
</p>
|
||
@if ($redactionPreview !== null)
|
||
<span class="badge dot {{ $redactionPreview['total_hits'] > 0 ? 'ok' : 'warn' }}">
|
||
{{ __(':count Ersetzung(en)', ['count' => $redactionPreview['total_hits']]) }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
|
||
@foreach ($this->redactableFieldLabels() as $field => $label)
|
||
@php
|
||
$fieldValue = $redactionPreview['fields'][$field]['after']
|
||
?? $request->pressRelease->getAttribute($field);
|
||
$fieldHits = $redactionPreview['fields'][$field]['hits'] ?? 0;
|
||
@endphp
|
||
@continue(blank($fieldValue))
|
||
<div class="text-[12.5px]">
|
||
<span class="font-mono text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)]">
|
||
{{ $label }}@if ($fieldHits > 0) · {{ $fieldHits }}× {{ __('ersetzt') }}@endif
|
||
</span>
|
||
<p class="mt-1 text-[color:var(--color-ink)] whitespace-pre-line">{{ $this->displayValue($fieldValue) }}</p>
|
||
</div>
|
||
@endforeach
|
||
|
||
@if ($redactionPreview !== null && $redactionPreview['total_hits'] === 0)
|
||
<p class="text-[12.5px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Keine Texttreffer — es würden nur Kontakte/Bilder entfernt.') }}
|
||
</p>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@endif
|
||
|
||
{{-- ============== BEARBEITUNG ============== --}}
|
||
<article class="panel">
|
||
<div class="panel-head">
|
||
<span class="section-eyebrow">{{ __('Bearbeitung') }}</span>
|
||
</div>
|
||
<div class="p-5 space-y-4">
|
||
<flux:field>
|
||
<flux:textarea wire:model="adminNote" :label="__('Interne Notiz')" rows="4"
|
||
:placeholder="__('Wie wurde die Anfrage bearbeitet?')" />
|
||
</flux:field>
|
||
|
||
@if ($request->resolver)
|
||
<p class="text-[12px] text-[color:var(--color-ink-3)]">
|
||
{{ __('Zuletzt bearbeitet von :name am :date', [
|
||
'name' => $request->resolver->name,
|
||
'date' => $request->resolved_at?->format('d.m.Y H:i') ?? '-',
|
||
]) }}
|
||
</p>
|
||
@endif
|
||
|
||
<dl class="text-[12px] text-[color:var(--color-ink-3)] space-y-1">
|
||
<div class="flex justify-between gap-2">
|
||
<dt>{{ __('Verifikation') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)]">
|
||
{{ $request->verification_method?->label() ?? __('nicht verifiziert') }}
|
||
@if ($request->verified_at)
|
||
· {{ $request->verified_at->format('d.m.Y H:i') }}
|
||
@endif
|
||
</dd>
|
||
</div>
|
||
@if ($request->executed_at)
|
||
<div class="flex justify-between gap-2">
|
||
<dt>{{ __('Ausgeführt') }}</dt>
|
||
<dd class="text-[color:var(--color-ink)]">{{ $request->executed_at->format('d.m.Y H:i') }}</dd>
|
||
</div>
|
||
@endif
|
||
</dl>
|
||
|
||
@if ($request->executed_at && $request->redactionSnapshots()->exists())
|
||
<flux:button variant="filled" icon="arrow-uturn-left" wire:click="restoreSnapshot"
|
||
wire:confirm="{{ __('Vorher-Stand aus dem verschlüsselten Snapshot wiederherstellen? Die Re-Sync-Sperre bleibt bestehen.') }}">
|
||
{{ __('Rückgängig machen (Snapshot)') }}
|
||
</flux:button>
|
||
@endif
|
||
|
||
<div class="flex flex-col gap-2 pt-2 border-t border-[color:var(--color-bg-rule)]">
|
||
@if ($request->status->isOpen())
|
||
@if ($request->status !== \App\Enums\LegalRequestStatus::InProgress)
|
||
<flux:button variant="filled" icon="clock" wire:click="markInProgress">
|
||
{{ __('In Bearbeitung nehmen') }}
|
||
</flux:button>
|
||
@endif
|
||
<flux:button variant="primary" icon="check" wire:click="resolve">
|
||
{{ __('Als erledigt markieren') }}
|
||
</flux:button>
|
||
<flux:button variant="danger" icon="x-mark" wire:click="reject">
|
||
{{ __('Ablehnen') }}
|
||
</flux:button>
|
||
@else
|
||
<flux:button variant="filled" icon="arrow-path" wire:click="reopen">
|
||
{{ __('Wieder öffnen') }}
|
||
</flux:button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</div>
|