Files
AFC-Demo/resources/views/livewire/company-search.blade.php
T

255 lines
16 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\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>