Add Transaction View
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Company extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'legal_name',
|
||||
'ticker',
|
||||
'sector',
|
||||
'country',
|
||||
'headquarters',
|
||||
'kyc_risk_level',
|
||||
'summary',
|
||||
];
|
||||
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(Transaction::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Transaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_TRUE_POSITIVE = 'true_positive';
|
||||
public const STATUS_FALSE_POSITIVE = 'false_positive';
|
||||
public const STATUS_CLEARED = 'cleared';
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'reference',
|
||||
'amount',
|
||||
'currency',
|
||||
'counterparty',
|
||||
'counterparty_country',
|
||||
'channel',
|
||||
'executed_at',
|
||||
'risk_score',
|
||||
'status',
|
||||
'requires_review',
|
||||
'flagged_by',
|
||||
'flagged_reason',
|
||||
'signals',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'executed_at' => 'datetime',
|
||||
'requires_review' => 'boolean',
|
||||
'signals' => 'array',
|
||||
];
|
||||
|
||||
public function company(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
public function statusLabel(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
self::STATUS_TRUE_POSITIVE => __('Bestätigter Treffer'),
|
||||
self::STATUS_FALSE_POSITIVE => __('Fehlalarm'),
|
||||
default => __('Freigegeben'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('companies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('legal_name')->nullable();
|
||||
$table->string('ticker')->nullable();
|
||||
$table->string('sector')->nullable();
|
||||
$table->string('country', 2)->default('DE');
|
||||
$table->string('headquarters')->nullable();
|
||||
$table->string('kyc_risk_level')->default('medium');
|
||||
$table->text('summary')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('companies');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('reference')->unique();
|
||||
$table->decimal('amount', 16, 2);
|
||||
$table->string('currency', 3)->default('EUR');
|
||||
$table->string('counterparty');
|
||||
$table->string('counterparty_country', 2)->nullable();
|
||||
$table->string('channel')->nullable();
|
||||
$table->dateTime('executed_at');
|
||||
$table->unsignedTinyInteger('risk_score')->default(0);
|
||||
$table->string('status', 32)->index();
|
||||
$table->boolean('requires_review')->default(true);
|
||||
$table->string('flagged_by')->nullable();
|
||||
$table->text('flagged_reason')->nullable();
|
||||
$table->json('signals')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('transactions');
|
||||
}
|
||||
};
|
||||
@@ -15,9 +15,11 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
User::factory()->withoutTwoFactor()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
$this->call(DaxCompanySeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Transaction;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DaxCompanySeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$companies = [
|
||||
['name' => 'Adidas AG', 'legal_name' => 'adidas Aktiengesellschaft', 'ticker' => 'ADS', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Herzogenaurach', 'summary' => 'Globaler Sportartikelhersteller mit Schwerpunkt auf Schuhen, Bekleidung und digitalen Services.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Airbus SE', 'legal_name' => 'Airbus Societas Europaea', 'ticker' => 'AIR', 'sector' => 'Industrie', 'headquarters' => 'Leiden', 'country' => 'NL', 'summary' => 'Luft- und Raumfahrtkonzern mit zivilen, militärischen und Raumfahrtaktivitäten.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Allianz SE', 'legal_name' => 'Allianz Societas Europaea', 'ticker' => 'ALV', 'sector' => 'Finanzen', 'headquarters' => 'München', 'summary' => 'Versicherung und Vermögensverwaltung mit globalem Risikoportfolio.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'BASF SE', 'legal_name' => 'BASF Societas Europaea', 'ticker' => 'BAS', 'sector' => 'Grundstoffe', 'headquarters' => 'Ludwigshafen', 'summary' => 'Chemiekonzern mit Aktivitäten in Materialien, Industrial Solutions und Agricultural Solutions.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Bayer AG', 'legal_name' => 'Bayer Aktiengesellschaft', 'ticker' => 'BAYN', 'sector' => 'Gesundheit', 'headquarters' => 'Leverkusen', 'summary' => 'Life-Science-Unternehmen mit den Segmenten Pharmaceuticals, Consumer Health und Crop Science.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Beiersdorf AG', 'legal_name' => 'Beiersdorf Aktiengesellschaft', 'ticker' => 'BEI', 'sector' => 'Konsumgüter (Defensiv)', 'headquarters' => 'Hamburg', 'summary' => 'Hautpflegeexperte mit Marken wie Nivea, Eucerin und Tesa.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'BMW AG', 'legal_name' => 'Bayerische Motoren Werke Aktiengesellschaft', 'ticker' => 'BMW', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'München', 'summary' => 'Hersteller von Premiumfahrzeugen mit den Marken BMW, MINI und Rolls-Royce.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Brenntag SE', 'legal_name' => 'Brenntag Societas Europaea', 'ticker' => 'BNR', 'sector' => 'Industrie', 'headquarters' => 'Essen', 'summary' => 'Weltweit führender Chemie- und Inhaltsstoffdistributor mit Value-Added Services.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Commerzbank AG', 'legal_name' => 'Commerzbank Aktiengesellschaft', 'ticker' => 'CBK', 'sector' => 'Finanzen', 'headquarters' => 'Frankfurt am Main', 'summary' => 'Universalbank mit Fokus auf Mittelstands- und Firmenkunden.', 'kyc_risk_level' => 'high'],
|
||||
['name' => 'Daimler Truck AG', 'legal_name' => 'Daimler Truck Holding Aktiengesellschaft', 'ticker' => 'DTG', 'sector' => 'Industrie', 'headquarters' => 'Leinfelden-Echterdingen', 'summary' => 'Hersteller von schweren Lkw und Bussen mit globalem Produktionsnetzwerk.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Deutsche Bank AG', 'legal_name' => 'Deutsche Bank Aktiengesellschaft', 'ticker' => 'DBK', 'sector' => 'Finanzen', 'headquarters' => 'Frankfurt am Main', 'summary' => 'Globales Investment- und Firmenkundengeschäft mit Vermögensverwaltung.', 'kyc_risk_level' => 'high'],
|
||||
['name' => 'Deutsche Börse AG', 'legal_name' => 'Deutsche Börse Aktiengesellschaft', 'ticker' => 'DB1', 'sector' => 'Finanzen', 'headquarters' => 'Eschborn', 'summary' => 'Marktinfrastrukturbetreiber für Handel, Clearing und Settlement.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'Deutsche Post AG', 'legal_name' => 'Deutsche Post Aktiengesellschaft', 'ticker' => 'DPW', 'sector' => 'Industrie', 'headquarters' => 'Bonn', 'summary' => 'Logistikkonzern der Deutsche Post DHL Group mit globaler Supply-Chain-Plattform.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Deutsche Telekom AG', 'legal_name' => 'Deutsche Telekom Aktiengesellschaft', 'ticker' => 'DTE', 'sector' => 'Kommunikation', 'headquarters' => 'Bonn', 'summary' => 'Integrierter Telekommunikationsanbieter mit Fokus auf Europa und USA.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'E.ON SE', 'legal_name' => 'E.ON Societas Europaea', 'ticker' => 'EOAN', 'sector' => 'Versorger', 'headquarters' => 'Essen', 'summary' => 'Energieversorger mit Schwerpunkt auf Energienetzen und Kundenlösungen.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Fresenius SE & Co. KGaA', 'legal_name' => 'Fresenius SE & Co. KGaA', 'ticker' => 'FRE', 'sector' => 'Gesundheit', 'headquarters' => 'Bad Homburg vor der Höhe', 'summary' => 'Gesundheitskonzern mit Kliniken, Medizintechnik und Dienstleistungen.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Hannover Rück SE', 'legal_name' => 'Hannover Rück Societas Europaea', 'ticker' => 'HNR1', 'sector' => 'Finanzen', 'headquarters' => 'Hannover', 'summary' => 'Rückversicherer mit Sparten in Schaden/Unfall und Leben/Kranken.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'Heidelberg Materials AG', 'legal_name' => 'Heidelberg Materials Aktiengesellschaft', 'ticker' => 'HEI', 'sector' => 'Grundstoffe', 'headquarters' => 'Heidelberg', 'summary' => 'Baustoffhersteller mit Schwerpunkt auf Zement, Zuschlagstoffen und Transportbeton.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Henkel AG & Co. KGaA', 'legal_name' => 'Henkel AG & Co. Kommanditgesellschaft auf Aktien', 'ticker' => 'HEN3', 'sector' => 'Konsumgüter (Defensiv)', 'headquarters' => 'Düsseldorf', 'summary' => 'Hersteller von Konsumgütern und Klebstofftechnologien mit globaler Präsenz.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'Infineon Technologies AG', 'legal_name' => 'Infineon Technologies Aktiengesellschaft', 'ticker' => 'IFX', 'sector' => 'Informationstechnologie', 'headquarters' => 'Neubiberg', 'summary' => 'Halbleiterhersteller für Mobilität, Energieeffizienz und Sicherheit.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Mercedes-Benz Group AG', 'legal_name' => 'Mercedes-Benz Group Aktiengesellschaft', 'ticker' => 'MBG', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Stuttgart', 'summary' => 'Hersteller von Premium-Pkw und Vans mit starkem E-Mobility-Fokus.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Porsche SE', 'legal_name' => 'Porsche Automobil Holding Societas Europaea', 'ticker' => 'PAH3', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Stuttgart', 'summary' => 'Strategische Beteiligungsgesellschaft mit Schwerpunkt Automobil- und Mobilitätssektor.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'PUMA SE', 'legal_name' => 'PUMA Societas Europaea', 'ticker' => 'PUM', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Herzogenaurach', 'summary' => 'Sportartikelunternehmen mit Performance- und Lifestyle-Sortiment.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Qiagen N.V.', 'legal_name' => 'Qiagen Naamloze Vennootschap', 'ticker' => 'QIA', 'sector' => 'Gesundheit', 'headquarters' => 'Venlo', 'country' => 'NL', 'summary' => 'Spezialist für molekulare Diagnostik und Probenaufbereitung.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'RWE AG', 'legal_name' => 'RWE Aktiengesellschaft', 'ticker' => 'RWE', 'sector' => 'Versorger', 'headquarters' => 'Essen', 'summary' => 'Energieunternehmen in Transformation hin zu erneuerbaren Energien.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'SAP SE', 'legal_name' => 'SAP Societas Europaea', 'ticker' => 'SAP', 'sector' => 'Informationstechnologie', 'headquarters' => 'Walldorf', 'summary' => 'Anbieter von Unternehmenssoftware für ERP und Cloud-Lösungen.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'Siemens AG', 'legal_name' => 'Siemens Aktiengesellschaft', 'ticker' => 'SIE', 'sector' => 'Industrie', 'headquarters' => 'München', 'summary' => 'Technologiekonzern mit Fokus auf Automatisierung, Digitalisierung und Infrastruktur.', 'kyc_risk_level' => 'low'],
|
||||
['name' => 'Siemens Healthineers AG', 'legal_name' => 'Siemens Healthineers Aktiengesellschaft', 'ticker' => 'SHL', 'sector' => 'Gesundheit', 'headquarters' => 'Erlangen', 'summary' => 'Medizintechnikanbieter für Bildgebung, Diagnostik und Therapie.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Symrise AG', 'legal_name' => 'Symrise Aktiengesellschaft', 'ticker' => 'SY1', 'sector' => 'Grundstoffe', 'headquarters' => 'Holzminden', 'summary' => 'Hersteller von Duft-, Geschmacks- und Nährstofflösungen.', 'kyc_risk_level' => 'medium'],
|
||||
['name' => 'Volkswagen AG', 'legal_name' => 'Volkswagen Aktiengesellschaft', 'ticker' => 'VOW', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Wolfsburg', 'summary' => 'Automobilkonzern mit Markenportfolio von Volumen bis Luxus.', 'kyc_risk_level' => 'high'],
|
||||
];
|
||||
|
||||
$channels = [
|
||||
'SEPA Credit Transfer',
|
||||
'International Wire',
|
||||
'SWIFT MT103',
|
||||
'Cross-Border ACH',
|
||||
'High-Value Clearing',
|
||||
'Trade Finance Settlement',
|
||||
];
|
||||
|
||||
$flaggedReasons = [
|
||||
'Counterparty appeared on consolidated sanctions screening list.',
|
||||
'Transaction amount exceeded historical average by 300%.',
|
||||
'Unusual payment corridor for the industry segment.',
|
||||
'Multiple layered transfers flagged by transaction monitoring.',
|
||||
'Beneficiary bank located in heightened risk jurisdiction.',
|
||||
'Rapid movement of funds through nested accounts.',
|
||||
'Adverse media match with counterparty executive.',
|
||||
];
|
||||
|
||||
$analysts = ['AML Radar', 'Sanctions Sentinel', 'Behavioral Analytics', 'Network Intelligence', 'Adverse Media Scan'];
|
||||
|
||||
$signalCatalog = [
|
||||
['type' => 'Watchlist Match', 'value' => 'EU Consolidated List'],
|
||||
['type' => 'PEP Screening', 'value' => 'Tier 2 Politically Exposed Person'],
|
||||
['type' => 'Geo Risk', 'value' => 'High-risk jurisdiction: TR'],
|
||||
['type' => 'Velocity', 'value' => '6 linked payments within 24h'],
|
||||
['type' => 'Structuring', 'value' => 'Smurfing pattern detected'],
|
||||
['type' => 'Negative News', 'value' => 'Recent fraud investigation reported'],
|
||||
['type' => 'Beneficial Ownership', 'value' => 'Opaque corporate structure'],
|
||||
['type' => 'Industry Deviation', 'value' => 'Trade value outside peer median by 210%'],
|
||||
];
|
||||
|
||||
$statusPool = [
|
||||
Transaction::STATUS_TRUE_POSITIVE,
|
||||
Transaction::STATUS_FALSE_POSITIVE,
|
||||
Transaction::STATUS_FALSE_POSITIVE,
|
||||
Transaction::STATUS_CLEARED,
|
||||
Transaction::STATUS_CLEARED,
|
||||
Transaction::STATUS_CLEARED,
|
||||
];
|
||||
|
||||
$faker = fake('en_GB');
|
||||
$sequence = 1;
|
||||
|
||||
DB::transaction(function () use (
|
||||
$companies,
|
||||
$channels,
|
||||
$flaggedReasons,
|
||||
$analysts,
|
||||
$signalCatalog,
|
||||
$statusPool,
|
||||
$faker,
|
||||
&$sequence
|
||||
) {
|
||||
Transaction::query()->delete();
|
||||
Company::query()->delete();
|
||||
|
||||
foreach ($companies as $companyData) {
|
||||
$company = Company::create(array_merge(
|
||||
[
|
||||
'country' => $companyData['country'] ?? 'DE',
|
||||
],
|
||||
$companyData
|
||||
));
|
||||
|
||||
$transactionTotal = random_int(4, 7);
|
||||
|
||||
for ($i = 0; $i < $transactionTotal; $i++) {
|
||||
$status = Arr::random($statusPool);
|
||||
$riskScore = match ($status) {
|
||||
Transaction::STATUS_TRUE_POSITIVE => random_int(82, 95),
|
||||
Transaction::STATUS_FALSE_POSITIVE => random_int(48, 65),
|
||||
default => random_int(12, 28),
|
||||
};
|
||||
|
||||
$executedAt = Carbon::now()
|
||||
->subDays(random_int(1, 45))
|
||||
->setTime(random_int(7, 19), random_int(0, 59), random_int(0, 59));
|
||||
|
||||
$company->transactions()->create([
|
||||
'reference' => 'TRX-' . str_pad((string) $sequence++, 6, '0', STR_PAD_LEFT),
|
||||
'amount' => $faker->randomFloat(2, 8_500, 4_500_000),
|
||||
'currency' => Arr::random(['EUR', 'USD', 'GBP']),
|
||||
'counterparty' => $faker->company(),
|
||||
'counterparty_country' => Arr::random(['DE', 'DE', 'CH', 'AT', 'NL', 'GB', 'LU', 'SG']),
|
||||
'channel' => Arr::random($channels),
|
||||
'executed_at' => $executedAt,
|
||||
'risk_score' => $riskScore,
|
||||
'status' => $status,
|
||||
'requires_review' => true,
|
||||
'flagged_by' => Arr::random($analysts),
|
||||
'flagged_reason' => Arr::random($flaggedReasons),
|
||||
'signals' => array_values(Arr::random(
|
||||
$signalCatalog,
|
||||
random_int(1, min(3, count($signalCatalog)))
|
||||
)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Platform')" class="grid">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Übersicht') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="magnifying-glass" :href="route('company-search')" :current="request()->routeIs('company-search')" wire:navigate>{{ __('Unternehmensauskunft') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="shield-check" :href="route('transaction-review')" :current="request()->routeIs('transaction-review')" wire:navigate>{{ __('Transaktionsprüfung') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Einstellungen') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
@@ -70,7 +71,7 @@
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full" data-test="logout-button">
|
||||
{{ __('Log Out') }}
|
||||
{{ __('Abmelden') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
@@ -112,7 +113,7 @@
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>{{ __('Einstellungen') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
@@ -120,7 +121,7 @@
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full" data-test="logout-button">
|
||||
{{ __('Log Out') }}
|
||||
{{ __('Abmelden') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu>
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
new #[Layout('components.layouts.app')] class extends Component {
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $status = 'all';
|
||||
public ?int $selectedTransactionId = null;
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'status' => ['except' => 'all'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->selectedTransactionId = Transaction::query()
|
||||
->orderByDesc('risk_score')
|
||||
->orderByDesc('executed_at')
|
||||
->value('id');
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'title' => __('Transaktionsprüfung'),
|
||||
];
|
||||
}
|
||||
|
||||
public function selectTransaction(int $transactionId): void
|
||||
{
|
||||
$this->selectedTransactionId = $transactionId;
|
||||
}
|
||||
|
||||
public function updatingSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->refreshSelection();
|
||||
}
|
||||
|
||||
public function updatingStatus(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedStatus(): void
|
||||
{
|
||||
$this->refreshSelection();
|
||||
}
|
||||
|
||||
public function updatedPage(): void
|
||||
{
|
||||
$this->refreshSelection();
|
||||
}
|
||||
|
||||
protected function refreshSelection(): void
|
||||
{
|
||||
$first = $this->transactions->first();
|
||||
$this->selectedTransactionId = $first?->id;
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function statusOptions(): array
|
||||
{
|
||||
return [
|
||||
'all' => 'Alle Prüfaufträge',
|
||||
Transaction::STATUS_TRUE_POSITIVE => 'Bestätigte Treffer',
|
||||
Transaction::STATUS_FALSE_POSITIVE => 'Fehlalarme',
|
||||
Transaction::STATUS_CLEARED => 'Freigegebene Vorgänge',
|
||||
];
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function transactions(): LengthAwarePaginator
|
||||
{
|
||||
return $this->filteredQuery()
|
||||
->with('company')
|
||||
->orderByDesc('requires_review')
|
||||
->orderByDesc('risk_score')
|
||||
->orderByDesc('executed_at')
|
||||
->paginate(12);
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function metrics(): array
|
||||
{
|
||||
$baseQuery = $this->filteredQuery();
|
||||
|
||||
/** @var Collection<int, array{count:int, amount:float}> $grouped */
|
||||
$grouped = (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,
|
||||
],
|
||||
]);
|
||||
|
||||
return [
|
||||
'by_status' => $grouped->toArray(),
|
||||
'total' => [
|
||||
'count' => $grouped->sum('count'),
|
||||
'amount' => $grouped->sum('amount'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function selectedTransaction(): ?Transaction
|
||||
{
|
||||
$transaction = $this->transactions->firstWhere('id', $this->selectedTransactionId);
|
||||
|
||||
if (! $transaction && $this->selectedTransactionId) {
|
||||
$transaction = Transaction::query()
|
||||
->with('company')
|
||||
->find($this->selectedTransactionId);
|
||||
}
|
||||
|
||||
return $transaction ?? $this->transactions->first();
|
||||
}
|
||||
|
||||
protected function filteredQuery(): Builder
|
||||
{
|
||||
$search = trim($this->search);
|
||||
|
||||
return Transaction::query()
|
||||
->when($this->status !== 'all', fn (Builder $query) => $query->where('status', $this->status))
|
||||
->when($search !== '', function (Builder $query) use ($search) {
|
||||
$like = '%' . $search . '%';
|
||||
|
||||
$query->where(function (Builder $inner) use ($like) {
|
||||
$inner->where('reference', 'like', $like)
|
||||
->orWhere('counterparty', 'like', $like)
|
||||
->orWhereHas('company', function (Builder $company) use ($like) {
|
||||
$company->where('name', 'like', $like)
|
||||
->orWhere('legal_name', 'like', $like)
|
||||
->orWhere('ticker', 'like', $like);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}; ?>
|
||||
|
||||
@php
|
||||
/** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator $transactions */
|
||||
$transactions = $this->transactions;
|
||||
/** @var \App\Models\Transaction|null $selected */
|
||||
$selected = $this->selectedTransaction;
|
||||
$metrics = $this->metrics;
|
||||
$byStatus = collect($metrics['by_status']);
|
||||
$resolveMetrics = static fn (string $status): array => $byStatus->get($status, ['count' => 0, 'amount' => 0.0]);
|
||||
$formatAmount = static fn (float $value): string => number_format($value, 2, ',', '.') . ' €';
|
||||
|
||||
$confirmed = $resolveMetrics(Transaction::STATUS_TRUE_POSITIVE);
|
||||
$falseAlarm = $resolveMetrics(Transaction::STATUS_FALSE_POSITIVE);
|
||||
$cleared = $resolveMetrics(Transaction::STATUS_CLEARED);
|
||||
$total = $metrics['total'];
|
||||
|
||||
$statusStyles = [
|
||||
Transaction::STATUS_TRUE_POSITIVE => 'bg-rose-500/15 text-rose-700 dark:text-rose-300 border border-rose-500/30',
|
||||
Transaction::STATUS_FALSE_POSITIVE => 'bg-amber-500/15 text-amber-700 dark:text-amber-300 border border-amber-500/30',
|
||||
Transaction::STATUS_CLEARED => 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border border-emerald-500/30',
|
||||
];
|
||||
|
||||
$statusCopy = [
|
||||
Transaction::STATUS_TRUE_POSITIVE => 'Zur Sonderprüfung eskalieren und Ermittlungsakte anlegen.',
|
||||
Transaction::STATUS_FALSE_POSITIVE => 'Anomalie begründen, Dokumentation ergänzen und Vorgang schließen.',
|
||||
Transaction::STATUS_CLEARED => 'Review-Ergebnis dokumentieren und Kontrollnachweis archivieren.',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="rounded-3xl border border-slate-200/60 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 p-8 text-white shadow-xl dark:border-slate-700/60 dark:from-slate-950 dark:via-slate-900 dark:to-slate-950">
|
||||
<div class="flex flex-wrap items-end justify-between gap-6">
|
||||
<div class="space-y-2 max-w-xl">
|
||||
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">Finanzkriminalitäts-Workbench</h1>
|
||||
<p class="text-sm text-slate-300 sm:text-base">
|
||||
Überwachen Sie verdächtige Transaktionen im DAX-Portfolio und steuern Sie das manuelle Review nahtlos.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 text-sm sm:text-base md:grid-cols-4">
|
||||
<div class="rounded-2xl border border-white/10 bg-white/10 p-4 text-center backdrop-blur">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-slate-200">Gesamtfälle</p>
|
||||
<p class="text-2xl font-semibold text-white">{{ $total['count'] }}</p>
|
||||
<p class="mt-1 text-xs text-slate-300">Volumen: {{ $formatAmount($total['amount']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-rose-400/40 bg-rose-500/10 p-4 text-center backdrop-blur">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-rose-100">Bestätigte Treffer</p>
|
||||
<p class="text-2xl font-semibold text-rose-200">{{ $confirmed['count'] }}</p>
|
||||
<p class="mt-1 text-xs text-rose-100">Volumen: {{ $formatAmount($confirmed['amount']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-amber-400/40 bg-amber-500/10 p-4 text-center backdrop-blur">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-amber-100">Fehlalarme</p>
|
||||
<p class="text-2xl font-semibold text-amber-100">{{ $falseAlarm['count'] }}</p>
|
||||
<p class="mt-1 text-xs text-amber-100">Volumen: {{ $formatAmount($falseAlarm['amount']) }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-emerald-400/40 bg-emerald-500/10 p-4 text-center backdrop-blur">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-emerald-100">Freigegeben</p>
|
||||
<p class="text-2xl font-semibold text-emerald-100">{{ $cleared['count'] }}</p>
|
||||
<p class="mt-1 text-xs text-emerald-100">Volumen: {{ $formatAmount($cleared['amount']) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-xl font-semibold text-zinc-900 dark:text-zinc-100">Aktive Alert-Warteschlange</h2>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Alle untenstehenden Meldungen benötigen eine finale Analystenentscheidung.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex w-full flex-col gap-3 sm:w-auto sm:flex-row">
|
||||
<flux:select wire:model.live="status" class="w-full sm:w-56">
|
||||
@foreach ($this->statusOptions as $value => $label)
|
||||
<option value="{{ $value }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input
|
||||
wire:model.debounce.400ms="search"
|
||||
type="search"
|
||||
placeholder="Unternehmen, Gegenpartei oder Referenz suchen …"
|
||||
class="w-full sm:w-80"
|
||||
icon="magnifying-glass" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,2.1fr)_minmax(0,1fr)]">
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200/70 bg-white/95 shadow-sm backdrop-blur dark:border-zinc-700/70 dark:bg-zinc-900/90">
|
||||
<div class="hidden border-b border-zinc-200/70 bg-zinc-50/80 px-6 py-3 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:border-zinc-700/60 dark:bg-zinc-900/70 dark:text-zinc-400 sm:grid sm:grid-cols-[1.6fr_1fr_0.7fr]">
|
||||
<span>Unternehmen & Gegenpartei</span>
|
||||
<span>Kanal & Referenz</span>
|
||||
<span class="text-right">Betrag</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-zinc-200/60 dark:divide-zinc-700/70">
|
||||
@forelse ($transactions as $transaction)
|
||||
@php
|
||||
$selectedState = $transaction->id === $this->selectedTransactionId;
|
||||
@endphp
|
||||
<button
|
||||
wire:click="selectTransaction({{ $transaction->id }})"
|
||||
type="button"
|
||||
class="flex w-full flex-col gap-3 px-4 py-4 text-left transition sm:grid sm:grid-cols-[1.6fr_1fr_0.7fr]"
|
||||
@class([
|
||||
'bg-indigo-500/10 dark:bg-indigo-500/20 border-s-4 border-indigo-500/80',
|
||||
'hover:bg-zinc-50/80 dark:hover:bg-zinc-800/70 border-s-transparent' => ! $selectedState,
|
||||
])
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{{ $transaction->company->legal_name }}</p>
|
||||
<p class="text-xs uppercase tracking-wide text-zinc-400">{{ $transaction->company->name }} · {{ $transaction->company->sector }}</p>
|
||||
</div>
|
||||
<span class="inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold {{ $statusStyles[$transaction->status] ?? 'bg-zinc-500/10 text-zinc-600 dark:text-zinc-300 border border-zinc-500/20' }}">
|
||||
{{ $transaction->statusLabel() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<span class="font-medium text-zinc-700 dark:text-zinc-200">{{ $transaction->counterparty }}</span>
|
||||
<span>•</span>
|
||||
<span>{{ $transaction->counterparty_country }}</span>
|
||||
<span>•</span>
|
||||
<span>{{ $transaction->executed_at?->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $transaction->channel }}</p>
|
||||
<p class="font-mono text-xs text-zinc-400">{{ $transaction->reference }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end justify-between gap-2">
|
||||
<p class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ number_format($transaction->amount, 2, ',', '.') }} {{ $transaction->currency }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span class="rounded-md bg-slate-100/60 px-2 py-1 font-semibold text-slate-700 dark:bg-slate-900/60 dark:text-slate-200">
|
||||
Risiko {{ $transaction->risk_score }}
|
||||
</span>
|
||||
<span>{{ $statusCopy[$transaction->status] ?? '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@empty
|
||||
<div class="px-6 py-16 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Keine Transaktionen für die aktuellen Filterkriterien gefunden. Bitte Seed-Daten neu laden oder Filter anpassen.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@if ($transactions->hasPages())
|
||||
<div class="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="space-y-4">
|
||||
@if ($selected)
|
||||
<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-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">{{ $selected->company->legal_name }}</h3>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ $selected->company->summary }}</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<span class="inline-flex items-center rounded-full bg-slate-900/90 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white dark:bg-slate-200/20">
|
||||
Prüfung erforderlich
|
||||
</span>
|
||||
<span class="text-3xl font-semibold text-slate-900 dark:text-white">
|
||||
{{ number_format($selected->amount, 2, ',', '.') }} {{ $selected->currency }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<span class="rounded-md bg-slate-900/10 px-2 py-1 font-semibold text-slate-700 dark:bg-slate-200/10 dark:text-slate-200">
|
||||
Risikoscore {{ $selected->risk_score }}
|
||||
</span>
|
||||
<span>{{ $selected->executed_at?->format('d.m.Y, H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="mt-6 grid grid-cols-1 gap-4 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<div class="rounded-2xl bg-zinc-50/80 p-4 dark:bg-zinc-800/80">
|
||||
<dt class="text-xs uppercase tracking-wide text-zinc-400">Gegenpartei</dt>
|
||||
<dd class="mt-1 font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->counterparty }}</dd>
|
||||
<dd class="text-xs text-zinc-500">{{ $selected->counterparty_country }}</dd>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-zinc-50/80 p-4 dark:bg-zinc-800/80">
|
||||
<dt class="text-xs uppercase tracking-wide text-zinc-400">Zahlungskanal & Referenz</dt>
|
||||
<dd class="mt-1 font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->channel }}</dd>
|
||||
<dd class="font-mono text-xs text-zinc-500">{{ $selected->reference }}</dd>
|
||||
</div>
|
||||
<div class="rounded-2xl bg-zinc-50/80 p-4 dark:bg-zinc-800/80">
|
||||
<dt class="text-xs uppercase tracking-wide text-zinc-400">Flagging-Engine</dt>
|
||||
<dd class="mt-1 font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->flagged_by }}</dd>
|
||||
<dd class="text-xs text-zinc-500">{{ $selected->flagged_reason }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@if (! empty($selected->signals))
|
||||
<div class="mt-6 space-y-2">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wide text-zinc-400">Detektionssignale</h4>
|
||||
<div class="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/60 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-6 rounded-2xl border border-indigo-200/60 bg-indigo-500/10 p-4 text-sm text-indigo-900 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-100">
|
||||
<p class="font-medium">Empfohlene Maßnahme</p>
|
||||
<p class="mt-1 text-sm leading-relaxed">
|
||||
{{ $statusCopy[$selected->status] ?? 'Review abschließen und vollständigen Audit-Trail dokumentieren.' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl border border-zinc-200/60 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">Unternehmensprofil</h4>
|
||||
<ul class="mt-4 space-y-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Rechtlicher Name</span>
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->legal_name }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Ticker</span>
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->ticker }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Sektor</span>
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->sector }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Hauptsitz</span>
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->headquarters }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>Land</span>
|
||||
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ $selected->company->country }}</span>
|
||||
</li>
|
||||
<li class="flex items-center justify-between">
|
||||
<span>KYC-Risikostufe</span>
|
||||
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-1 text-xs font-semibold uppercase tracking-wide text-slate-700 dark:bg-slate-800 dark:text-slate-200">
|
||||
{{ Str::title($selected->company->kyc_risk_level) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-3xl border border-dashed border-zinc-300/70 bg-white/80 p-6 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 einen Alert aus, um den vollständigen Prüfkontext einzusehen.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,6 +35,10 @@ Volt::route('company-search', 'company-search')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('company-search');
|
||||
|
||||
Volt::route('transaction-review', 'transaction-review')
|
||||
->middleware(['auth', 'verified'])
|
||||
->name('transaction-review');
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::redirect('settings', 'settings/profile');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user