255 lines
16 KiB
PHP
255 lines
16 KiB
PHP
<?php
|
||
|
||
use App\Models\Company;
|
||
use App\Models\Transaction;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Str;
|
||
use Livewire\Attributes\Computed;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Volt\Component;
|
||
|
||
new #[Layout('components.layouts.app'), Title('Unternehmensauskunft')] class extends Component {
|
||
public string $search = '';
|
||
|
||
#[Computed]
|
||
public function overview(): array
|
||
{
|
||
$totalCompanies = Company::count();
|
||
|
||
$openAlertsQuery = Transaction::query()->where('requires_review', true);
|
||
$openAlertsCount = (clone $openAlertsQuery)->count();
|
||
$openAlertsVolume = (clone $openAlertsQuery)->sum('amount');
|
||
$avgRisk = round((clone $openAlertsQuery)->avg('risk_score') ?? 0);
|
||
$watchlistHits = (clone $openAlertsQuery)->where('signals', 'like', '%Sanktionsliste%')->count();
|
||
|
||
$totalTransactions = Transaction::count();
|
||
$falsePositives = Transaction::where('status', Transaction::STATUS_FALSE_POSITIVE)->count();
|
||
$automationShare = 100 - (int) round(($falsePositives / max($totalTransactions, 1)) * 100);
|
||
$automationShare = max(68, min($automationShare, 94));
|
||
|
||
return [
|
||
'companies' => $totalCompanies,
|
||
'open_alerts' => $openAlertsCount,
|
||
'open_volume' => $openAlertsVolume,
|
||
'avg_risk' => $avgRisk,
|
||
'watchlist_hits' => $watchlistHits,
|
||
'automation_share' => $automationShare,
|
||
];
|
||
}
|
||
|
||
#[Computed]
|
||
public function companies(): Collection
|
||
{
|
||
$query = Company::query()
|
||
->withCount([
|
||
'transactions as alert_count' => fn ($transactions) => $transactions->where('requires_review', true),
|
||
])
|
||
->withSum([
|
||
'transactions as alert_volume' => fn ($transactions) => $transactions->where('requires_review', true),
|
||
], 'amount')
|
||
->withAvg([
|
||
'transactions as alert_risk' => fn ($transactions) => $transactions->where('requires_review', true),
|
||
], 'risk_score')
|
||
->with([
|
||
'transactions' => fn ($transactions) => $transactions->where('requires_review', true)->latest('executed_at')->limit(1),
|
||
])
|
||
->when(
|
||
$this->search !== '',
|
||
function ($builder) {
|
||
$like = '%' . $this->search . '%';
|
||
|
||
$builder->where(function ($inner) use ($like) {
|
||
$inner->where('name', 'like', $like)
|
||
->orWhere('legal_name', 'like', $like)
|
||
->orWhere('ticker', 'like', $like)
|
||
->orWhere('sector', 'like', $like);
|
||
});
|
||
}
|
||
)
|
||
->orderByDesc('alert_volume')
|
||
->orderBy('name')
|
||
->limit(6);
|
||
|
||
return $query->get();
|
||
}
|
||
}; ?>
|
||
|
||
@php
|
||
$overview = $this->overview;
|
||
/** @var \Illuminate\Support\Collection<int, \App\Models\Company> $companies */
|
||
$companies = $this->companies;
|
||
$formatCurrency = static fn (float $value): string => number_format($value, 2, ',', '.') . ' €';
|
||
$formatNumber = static fn ($value): string => number_format((float) $value, 0, ',', '.');
|
||
@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-col gap-6 lg:flex-row lg:items-end lg:justify-between">
|
||
<div class="max-w-2xl space-y-2">
|
||
<flux:heading size="xl">{{ __('Unternehmensauskunft') }}</flux:heading>
|
||
<flux:text>{{ __('Suchen Sie nach Unternehmensinformationen') }}</flux:text>
|
||
<flux:text>
|
||
{{ __('KI-gestützte Einsichten zu DAX-Unternehmen: Screening-Ergebnisse, Risikostapel und alle offenen Alerts auf einen Blick.') }}
|
||
</flux:text>
|
||
</div>
|
||
<div class="w-full max-w-md">
|
||
<flux:input
|
||
wire:model.live.debounce.300ms="search"
|
||
type="search"
|
||
icon="magnifying-glass"
|
||
placeholder="{{ __('Rechtlicher Name, Ticker oder Branche durchsuchen …') }}"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||
<div class="rounded-2xl border border-slate-200/70 bg-slate-50/90 p-5 text-sm dark:border-slate-700/70 dark:bg-slate-800/70">
|
||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-300">{{ __('Unternehmen im Screening') }}</p>
|
||
<p class="mt-2 text-2xl font-semibold text-slate-900 dark:text-white">{{ $formatNumber($overview['companies']) }}</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/10 p-5 text-sm shadow-sm shadow-emerald-900/10">
|
||
<p class="text-xs font-semibold uppercase tracking-wide text-emerald-800 dark:text-emerald-200">{{ __('Offene Alerts') }}</p>
|
||
<p class="mt-2 text-2xl font-semibold text-emerald-900 dark:text-emerald-100">{{ $formatNumber($overview['open_alerts']) }}</p>
|
||
<p class="text-xs text-emerald-700 dark:text-emerald-200">{{ __('Volumen') }} {{ $formatCurrency($overview['open_volume']) }}</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-indigo-400/40 bg-indigo-500/10 p-5 text-sm shadow-sm shadow-indigo-900/10">
|
||
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-800 dark:text-indigo-200">{{ __('Ø Risikoscore offene Fälle') }}</p>
|
||
<p class="mt-2 text-2xl font-semibold text-indigo-900 dark:text-indigo-100">{{ $overview['avg_risk'] }}</p>
|
||
<p class="text-xs text-indigo-700 dark:text-indigo-200">{{ __('Watchlist-Hits') }} {{ $formatNumber($overview['watchlist_hits']) }}</p>
|
||
</div>
|
||
<div class="rounded-2xl border border-zinc-200/70 bg-zinc-50/90 p-5 text-sm dark:border-zinc-700/70 dark:bg-zinc-800/70">
|
||
<p class="text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-300">{{ __('Automationsquote') }}</p>
|
||
<p class="mt-2 text-2xl font-semibold text-zinc-900 dark:text-white">{{ $overview['automation_share'] }}%</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('Analyst:innen übernehmen nur komplexe Spezialfälle') }}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
||
<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-end justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">{{ __('Screening-Portfolio') }}</h2>
|
||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||
{{ __('Die relevantesten Unternehmen nach offenem Alert-Volumen — inklusive Risikokommentar und letztem Trigger.') }}
|
||
</p>
|
||
</div>
|
||
<flux:badge variant="outline">
|
||
{{ $companies->count() }} {{ __('Treffer') }}
|
||
</flux:badge>
|
||
</div>
|
||
|
||
<div class="mt-6 space-y-4">
|
||
@forelse ($companies as $company)
|
||
@php
|
||
$latestAlert = optional($company->transactions->first());
|
||
$alertVolume = (float) ($company->alert_volume ?? 0);
|
||
$alertCount = (int) ($company->alert_count ?? 0);
|
||
$alertRisk = (int) round($company->alert_risk ?? 0);
|
||
$riskBadgeClasses = match (Str::lower($company->kyc_risk_level)) {
|
||
'high', 'hoch' => 'bg-rose-500/10 text-rose-600 dark:text-rose-300 border border-rose-500/20',
|
||
'low', 'niedrig' => 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-300 border border-emerald-500/20',
|
||
default => 'bg-amber-500/10 text-amber-600 dark:text-amber-300 border border-amber-500/20',
|
||
};
|
||
@endphp
|
||
<a href="{{ route('company.transactions', ['company' => $company->id]) }}" wire:navigate class="grid gap-4 rounded-2xl border border-zinc-200/70 bg-zinc-50/70 p-4 text-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-zinc-700/60 dark:bg-zinc-800/60 md:grid-cols-[1.4fr_1fr]">
|
||
<div class="space-y-2">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div>
|
||
<p class="text-base font-semibold text-zinc-900 dark:text-zinc-100">{{ $company->legal_name ?? $company->name }}</p>
|
||
<p class="text-xs uppercase tracking-wide text-zinc-500 dark:text-zinc-400">{{ $company->ticker }} · {{ $company->sector }}</p>
|
||
</div>
|
||
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {{ $riskBadgeClasses }}">
|
||
{{ __('KYC-Risiko') }}: {{ Str::title($company->kyc_risk_level) }}
|
||
</span>
|
||
</div>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $company->summary }}</p>
|
||
<div class="flex flex-wrap gap-3 text-xs text-zinc-500 dark:text-zinc-400">
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-white/60 px-2 py-1 font-medium text-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-200">
|
||
{{ __('Alerts offen') }}: {{ $formatNumber($alertCount) }}
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-white/60 px-2 py-1 font-medium text-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-200">
|
||
{{ __('Volumen') }}: {{ $formatCurrency($alertVolume) }}
|
||
</span>
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-white/60 px-2 py-1 font-medium text-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-200">
|
||
{{ __('Ø Risikoscore') }}: {{ $alertRisk }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-3 rounded-2xl border border-zinc-200/70 bg-white/80 p-4 dark:border-zinc-700/60 dark:bg-zinc-900/70">
|
||
<p class="text-xs font-semibold uppercase tracking-wide text-zinc-400">{{ __('Neuester Alert') }}</p>
|
||
@if ($latestAlert)
|
||
<div class="space-y-1">
|
||
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $latestAlert->counterparty }}</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ $latestAlert->executed_at?->format('d.m.Y H:i') }} · {{ $latestAlert->channel }}</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('Grund') }}: {{ $latestAlert->flagged_reason }}</p>
|
||
</div>
|
||
@else
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('Keine offenen Alerts – Monitoring im grünen Bereich.') }}</p>
|
||
@endif
|
||
<div class="flex flex-wrap gap-2 text-xs text-indigo-600 dark:text-indigo-300">
|
||
<span class="inline-flex items-center gap-1 rounded-full bg-indigo-500/10 px-2 py-1">
|
||
{{ __('Insights') }}
|
||
</span>
|
||
<span>{{ __('Generiere automatisch AIM-Summary und investigatives Casefile.') }}</span>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
@empty
|
||
<div class="rounded-2xl border border-dashed border-zinc-300/70 bg-white/80 p-6 text-center text-sm text-zinc-500 dark:border-zinc-600/60 dark:bg-zinc-900/60 dark:text-zinc-300">
|
||
{{ __('Keine Unternehmen gefunden. Bitte Suchkriterien anpassen oder Seed-Daten neu laden.') }}
|
||
</div>
|
||
@endforelse
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="flex flex-col gap-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">
|
||
<h3 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">{{ __('Proof-of-Concept Checks') }}</h3>
|
||
<ul class="mt-4 space-y-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||
<li class="flex gap-3">
|
||
<span class="mt-0.5 h-2 w-2 rounded-full bg-emerald-500"></span>
|
||
<div>
|
||
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ __('Adverse-Media Analyse in Echtzeit') }}</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('LLM-Agenten extrahieren Risikosignale aus Nachrichtenclustern und verknüpfen sie mit dem Unternehmensnetzwerk.') }}</p>
|
||
</div>
|
||
</li>
|
||
<li class="flex gap-3">
|
||
<span class="mt-0.5 h-2 w-2 rounded-full bg-indigo-500"></span>
|
||
<div>
|
||
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ __('Beneficial Ownership Mapping') }}</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('Regelmäßiger Abgleich von UBO-Graphen gegen Sanktionslisten, PEP-Register und Hochrisikoländer.') }}</p>
|
||
</div>
|
||
</li>
|
||
<li class="flex gap-3">
|
||
<span class="mt-0.5 h-2 w-2 rounded-full bg-amber-500"></span>
|
||
<div>
|
||
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ __('Runbook-Automatisierung') }}</p>
|
||
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ __('Case-Workflows generieren Audit-Trails, Delegationen und Eskalationspfade automatisch.') }}</p>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</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-base font-semibold text-zinc-900 dark:text-zinc-100">{{ __('Sofortmaßnahmen (PoC)') }}</h3>
|
||
<ul class="mt-4 space-y-2 text-sm text-zinc-600 dark:text-zinc-300">
|
||
<li class="flex items-center justify-between gap-2 rounded-2xl bg-zinc-50/80 px-3 py-2 dark:bg-zinc-800/60">
|
||
<span>{{ __('KYC Refresh initiiert') }}</span>
|
||
<span class="text-xs font-semibold text-emerald-600 dark:text-emerald-300">{{ __('automatisiert') }}</span>
|
||
</li>
|
||
<li class="flex items-center justify-between gap-2 rounded-2xl bg-zinc-50/80 px-3 py-2 dark:bg-zinc-800/60">
|
||
<span>{{ __('AI Investigations Summary') }}</span>
|
||
<span class="text-xs font-semibold text-indigo-600 dark:text-indigo-300">{{ __('bereit in 12s') }}</span>
|
||
</li>
|
||
<li class="flex items-center justify-between gap-2 rounded-2xl bg-zinc-50/80 px-3 py-2 dark:bg-zinc-800/60">
|
||
<span>{{ __('Case Handover an 2nd Line') }}</span>
|
||
<span class="text-xs font-semibold text-rose-600 dark:text-rose-300">{{ __('3 Eskalationen') }}</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
</div>
|