Files
AFC-Demo/resources/views/livewire/companies/transactions.blade.php
T
2025-10-31 13:55:05 +01:00

958 lines
60 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\Models\Company;
use App\Models\Transaction;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app'), Title('Unternehmensauskunft Transaktionen')] class extends Component {
use WithPagination;
public Company $company;
public ?int $selectedTransactionId = null;
public string $status = 'all';
public string $channel = 'all';
protected int $perPage = 10;
/**
* @var array<string, array<string, mixed>>
*/
protected $queryString = [
'status' => ['except' => 'all'],
'channel' => ['except' => 'all'],
'page' => ['except' => 1],
'selectedTransactionId' => ['except' => null, 'as' => 'transaction'],
];
public function mount(Company $company, ?int $transaction = null): void
{
$this->company = $company;
if ($transaction) {
$this->selectedTransactionId = $this->baseQuery()
->whereKey($transaction)
->value('id');
}
if (! $this->selectedTransactionId) {
$this->selectedTransactionId = $this->baseQuery()
->orderByDesc('requires_review')
->orderByDesc('risk_score')
->orderByDesc('executed_at')
->value('id');
}
}
public function selectTransaction(int $transactionId): void
{
$this->selectedTransactionId = $transactionId;
}
public function updatingStatus(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->refreshSelection();
}
public function updatingChannel(): void
{
$this->resetPage();
}
public function updatedChannel(): void
{
$this->refreshSelection();
}
public function updatedPage(): void
{
$this->refreshSelection();
}
protected function refreshSelection(): void
{
$first = $this->filteredQuery()
->orderByDesc('requires_review')
->orderByDesc('risk_score')
->orderByDesc('executed_at')
->forPage($this->getPage(), $this->perPage)
->first();
$this->selectedTransactionId = $first?->id;
}
#[Computed]
public function statusOptions(): array
{
return [
'all' => __('Alle Status'),
Transaction::STATUS_TRUE_POSITIVE => __('Bestätigte Treffer'),
Transaction::STATUS_FALSE_POSITIVE => __('Fehlalarme'),
Transaction::STATUS_CLEARED => __('Freigegeben'),
];
}
#[Computed]
public function channelOptions(): array
{
$channels = $this->baseQuery()
->select('channel')
->distinct()
->orderBy('channel')
->pluck('channel')
->filter()
->values();
$options = [
'all' => __('Alle Kanäle'),
];
foreach ($channels as $channel) {
$options[$channel] = $channel;
}
return $options;
}
#[Computed]
public function metrics(): array
{
$baseQuery = $this->baseQuery();
$totalCount = (clone $baseQuery)->count();
$totalVolume = (clone $baseQuery)->sum('amount');
$openAlertsQuery = $this->baseQuery()->where('requires_review', true);
$openAlertsCount = (clone $openAlertsQuery)->count();
$openAlertsVolume = (clone $openAlertsQuery)->sum('amount');
$highRiskCount = $totalCount > 0
? $this->baseQuery()->where('risk_score', '>=', 80)->count()
: 0;
$last30DaysQuery = $this->baseQuery()->where('executed_at', '>=', now()->subDays(30));
$last30DaysCount = (clone $last30DaysQuery)->count();
$last30DaysVolume = (clone $last30DaysQuery)->sum('amount');
$statusBreakdown = (clone $baseQuery)
->selectRaw('status, COUNT(*) as total, COALESCE(SUM(amount), 0) as volume')
->groupBy('status')
->get()
->mapWithKeys(fn ($row) => [
$row->status => [
'count' => (int) $row->total,
'amount' => (float) $row->volume,
],
])
->toArray();
return [
'total_count' => $totalCount,
'total_volume' => $totalVolume,
'open_alerts' => [
'count' => $openAlertsCount,
'amount' => $openAlertsVolume,
],
'high_risk_share' => $totalCount > 0 ? (int) round(($highRiskCount / $totalCount) * 100) : 0,
'last_30_days' => [
'count' => $last30DaysCount,
'amount' => $last30DaysVolume,
],
'by_status' => $statusBreakdown,
];
}
#[Computed]
public function transactions(): LengthAwarePaginator
{
return $this->filteredQuery()
->with('company')
->orderByDesc('requires_review')
->orderByDesc('risk_score')
->orderByDesc('executed_at')
->paginate($this->perPage);
}
#[Computed]
public function selectedTransaction(): ?Transaction
{
$transaction = $this->transactions->firstWhere('id', $this->selectedTransactionId);
if (! $transaction && $this->selectedTransactionId) {
$transaction = $this->baseQuery()
->with('company')
->whereKey($this->selectedTransactionId)
->first();
}
return $transaction ?? $this->transactions->first();
}
#[Computed]
public function counterpartyHistory(): Collection
{
$selected = $this->selectedTransaction;
if (! $selected) {
return collect();
}
return $this->baseQuery()
->where('counterparty', $selected->counterparty)
->orderByDesc('executed_at')
->limit(6)
->get();
}
#[Computed]
public function recentAlerts(): Collection
{
return $this->baseQuery()
->where('requires_review', true)
->orderByDesc('executed_at')
->limit(5)
->get();
}
#[Computed]
public function actionChecklist(): array
{
$selected = $this->selectedTransaction;
$items = [
[
'title' => __('KYC- und Screening-Daten abgleichen'),
'description' => __('Prüfen Sie Unternehmens- und Gegenparteidaten gegen Sanktionslisten, PEP-Register und interne Sperrlisten.'),
],
[
'title' => __('Transaktionsverlauf analysieren'),
'description' => __('Bewerten Sie Häufigkeit, Muster und Gegenparteien der letzten Monate, um ungewöhnliche Aktivitäten zu erkennen.'),
],
[
'title' => __('Vier-Augen-Prinzip sicherstellen'),
'description' => __('Organisieren Sie eine Zweitprüfung und dokumentieren Sie alle Entscheidungen revisionssicher.'),
],
];
if ($selected?->status === Transaction::STATUS_TRUE_POSITIVE) {
array_unshift($items, [
'title' => __('Verdachtsmeldung vorbereiten'),
'description' => __('Erstellen Sie den Meldeentwurf für die FIU, sammeln Sie Belege und stellen Sie eine Eskalation sicher.'),
]);
}
return $items;
}
protected function filteredQuery(): Builder
{
return $this->baseQuery()
->when($this->status !== 'all', fn (Builder $query) => $query->where('status', $this->status))
->when($this->channel !== 'all', fn (Builder $query) => $query->where('channel', $this->channel));
}
protected function baseQuery(): Builder
{
return Transaction::query()->where('company_id', $this->company->id);
}
};
?>
@php
/** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator $transactions */
$transactions = $this->transactions;
/** @var \App\Models\Transaction|null $selected */
$selected = $this->selectedTransaction;
$metrics = $this->metrics;
$statusOptions = $this->statusOptions;
$channelOptions = $this->channelOptions;
$counterpartyHistory = $this->counterpartyHistory;
$recentAlerts = $this->recentAlerts;
$actionChecklist = $this->actionChecklist;
$checklistIntro = __('Fokusbereiche für diesen Fall');
$formatCurrency = static fn (float $value): string => number_format($value, 2, ',', '.') . ' €';
$formatAmount = static fn (float $value, string $currency): string => number_format($value, 2, ',', '.') . ' ' . $currency;
$statusStyles = [
Transaction::STATUS_TRUE_POSITIVE => 'bg-gradient-to-r from-rose-500/25 via-rose-500/10 to-rose-400/20 text-rose-700 dark:text-rose-200 border border-rose-500/30 shadow-[0_0_25px_-14px_rgba(244,63,94,0.85)]',
Transaction::STATUS_FALSE_POSITIVE => 'bg-gradient-to-r from-amber-400/25 via-amber-400/10 to-amber-300/20 text-amber-700 dark:text-amber-200 border border-amber-400/30 shadow-[0_0_25px_-14px_rgba(251,191,36,0.85)]',
Transaction::STATUS_CLEARED => 'bg-gradient-to-r from-emerald-400/25 via-emerald-400/10 to-emerald-300/20 text-emerald-700 dark:text-emerald-200 border border-emerald-400/30 shadow-[0_0_25px_-14px_rgba(52,211,153,0.85)]',
];
$statusCopy = [
Transaction::STATUS_TRUE_POSITIVE => __('Ein schwerwiegender Verdacht liegt vor. Priorisieren Sie die Eskalation und bereiten Sie eine Verdachtsmeldung vor.'),
Transaction::STATUS_FALSE_POSITIVE => __('Alarm konnte entkräftet werden. Dokumentieren Sie die Begründung und schließen Sie den Fall.'),
Transaction::STATUS_CLEARED => __('Keine Auffälligkeiten. Dokumentieren und archivieren Sie den Prüfschritt.'),
];
@endphp
<div class="flex flex-col gap-8">
<section class="rounded-3xl border border-slate-200/70 bg-white/95 p-8 shadow-sm backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/90">
<div class="flex flex-wrap items-start justify-between gap-6">
<div class="max-w-2xl space-y-2">
<flux:link :href="route('transaction-review')" wire:navigate class="inline-flex items-center gap-2 text-sm font-semibold text-slate-500 hover:text-slate-700 dark:text-slate-300 dark:hover:text-white">
<flux:icon name="arrow-left" class="h-4 w-4" />
{{ __('Zurück zur Transaktionsprüfung') }}
</flux:link>
<flux:heading size="xl">
{{ $company->legal_name }}
</flux:heading>
<flux:text class="text-sm text-slate-500 dark:text-slate-300">
{{ $company->summary }}
</flux:text>
<div class="flex flex-wrap gap-2 text-xs text-slate-600 dark:text-slate-300">
<span class="inline-flex items-center gap-2 rounded-full bg-slate-100/60 px-3 py-1 font-medium uppercase tracking-wide text-slate-600 dark:bg-slate-800/70 dark:text-slate-200">
{{ __('Ticker') }} {{ $company->ticker }}
</span>
<span class="inline-flex items-center gap-2 rounded-full bg-indigo-100/60 px-3 py-1 font-medium uppercase tracking-wide text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-200">
{{ $company->sector }}
</span>
<span class="inline-flex items-center gap-2 rounded-full bg-emerald-100/60 px-3 py-1 font-medium uppercase tracking-wide text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-100">
{{ __('KYC-Risiko') }} {{ \Illuminate\Support\Str::title($company->kyc_risk_level) }}
</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm sm:grid-cols-3">
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-4 dark:border-slate-700/70 dark:bg-slate-800/80">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Transaktionen gesamt') }}</p>
<p class="mt-2 text-2xl font-semibold text-slate-900 dark:text-white">{{ $metrics['total_count'] }}</p>
<p class="text-xs text-slate-500 dark:text-slate-300">{{ __('Volumen') }} {{ $formatCurrency($metrics['total_volume']) }}</p>
</div>
<div class="rounded-2xl border border-emerald-400/40 bg-emerald-400/10 p-4 shadow-sm shadow-emerald-900/10">
<p class="text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-200">{{ __('Offene Alerts') }}</p>
<p class="mt-2 text-2xl font-semibold text-emerald-900 dark:text-emerald-100">{{ $metrics['open_alerts']['count'] }}</p>
<p class="text-xs text-emerald-700 dark:text-emerald-100">{{ __('Volumen') }} {{ $formatCurrency($metrics['open_alerts']['amount']) }}</p>
</div>
<div class="rounded-2xl border border-indigo-400/40 bg-indigo-500/10 p-4 shadow-sm shadow-indigo-900/10">
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-800 dark:text-indigo-200">{{ __('High-Risk-Anteil') }}</p>
<p class="mt-2 text-2xl font-semibold text-indigo-900 dark:text-indigo-100">{{ $metrics['high_risk_share'] }}%</p>
<p class="text-xs text-indigo-700 dark:text-indigo-200">{{ __('Letzte 30 Tage') }} {{ $metrics['last_30_days']['count'] }}</p>
</div>
</div>
</div>
</section>
@if (! empty($actionChecklist))
<section class="rounded-3xl border border-indigo-300/60 bg-gradient-to-br from-white via-indigo-50/60 to-purple-100/50 p-6 shadow-sm backdrop-blur dark:border-indigo-500/40 dark:bg-indigo-950/80 dark:from-indigo-950/70 dark:via-purple-900/60 dark:to-indigo-900/70">
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wide text-indigo-800 dark:text-indigo-100">{{ $checklistIntro }}</h3>
<p class="mt-1 text-xs text-indigo-800/80 dark:text-indigo-200/80">
{{ __('Bearbeiten Sie die wichtigsten Schritte nacheinander, um den Fallabschluss vorzubereiten.') }}
</p>
</div>
<span class="inline-flex items-center rounded-full border border-indigo-300/60 bg-white/80 px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-indigo-700 dark:border-indigo-500/40 dark:bg-indigo-500/20 dark:text-indigo-100">
{{ count($actionChecklist) }} {{ __('Schritte') }}
</span>
</div>
<div class="mt-5 overflow-x-auto">
<ol class="flex min-w-full gap-4">
@foreach ($actionChecklist as $index => $item)
<li class="group flex min-w-[240px] items-start gap-3 rounded-2xl border border-indigo-200/60 bg-white/95 p-4 shadow-sm transition hover:-translate-y-0.5 hover:border-indigo-300/70 hover:shadow-indigo-500/10 dark:border-indigo-500/30 dark:bg-indigo-900/70">
<span class="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-indigo-300/60 bg-indigo-500/10 text-xs font-semibold text-indigo-700 dark:border-indigo-500/40 dark:bg-indigo-500/25 dark:text-indigo-100">
{{ sprintf('%02d', $index + 1) }}
</span>
<div class="space-y-1">
<p class="font-semibold text-indigo-900 transition group-hover:text-indigo-700 dark:text-indigo-100 dark:group-hover:text-indigo-200">{{ $item['title'] }}</p>
<p class="text-xs leading-relaxed text-indigo-800/80 dark:text-indigo-100/80">{{ $item['description'] }}</p>
</div>
</li>
@endforeach
</ol>
</div>
</section>
@endif
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,1.85fr)]">
<div class="space-y-4">
<div class="rounded-3xl border border-zinc-200/70 bg-white/95 p-6 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
<div class="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">{{ __('Transaktionsliste') }}</h2>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('Filtern Sie nach Status und Kanal, um relevante Vorgänge für dieses Unternehmen zu analysieren.') }}</p>
</div>
<flux:badge variant="outline">
{{ $transactions->total() }} {{ __('Ergebnisse') }}
</flux:badge>
</div>
<div class="mt-4 flex flex-wrap items-center gap-3">
<flux:select wire:model.live="status">
@foreach ($statusOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
<flux:select wire:model.live="channel">
@foreach ($channelOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</flux:select>
</div>
<div class="mt-6 space-y-3">
@forelse ($transactions as $transaction)
<button
wire:click="selectTransaction({{ $transaction->id }})"
wire:key="txn-{{ $transaction->id }}"
@class([
'w-full rounded-2xl border p-4 text-left transition focus:outline-none focus:ring-2 focus:ring-indigo-500/80',
'border-zinc-200/70 bg-white shadow-sm backdrop-blur dark:border-zinc-700/60 dark:bg-zinc-900/80' => $selected?->id !== $transaction->id,
'border-indigo-300/70 bg-indigo-50/80 shadow-md ring-2 ring-indigo-200/60 dark:border-indigo-500/40 dark:bg-indigo-900/50 dark:ring-indigo-500/30' => $selected?->id === $transaction->id,
])>
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="space-y-1">
<p class="text-sm font-semibold text-zinc-800 dark:text-zinc-100">
{{ $transaction->counterparty }}
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $transaction->counterparty_country }} • {{ $transaction->executed_at?->format('d.m.Y H:i') }}
</p>
</div>
<div class="flex flex-col items-end gap-2">
<span class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{{ $formatAmount($transaction->amount, $transaction->currency) }}
</span>
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {{ $statusStyles[$transaction->status] ?? 'bg-zinc-500/10 text-zinc-600 border border-zinc-500/20' }}">
{{ $transaction->statusLabel() }}
</span>
</div>
</div>
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
<span class="inline-flex items-center gap-1 rounded-md bg-slate-100/60 px-2 py-1 font-semibold text-slate-700 dark:bg-slate-800/80 dark:text-slate-200">
Risiko {{ $transaction->risk_score }}
</span>
<span>{{ $transaction->channel }}</span>
<span>•</span>
<span class="font-mono">{{ $transaction->reference }}</span>
<flux:link
:href="route('company.transactions', ['company' => $company->id, 'transaction' => $transaction->id])"
wire:navigate
class="inline-flex items-center gap-1 font-medium text-indigo-600 transition hover:text-indigo-800 dark:text-indigo-300 dark:hover:text-indigo-200"
>
{{ __('Fallansicht öffnen') }}
<flux:icon name="arrow-top-right-on-square" class="h-4 w-4" />
</flux:link>
</div>
</button>
@empty
<div class="rounded-2xl border border-dashed border-zinc-300/70 bg-zinc-50/70 p-6 text-center text-sm text-zinc-500 dark:border-zinc-600/50 dark:bg-zinc-900/70 dark:text-zinc-300">
{{ __('Keine Transaktionen für die aktuellen Filter gefunden.') }}
</div>
@endforelse
</div>
@if ($transactions->hasPages())
<div class="mt-4 flex items-center justify-between gap-3 border-t border-zinc-200/70 bg-zinc-50/70 px-4 py-3 text-sm text-zinc-500 dark:border-zinc-700/60 dark:bg-zinc-900/70 dark:text-zinc-300">
<button
wire:click="previousPage"
wire:loading.attr="disabled"
@disabled($transactions->onFirstPage())
class="inline-flex items-center gap-2 rounded-full border border-zinc-300/70 bg-white/70 px-4 py-1.5 font-medium text-zinc-700 transition hover:bg-white dark:border-zinc-600/70 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:bg-zinc-800/60"
>
{{ __('Zurück') }}
</button>
<span class="text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
{{ __('Seite') }} {{ $transactions->currentPage() }} {{ __('von') }} {{ $transactions->lastPage() }}
</span>
<button
wire:click="nextPage"
wire:loading.attr="disabled"
@disabled(! $transactions->hasMorePages())
class="inline-flex items-center gap-2 rounded-full border border-zinc-300/70 bg-white/70 px-4 py-1.5 font-medium text-zinc-700 transition hover:bg-white dark:border-zinc-600/70 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:bg-zinc-800/60"
>
{{ __('Weiter') }}
</button>
</div>
@endif
</div>
<div class="rounded-3xl border border-zinc-200/70 bg-white/95 p-6 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
<h3 class="text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Neueste Alerts') }}</h3>
<ul class="mt-4 space-y-3 text-sm text-zinc-600 dark:text-zinc-300">
@forelse ($recentAlerts as $alert)
<li class="flex items-start justify-between gap-3 rounded-2xl border border-zinc-100/70 bg-zinc-50/70 p-3 dark:border-zinc-700/70 dark:bg-zinc-800/70">
<div>
<p class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $alert->counterparty }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $alert->executed_at?->format('d.m.Y H:i') }} • {{ $alert->channel }}</p>
</div>
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {{ $statusStyles[$alert->status] ?? 'bg-zinc-500/10 text-zinc-600 border border-zinc-500/20' }}">
{{ $alert->statusLabel() }}
</span>
</li>
@empty
<li class="rounded-2xl border border-dashed border-zinc-300/60 bg-zinc-50/60 p-4 text-center text-xs text-zinc-500 dark:border-zinc-600/50 dark:bg-zinc-900/60">
{{ __('Aktuell liegen keine offenen Alerts vor.') }}
</li>
@endforelse
</ul>
</div>
</div>
<div class="space-y-6">
@if ($selected)
@php
$badgeStyle = $statusStyles[$selected->status] ?? 'bg-zinc-500/10 text-zinc-600 border border-zinc-500/20';
$riskLevel = match (true) {
$selected->risk_score >= 80 => __('Hoch'),
$selected->risk_score >= 60 => __('Erhöht'),
$selected->risk_score >= 40 => __('Moderat'),
default => __('Niedrig'),
};
$riskNarrative = match (true) {
$selected->risk_score >= 80 => __('Die Einstufung über 80 Punkten signalisiert einen erhöhten AML-Prüffokus. Eskalation und enges Monitoring sind angeraten.'),
$selected->risk_score >= 60 => __('Das Risiko liegt im oberen Mittelfeld. Prüfen Sie Plausibilitäten und dokumentieren Sie Gegenmaßnahmen.'),
$selected->risk_score >= 40 => __('Die Einstufung erscheint moderat. Stellen Sie sicher, dass grundlegende Prüfungen vollständig dokumentiert sind.'),
default => __('Der Score deutet auf ein geringes AML-Risiko hin. Ein strukturierter Nachweis der Prüfung ist dennoch erforderlich.'),
};
$statusGuidance = $statusCopy[$selected->status] ?? __('Review abschließen und Audit-Trail ergänzen.');
$executedDate = $selected->executed_at?->format('d.m.Y');
$executedTime = $selected->executed_at?->format('H:i');
$amountValue = $formatAmount($selected->amount, $selected->currency);
$companyName = $company->legal_name ?? $company->name;
$companyCountryLabel = $company->country ?? __('Jurisdiktion offen');
$counterpartyCountryLabel = $selected->counterparty_country ?? __('Jurisdiktion offen');
$referenceLabel = $selected->reference ?: __('Keine Referenz angegeben');
$flaggedReasonLabel = $selected->flagged_reason ?: __('Nicht dokumentiert');
$domesticFlow = $company->country && $selected->counterparty_country && strcasecmp($company->country, $selected->counterparty_country) === 0;
$purposeDescription = __('Die Zahlung nutzt den Referenztext ":reference" und wurde mit dem Flag ":reason" markiert. Prüfen Sie Rechnungs- und Liefernachweise, um den wirtschaftlichen Anlass zu bestätigen.', [
'reference' => $referenceLabel,
'reason' => $flaggedReasonLabel,
]);
$countryNotes = [];
if ($domesticFlow) {
$countryNotes[] = __('Transparency International CPI 2024: 75/100 (Rang 15).');
} else {
$countryNotes[] = __('Ermitteln Sie länderspezifische Restrisiken (Sanktionen, Exportkontrolle, Devisenauflagen).');
}
if (! $company->country || ! $selected->counterparty_country) {
$countryNotes[] = __('Länderangaben unvollständig KYC-Stammdaten aktualisieren.');
}
$analysisSections = [
[
'title' => __('Verwendungszweck'),
'description' => $purposeDescription,
'notes' => [
__('Rechnungskopie inkl. HR-/VAT-/IBAN-Angaben anfordern.'),
__('3-Way-Match (POGRInvoice) vollständig dokumentieren.'),
],
],
[
'title' => __('LEI'),
'description' => __('Für :company und :counterparty liegt kein verifizierter LEI im Datensatz vor. Ein Direktabruf im GLEIF-Register wird empfohlen.', [
'company' => $companyName,
'counterparty' => $selected->counterparty,
]),
'notes' => [
__('Status: not verified.'),
__('Bankinterne LEI-Prüfung vor Freigabe abschließen.'),
],
],
[
'title' => __('Länderanalyse'),
'description' => $domesticFlow
? __('Vorliegende Daten deuten auf eine Inlandszahlung (:from → :to) hin. Das systemische Korruptionsrisiko gilt als niedrig.', [
'from' => $companyCountryLabel,
'to' => $counterpartyCountryLabel,
])
: __('Transaktion verläuft von :from nach :to. Prüfen Sie Export-/Importauflagen sowie mögliche Hochrisikofaktoren.', [
'from' => $companyCountryLabel,
'to' => $counterpartyCountryLabel,
]),
'notes' => $countryNotes,
],
[
'title' => __('PEP-Exposure'),
'description' => __('Es liegen keine Hinweise auf PEP-Einfluss in den Stammdaten vor. Screening der wirtschaftlich Berechtigten bleibt fortzuführen.'),
'notes' => [
__('Manuelles Screening der Management- und Eigentümerebene abschließen (Status: not verified).'),
],
],
[
'title' => __('AML Exposure (transaktionsbezogen)'),
'description' => __('Klarer Leistungszweck und etablierte Gegenparteien wirken risikomindernd. Restrisiko besteht in der eindeutigen Identifikation der Rechnungseinheit und des Zahlungswegs.'),
'notes' => [
__('Kontoinhaber und IBAN mit der Rechnungseinheit matchen.'),
__('Preis- und Mengenabweichungen gegen Bestellung analysieren.'),
],
],
[
'title' => __('Sanktions-Exposure'),
'description' => __('Bisherige Namenstests ergaben keine Treffer. Automatisierte Listenprüfungen sind auf den finalen Rechtsträger erneut auszuführen.'),
'notes' => [
__('EU-/OFAC-Abgleich sowie interne Sanktionslisten auf finaler Rechnungseinheit bestätigen.'),
],
],
[
'title' => __('Korruptions-/Bestechungs-Exposure'),
'description' => __('Makrorisiko im deutschen Markt ist gering. Beschaffungsrisiken (Kick-backs, Scheinrechnungen) bleiben bei Großvolumina bestehen.'),
'notes' => [
__('Zahlungen ausschließlich an bestätigte Gesellschaftskonten leisten.'),
__('Vier-Augen-Prinzip und Liefernachweise dokumentieren.'),
],
],
[
'title' => __('Adverse Media Scanning (≤ 12 Monate)'),
'description' => __('In den letzten 12 Monaten wurden keine AML-relevanten Negativmeldungen identifiziert. Restrisiko: laufende Restrukturierungs- und Compliance-News beobachten.'),
'notes' => [
__('Interne Newsfeeds und Watchlists weiterführen.'),
],
],
[
'title' => __('KYC/EDD Feststellungen'),
'description' => __('KYC-Dossier aktualisieren: rechtliche Einheit, wirtschaftlich Berechtigte und Zahlungsweg eindeutig dokumentieren.'),
'notes' => [
__('IBAN/BIC sowie Steuerkennzeichen im Dossier hinterlegen.'),
__('Rechnungskopie, Liefernachweis und Vertragsgrundlage einholen.'),
],
],
[
'title' => __('Gesamtbeurteilung (AML-Sicht)'),
'description' => __('Die Transaktion über :amount zwischen :company und :counterparty wirkt branchenüblich. Freigabe ist möglich, sobald Rechnungseinheit, Zahlungsweg und Screening final bestätigt sind.', [
'amount' => $amountValue,
'company' => $companyName,
'counterparty' => $selected->counterparty,
]),
'notes' => [
__('Votum: Freigabefähig unter Auflagen (Rechnung, 3-Way-Match, Screening, Exportkontrolle).'),
],
],
];
$combineNotes = static function (array $sections): array {
$notes = [];
foreach ($sections as $section) {
$notes = array_merge($notes, $section['notes'] ?? []);
}
return $notes;
};
$clusterFactory = static function (string $title, array $sections) use ($combineNotes): array {
$leadSection = $sections[0] ?? null;
$contexts = array_slice($sections, 1);
return [
'title' => $title,
'lead_label' => $leadSection['title'] ?? null,
'lead' => $leadSection['description'] ?? '',
'contexts' => array_map(
static fn (array $section): array => [
'label' => $section['title'],
'body' => $section['description'],
],
$contexts
),
'actions' => $combineNotes($sections),
'count' => count($sections),
];
};
$transactionSections = array_slice($analysisSections, 0, 2);
$riskSections = array_slice($analysisSections, 2, 6);
$dueSections = array_slice($analysisSections, 8);
$analysisClusters = [
$clusterFactory(__('Transaktion & Zweck'), $transactionSections),
$clusterFactory(__('Risikoperspektive'), $riskSections),
$clusterFactory(__('Due Diligence & Abschluss'), $dueSections),
];
$analysisClusterCount = array_sum(array_map(static fn (array $cluster): int => $cluster['count'], $analysisClusters));
$sources = array_values(array_filter([
$companyName ? __('Unternehmensprofil :company (:country) interne KYC-Stammdaten.', [
'company' => $companyName,
'country' => $companyCountryLabel,
]) : null,
__('Transaktionsdatensatz #:id Core Banking Export.', ['id' => $selected->id]),
$selected->reference ? __('Rechnungsreferenz ":reference" Plausibilisierung steht aus.', ['reference' => $referenceLabel]) : null,
__('GLEIF LEI Register (Direktabruf, Status: not verified).'),
__('Transparency International CPI 2024 (Deutschland 75/100).'),
__('EU/OFAC Sanktionsportale orientierender Namensabgleich (endgültige Prüfung intern).'),
]));
@endphp
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.65fr)_minmax(0,1fr)]">
<div class="rounded-3xl border border-slate-200/70 bg-white/95 p-7 shadow-lg shadow-slate-900/5 backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/90">
<div class="flex flex-wrap items-start justify-between gap-6">
<div class="space-y-2">
<flux:badge variant="outline">
{{ __('Case ID') }} {{ $selected->id }}
</flux:badge>
<h3 class="text-2xl font-semibold text-slate-900 dark:text-white">
{{ $selected->counterparty }} — {{ $amountValue }}
</h3>
<p class="text-sm text-slate-500 dark:text-slate-300">
{{ __('Ausgeführt am') }} {{ $executedDate ?? __('Nicht verfügbar') }}
@if ($executedTime)
• {{ $executedTime }}
@endif
• {{ $selected->channel ?: __('Kanal offen') }}
</p>
</div>
<div class="flex flex-col items-end gap-2">
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {{ $badgeStyle }}">
{{ $selected->statusLabel() }}
</span>
<span class="inline-flex items-center gap-1 rounded-md bg-slate-900/10 px-3 py-1 font-semibold text-slate-800 dark:bg-slate-200/10 dark:text-slate-200">
{{ __('Risikowert') }} {{ $selected->risk_score }} ({{ $riskLevel }})
</span>
</div>
</div>
<div class="mt-7 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-4 dark:border-slate-700/70 dark:bg-slate-800/70">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Stichtag') }}</p>
<p class="mt-1 text-sm font-medium text-slate-900 dark:text-slate-100">{{ $executedDate ?? __('Nicht verfügbar') }}</p>
<p class="text-xs text-slate-500 dark:text-slate-300">{{ $executedTime ? __('Zeit') . ' ' . $executedTime : __('Zeitpunkt offen') }}</p>
</div>
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-4 dark:border-slate-700/70 dark:bg-slate-800/70">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Kanal & Referenz') }}</p>
<p class="mt-1 text-sm font-medium text-slate-900 dark:text-slate-100">{{ $selected->channel ?: __('Unbekannt') }}</p>
<p class="text-xs text-slate-500 dark:text-slate-300">{{ $selected->reference ?: __('Keine Referenz hinterlegt') }}</p>
</div>
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-4 dark:border-slate-700/70 dark:bg-slate-800/70">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Flagging durch') }}</p>
<p class="mt-1 text-sm font-medium text-slate-900 dark:text-slate-100">{{ $selected->flagged_by ?: __('Automatisiertes Monitoring') }}</p>
<p class="text-xs text-slate-500 dark:text-slate-300">{{ $selected->flagged_reason ?: __('Flagging-Grund nicht dokumentiert') }}</p>
</div>
<div class="rounded-2xl border border-indigo-300/60 bg-indigo-500/10 p-4 text-indigo-900 shadow-sm shadow-indigo-900/10 dark:border-indigo-500/40 dark:bg-indigo-500/10 dark:text-indigo-100">
<p class="text-xs font-semibold uppercase tracking-wide">{{ __('Empfohlene Maßnahme') }}</p>
<p class="mt-1 text-sm font-medium leading-relaxed">{{ $statusGuidance }}</p>
</div>
</div>
<div class="mt-8">
<h4 class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Parteiprofile') }}</h4>
<div class="mt-3 grid gap-4 lg:grid-cols-2">
<div class="group rounded-2xl border border-slate-200/80 bg-white/70 p-4 transition hover:border-indigo-300/70 hover:shadow-md dark:border-slate-700/70 dark:bg-slate-900/70 dark:hover:border-indigo-400/50">
<div class="flex items-center justify-between">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">{{ __('Sending party') }}</p>
<span class="inline-flex items-center rounded-full border border-indigo-300/60 bg-indigo-500/10 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-indigo-700 dark:border-indigo-500/40 dark:bg-indigo-500/20 dark:text-indigo-100">
{{ __('Unternehmensprofil') }}
</span>
</div>
<p class="mt-2 text-sm font-semibold text-slate-900 dark:text-white">{{ $selected->company?->legal_name ?? $company->legal_name ?? $company->name }}</p>
<dl class="mt-3 space-y-1 text-xs text-slate-600 dark:text-slate-300">
<div class="flex justify-between gap-3">
<dt>{{ __('Jurisdiktion') }}</dt>
<dd class="text-right">{{ $company->country ?? __('Nicht verifiziert') }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt>{{ __('Sitz') }}</dt>
<dd class="text-right">{{ $company->headquarters ?? __('Nicht verifiziert') }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt>{{ __('Sektor') }}</dt>
<dd class="text-right">{{ $company->sector ?? __('Nicht verifiziert') }}</dd>
</div>
</dl>
</div>
<div class="group rounded-2xl border border-slate-200/80 bg-white/70 p-4 transition hover:border-indigo-300/70 hover:shadow-md dark:border-slate-700/70 dark:bg-slate-900/70 dark:hover:border-indigo-400/50">
<div class="flex items-center justify-between">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">{{ __('Receiving party') }}</p>
<span class="inline-flex items-center rounded-full border border-zinc-300/60 bg-zinc-100/60 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-zinc-700 dark:border-zinc-600/50 dark:bg-zinc-800/60 dark:text-zinc-200">
{{ __('Verifizierung offen') }}
</span>
</div>
<p class="mt-2 text-sm font-semibold text-slate-900 dark:text-white">{{ $selected->counterparty }}</p>
<dl class="mt-3 space-y-1 text-xs text-slate-600 dark:text-slate-300">
<div class="flex justify-between gap-3">
<dt>{{ __('Jurisdiktion') }}</dt>
<dd class="text-right">{{ $selected->counterparty_country ?? __('Nicht verifiziert') }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt>{{ __('Referenzhinweis') }}</dt>
<dd class="text-right">{{ $selected->reference ?: __('Keine Angabe') }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt>{{ __('Next step') }}</dt>
<dd class="text-right">{{ __('IBAN/Beneficiary eindeutig zuordnen') }}</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="mt-8 grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/80 p-5 dark:border-slate-700/70 dark:bg-slate-800/70">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Transaktionsbewertung') }}</p>
<p class="mt-2 text-sm text-slate-700 dark:text-slate-200">{{ $riskNarrative }}</p>
<ul class="mt-3 space-y-2 text-xs text-slate-500 dark:text-slate-300">
<li>• {{ __('Betrag') }} {{ $amountValue }}</li>
<li>• {{ __('Flagging-Grund') }}: {{ $selected->flagged_reason ?: __('nicht dokumentiert') }}</li>
<li>• {{ __('Kanal') }}: {{ $selected->channel ?: __('unbekannt') }}</li>
</ul>
</div>
<div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/10 p-5 text-sm text-emerald-900 shadow-sm shadow-emerald-900/10 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-100">
<p class="text-xs font-semibold uppercase tracking-wide">{{ __('Kontrollaufgaben (Zusammenfassung)') }}</p>
<p class="mt-2 leading-relaxed">
{{ __('Sicherstellen, dass Rechnungskopf, Beneficiary-Daten und 3-Way-Match (POGRInvoice) dokumentiert sind. Interne Listenabgleiche (Sanktionen/PEP) auf den konkreten Rechtsträger erneut ausführen.') }}
</p>
</div>
</div>
@if (! empty($selected->signals))
<div class="mt-8">
<h4 class="text-xs font-semibold uppercase tracking-wide text-slate-400">{{ __('Detektionssignale') }}</h4>
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($selected->signals as $signal)
<span class="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-slate-100/70 px-3 py-1 text-xs font-medium text-slate-700 dark:border-slate-600 dark:bg-slate-800/60 dark:text-slate-200">
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
{{ data_get($signal, 'type') }} — {{ data_get($signal, 'value') }}
</span>
@endforeach
</div>
</div>
@endif
<div class="mt-8 rounded-2xl border border-indigo-200/60 bg-indigo-500/10 p-5 text-sm text-indigo-900 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-100">
<p class="text-xs font-semibold uppercase tracking-wide">{{ __('Narrative Zusammenfassung') }}</p>
<p class="mt-2 leading-relaxed">
{{ __('Die Transaktion verbindet :company (:jurisdiction) mit :counterparty (:country). Der Betrag von :amount wurde über den Kanal :channel ausgeführt. Für die abschließende AML-Beurteilung ist der Nachweis der wirtschaftlich Berechtigten sowie ein Abgleich der Zahlungsempfänger-Daten erforderlich.', [
'company' => $company->legal_name ?? $company->name,
'jurisdiction' => $company->country ?? __('Jurisdiktion offen'),
'counterparty' => $selected->counterparty,
'country' => $selected->counterparty_country ?? __('Jurisdiktion offen'),
'amount' => $amountValue,
'channel' => $selected->channel ?: __('unbekannt'),
]) }}
</p>
<p class="mt-2 text-xs text-indigo-800 dark:text-indigo-200">
{{ __('Hinweis: Interne Quellen (ERP, KYC-Stammdaten, Screening) ergänzen, um den Verifizierungsstatus auf „abgeschlossen“ anzuheben.') }}
</p>
</div>
@if (! empty($analysisClusters))
<div class="mt-8 space-y-5">
<div class="rounded-3xl border border-slate-200/70 bg-white/95 p-6 shadow-sm transition hover:shadow-md dark:border-slate-700/70 dark:bg-slate-900/90">
<div class="flex flex-wrap items-center justify-between gap-3">
<h4 class="text-sm font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-200">{{ __('Vertiefte Analysefelder') }}</h4>
<span class="inline-flex items-center gap-2 rounded-full border border-slate-200/70 bg-slate-50/80 px-3 py-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500 dark:border-slate-600/60 dark:bg-slate-800/70 dark:text-slate-200">
{{ $analysisClusterCount }} {{ __('Prüfpunkte') }}
</span>
</div>
<p class="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{{ __('Verdichten Sie die Prüfung auf drei Themenblöcke. Jeder Block fasst die wesentlichen Anforderungen zusammen.') }}
</p>
</div>
@foreach ($analysisClusters as $clusterIndex => $cluster)
<div class="rounded-3xl border border-slate-200/70 bg-white/95 p-6 shadow-sm dark:border-slate-700/70 dark:bg-slate-900/90">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="max-w-2xl space-y-2">
@if ($cluster['lead_label'])
<span class="inline-flex items-center rounded-full border border-slate-300/70 bg-slate-100/60 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-600 dark:border-slate-600/60 dark:bg-slate-800/70 dark:text-slate-200">
{{ $cluster['lead_label'] }}
</span>
@endif
<h5 class="text-lg font-semibold text-slate-900 dark:text-white">
{{ sprintf('%02d', $clusterIndex + 1) }} — {{ $cluster['title'] }}
</h5>
<p class="text-sm leading-relaxed text-slate-700 dark:text-slate-200">
{{ $cluster['lead'] }}
</p>
</div>
<span class="inline-flex items-center rounded-full border border-slate-200/70 bg-slate-50/80 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:border-slate-600/60 dark:bg-slate-800/70 dark:text-slate-300">
{{ $cluster['count'] }} {{ __('Teilfelder') }}
</span>
</div>
@if (! empty($cluster['contexts']))
<div class="mt-5 grid gap-3 md:grid-cols-2">
@foreach ($cluster['contexts'] as $context)
<div class="rounded-2xl border border-slate-200/60 bg-slate-50/70 p-4 dark:border-slate-700/60 dark:bg-slate-800/70">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300">{{ $context['label'] }}</p>
<p class="mt-1 text-sm leading-relaxed text-slate-600 dark:text-slate-200">{{ $context['body'] }}</p>
</div>
@endforeach
</div>
@endif
@if (! empty($cluster['actions']))
<div class="mt-5 rounded-2xl border border-dashed border-slate-300/60 bg-slate-50/70 p-4 text-xs text-slate-600 dark:border-slate-600/60 dark:bg-slate-800/70 dark:text-slate-200">
<p class="font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-200">{{ __('Schlüsselmaßnahmen') }}</p>
<ul class="mt-2 space-y-1 leading-relaxed">
@foreach ($cluster['actions'] as $action)
<li class="flex items-start gap-2">
<span class="mt-1 h-1 w-1 shrink-0 rounded-full bg-slate-400 dark:bg-slate-300"></span>
<span>{{ $action }}</span>
</li>
@endforeach
</ul>
</div>
@endif
</div>
@endforeach
</div>
@endif
@if (! empty($sources))
<div class="mt-6 rounded-2xl border border-slate-200/70 bg-slate-50/80 p-5 dark:border-slate-700/70 dark:bg-slate-800/70">
<h5 class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">{{ __('Quellen & Verifizierungsstand (≤ 365 Tage)') }}</h5>
<ul class="mt-3 list-disc space-y-1 pl-4 text-xs text-slate-600 dark:text-slate-300">
@foreach ($sources as $source)
<li>{{ $source }}</li>
@endforeach
</ul>
</div>
@endif
</div>
<div class="space-y-6">
<div class="rounded-3xl border border-zinc-200/70 bg-white/95 p-6 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
<h4 class="text-sm font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ __('Historie mit dieser Gegenpartei') }}</h4>
<div class="mt-4 space-y-3">
@forelse ($counterpartyHistory as $history)
<div class="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-zinc-100/70 bg-zinc-50/70 p-4 text-sm text-zinc-600 dark:border-zinc-700/70 dark:bg-zinc-800/70 dark:text-zinc-300">
<div>
<p class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $history->executed_at?->format('d.m.Y H:i') }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $history->channel }} • {{ $history->reference }}</p>
</div>
<div class="flex flex-col items-end gap-1">
<span class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{{ $formatAmount($history->amount, $history->currency) }}</span>
<span class="inline-flex items-center gap-1 rounded-md bg-slate-100/60 px-2 py-0.5 text-xs font-semibold text-slate-700 dark:bg-slate-800/80 dark:text-slate-200">
{{ __('Risiko') }} {{ $history->risk_score }}
</span>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-zinc-300/60 bg-zinc-50/60 p-4 text-center text-xs text-zinc-500 dark:border-zinc-600/50 dark:bg-zinc-900/60">
{{ __('Keine weiteren Transaktionen mit dieser Gegenpartei gefunden.') }}
</div>
@endforelse
</div>
</div>
</div>
</div>
@else
<div class="rounded-3xl border border-dashed border-zinc-300/70 bg-white/80 p-8 text-center text-sm text-zinc-500 shadow-sm dark:border-zinc-600/50 dark:bg-zinc-900/60 dark:text-zinc-300">
{{ __('Wählen Sie links eine Transaktion aus, um die vollständige Fallansicht zu öffnen.') }}
</div>
@endif
</div>
</div>
</div>