feat(asset_detail): modular fundamentals sections, executive salaries and logo resolution
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/widgets/glass_container.dart';
|
||||
import '../../../../core/widgets/status_badge.dart';
|
||||
import '../../models/fundamental_data_model.dart';
|
||||
|
||||
class AnalystPriceTargetCard extends StatelessWidget {
|
||||
final FundamentalDataModel data;
|
||||
final String currencySymbol;
|
||||
|
||||
const AnalystPriceTargetCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.currencySymbol = '\$',
|
||||
});
|
||||
|
||||
String _fmtCurrency(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
return '$currencySymbol${val.toStringAsFixed(2)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rating = data.consensusRating ?? 'N/A';
|
||||
final targetMean = data.priceTargetMean;
|
||||
final targetLow = data.priceTargetLow;
|
||||
final targetHigh = data.priceTargetHigh;
|
||||
|
||||
return GlassContainer(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.trending_up, color: AppTheme.primaryEmerald, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Analysten-Konsens & Kursziele',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatusBadge(label: rating.toUpperCase(), color: AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildTargetStat('Mindestkursziel', _fmtCurrency(targetLow), AppTheme.accentRed),
|
||||
_buildTargetStat('Konsens-Ziel (Durchschnitt)', _fmtCurrency(targetMean), AppTheme.primaryEmerald),
|
||||
_buildTargetStat('Höchstkursziel', _fmtCurrency(targetHigh), AppTheme.accentCyan),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTargetStat(String title, String val, Color col) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(title, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
const SizedBox(height: 4),
|
||||
Text(val, style: TextStyle(color: col, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/widgets/glass_container.dart';
|
||||
import '../../models/fundamental_data_model.dart';
|
||||
|
||||
class CompanyProfileSection extends StatelessWidget {
|
||||
final FundamentalDataModel data;
|
||||
final String currencySymbol;
|
||||
|
||||
const CompanyProfileSection({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.currencySymbol = '\$',
|
||||
});
|
||||
|
||||
String _fmtCompensation(double? val) {
|
||||
if (val == null || val <= 0) return '---';
|
||||
if (val >= 1e6) return '$currencySymbol${(val / 1e6).toStringAsFixed(2)}M';
|
||||
if (val >= 1e3) return '$currencySymbol${(val / 1e3).toStringAsFixed(0)}K';
|
||||
return '$currencySymbol${val.toStringAsFixed(0)}';
|
||||
}
|
||||
|
||||
String _formatExecutivePayment(CompanyExecutiveModel exec) {
|
||||
if (exec.compensation != null && exec.compensation! > 0) {
|
||||
return _fmtCompensation(exec.compensation);
|
||||
}
|
||||
if (exec.payment != null && exec.payment!.isNotEmpty) {
|
||||
final p = exec.payment!.trim();
|
||||
if (p.startsWith(currencySymbol) || p.startsWith('€') || p.startsWith(r'$')) {
|
||||
return p;
|
||||
}
|
||||
final numeric = double.tryParse(p);
|
||||
if (numeric != null && numeric > 0) {
|
||||
return _fmtCompensation(numeric);
|
||||
}
|
||||
return '$currencySymbol$p';
|
||||
}
|
||||
return '---';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GlassContainer(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (data.sector != null || data.industry != null || data.country != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
if (data.sector != null) ...[
|
||||
_buildProfileBadge(data.sector!, Icons.category_outlined),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (data.country != null)
|
||||
_buildProfileBadge(data.country!, Icons.place_outlined),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Text(
|
||||
data.businessSummary != null && data.businessSummary!.isNotEmpty
|
||||
? data.businessSummary!
|
||||
: 'Keine Beschreibung für dieses Asset verfügbar.',
|
||||
style: const TextStyle(color: Colors.white70, height: 1.5, fontSize: 13),
|
||||
),
|
||||
if (data.employees != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.people_outline, size: 16, color: AppTheme.textMuted),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Vollzeitbeschäftigte: ${data.employees}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (data.executives.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Führungskräfte & Vorstand',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GlassContainer(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < data.executives.length; i++) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
data.executives[i].name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
data.executives[i].title,
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final payStr = _formatExecutivePayment(data.executives[i]);
|
||||
if (payStr == '---') return const SizedBox.shrink();
|
||||
return Text(
|
||||
payStr,
|
||||
style: TextStyle(color: AppTheme.primaryEmerald, fontWeight: FontWeight.bold, fontSize: 12),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (i < data.executives.length - 1) const Divider(color: Colors.white10, height: 1),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileBadge(String text, IconData icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: AppTheme.primaryEmerald),
|
||||
const SizedBox(width: 6),
|
||||
Text(text, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/widgets/glass_container.dart';
|
||||
import '../../models/fundamental_data_model.dart';
|
||||
import '../../utils/metric_explanations.dart';
|
||||
|
||||
class _MetricRowItem {
|
||||
final String label;
|
||||
final String value;
|
||||
const _MetricRowItem(this.label, this.value);
|
||||
}
|
||||
|
||||
class FundamentalCategoryPanels extends StatelessWidget {
|
||||
final FundamentalDataModel data;
|
||||
final String currencySymbol;
|
||||
final String currencyCode;
|
||||
|
||||
const FundamentalCategoryPanels({
|
||||
super.key,
|
||||
required this.data,
|
||||
this.currencySymbol = '\$',
|
||||
this.currencyCode = 'USD',
|
||||
});
|
||||
|
||||
String _formatNumber(double? number) {
|
||||
if (number == null) return 'N/A';
|
||||
final abs = number.abs();
|
||||
final sign = number < 0 ? '-' : '';
|
||||
if (abs >= 1e12) return '$sign$currencySymbol${(abs / 1e12).toStringAsFixed(2)} Tsd. Mrd. $currencyCode';
|
||||
if (abs >= 1e9) return '$sign$currencySymbol${(abs / 1e9).toStringAsFixed(2)} Mrd. $currencyCode';
|
||||
if (abs >= 1e6) return '$sign$currencySymbol${(abs / 1e6).toStringAsFixed(2)} Mio. $currencyCode';
|
||||
return '$sign$currencySymbol${NumberFormat("#,##0.00", "de_DE").format(abs)} $currencyCode';
|
||||
}
|
||||
|
||||
String _fmtCurrency(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
return '$currencySymbol${val.toStringAsFixed(2)}';
|
||||
}
|
||||
|
||||
String _fmtMultiple(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
return '${val.toStringAsFixed(2)}x';
|
||||
}
|
||||
|
||||
String _fmtPercent(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
final p = (val.abs() <= 1.0 && val != 0.0) ? val * 100.0 : val;
|
||||
return '${p.toStringAsFixed(2)}%';
|
||||
}
|
||||
|
||||
String _fmtDebtToEquity(double? val) {
|
||||
if (val == null) return 'N/A';
|
||||
final p = val > 10.0 ? val : val * 100.0;
|
||||
return '${p.toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
String _fmtDate(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return 'N/A';
|
||||
final dt = DateTime.tryParse(raw);
|
||||
if (dt == null) return raw;
|
||||
return DateFormat('dd.MM.yyyy').format(dt);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final valuationItems = [
|
||||
_MetricRowItem('KGV (Trailing P/E)', _fmtMultiple(data.peRatioTrailing)),
|
||||
_MetricRowItem('KGV (Forward P/E)', _fmtMultiple(data.peRatioForward)),
|
||||
_MetricRowItem('PEG Ratio', _fmtMultiple(data.pegRatio)),
|
||||
_MetricRowItem('KBV (P/B Ratio)', _fmtMultiple(data.pbRatio)),
|
||||
_MetricRowItem('KUV (P/S Ratio)', _fmtMultiple(data.psRatio)),
|
||||
_MetricRowItem('EV / EBITDA', _fmtMultiple(data.evToEbitda)),
|
||||
_MetricRowItem('EV / Sales', _fmtMultiple(data.evToRevenue)),
|
||||
_MetricRowItem('Enterprise Value', _formatNumber(data.enterpriseValue)),
|
||||
_MetricRowItem('Marktkapitalisierung', _formatNumber(data.marketCapitalization)),
|
||||
_MetricRowItem('Gewinn je Aktie (EPS)', _fmtCurrency(data.dilutedEps)),
|
||||
_MetricRowItem('52W Höchststand', _fmtCurrency(data.fiftyTwoWeekHigh)),
|
||||
_MetricRowItem('52W Tiefststand', _fmtCurrency(data.fiftyTwoWeekLow)),
|
||||
];
|
||||
|
||||
final profitabilityItems = [
|
||||
_MetricRowItem('Umsatzerlöse (Revenue)', _formatNumber(data.totalRevenue)),
|
||||
_MetricRowItem('Umsatzwachstum (YoY)', _fmtPercent(data.revenueGrowthYoY)),
|
||||
_MetricRowItem('Bruttogewinn', _formatNumber(data.grossProfit)),
|
||||
_MetricRowItem('Bruttomarge (Gross)', _fmtPercent(data.grossMargin)),
|
||||
_MetricRowItem('EBITDA', _formatNumber(data.ebitda)),
|
||||
_MetricRowItem('Operative Marge', _fmtPercent(data.operatingMargin)),
|
||||
_MetricRowItem('Nettogewinnmarge', _fmtPercent(data.netProfitMargin)),
|
||||
_MetricRowItem('Eigenkapitalrendite (ROE)', _fmtPercent(data.returnOnEquity)),
|
||||
_MetricRowItem('Gesamtkapitalrendite (ROA)', _fmtPercent(data.returnOnAssets)),
|
||||
_MetricRowItem('Verschuldungsgrad (D/E)', _fmtDebtToEquity(data.debtToEquity)),
|
||||
_MetricRowItem('Current Ratio', _fmtMultiple(data.currentRatio)),
|
||||
_MetricRowItem('Liquide Mittel (Cash)', _formatNumber(data.totalCash)),
|
||||
_MetricRowItem('Gesamtverschuldung (Debt)', _formatNumber(data.totalDebt)),
|
||||
_MetricRowItem('Operativer Cashflow', _formatNumber(data.operatingCashFlow)),
|
||||
_MetricRowItem('Free Cashflow', _formatNumber(data.freeCashFlow)),
|
||||
];
|
||||
|
||||
final dividendItems = [
|
||||
_MetricRowItem('Dividendenrendite', _fmtPercent(data.dividendYield)),
|
||||
_MetricRowItem('Ausschüttungsquote (Payout)', _fmtPercent(data.payoutRatio)),
|
||||
_MetricRowItem('Ex-Dividendentag', _fmtDate(data.exDividendDate)),
|
||||
_MetricRowItem('Nächste Quartalszahlen', _fmtDate(data.nextEarningsDate)),
|
||||
_MetricRowItem('Konsens-Rating', data.consensusRating != null ? data.consensusRating!.toUpperCase() : 'N/A'),
|
||||
_MetricRowItem('Institutioneller Anteil', _fmtPercent(data.percentHeldByInstitutions)),
|
||||
_MetricRowItem('Insider Anteil', _fmtPercent(data.percentHeldByInsiders)),
|
||||
_MetricRowItem('Short % of Float', _fmtPercent(data.shortPercentOfFloat)),
|
||||
];
|
||||
|
||||
final panel1 = _buildCategoryPanel(
|
||||
context: context,
|
||||
title: 'Bewertungskennzahlen & Multiples',
|
||||
icon: Icons.analytics_outlined,
|
||||
items: valuationItems,
|
||||
);
|
||||
|
||||
final panel2 = _buildCategoryPanel(
|
||||
context: context,
|
||||
title: 'Rentabilität & Finanzen',
|
||||
icon: Icons.account_balance_outlined,
|
||||
items: profitabilityItems,
|
||||
);
|
||||
|
||||
final panel3 = _buildCategoryPanel(
|
||||
context: context,
|
||||
title: 'Dividenden & Termine',
|
||||
icon: Icons.pie_chart_outline,
|
||||
items: dividendItems,
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= 1050) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: panel1),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: panel2),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(child: panel3),
|
||||
],
|
||||
);
|
||||
} else if (constraints.maxWidth >= 680) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: panel1),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: panel2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
panel3,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
panel1,
|
||||
const SizedBox(height: 12),
|
||||
panel2,
|
||||
const SizedBox(height: 12),
|
||||
panel3,
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryPanel({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required List<_MetricRowItem> items,
|
||||
}) {
|
||||
return GlassContainer(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(icon, color: AppTheme.primaryEmerald, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Divider(color: Colors.white10, height: 1),
|
||||
const SizedBox(height: 4),
|
||||
for (int i = 0; i < items.length; i++) ...[
|
||||
_buildMetricTile(context, items[i].label, items[i].value, isEven: i.isEven),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricTile(BuildContext context, String label, String value, {bool isEven = false}) {
|
||||
final hasExplanation = MetricExplanations.hasExplanation(label);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: isEven ? Colors.white.withValues(alpha: 0.02) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (hasExplanation) ...[
|
||||
const SizedBox(width: 4),
|
||||
InkWell(
|
||||
onTap: () => MetricExplanations.showModal(context, label),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: Icon(Icons.info_outline, size: 12, color: AppTheme.textMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/widgets/asset_logo_widget.dart';
|
||||
import '../../../../shared/widgets/favorite_star_button.dart';
|
||||
import '../../bloc/header/asset_header_bloc.dart';
|
||||
import '../../bloc/header/asset_header_state.dart';
|
||||
import '../../bloc/fundamentals/asset_fundamentals_bloc.dart';
|
||||
import '../../bloc/fundamentals/asset_fundamentals_state.dart';
|
||||
import '../../bloc/technical/asset_technical_bloc.dart';
|
||||
import '../../bloc/technical/asset_technical_state.dart';
|
||||
import '../../models/asset_model.dart';
|
||||
import '../../models/fundamental_data_model.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AssetHeroHeader extends StatelessWidget {
|
||||
@@ -20,35 +20,31 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
const AssetHeroHeader({
|
||||
super.key,
|
||||
this.onExchangeChanged,
|
||||
this.onForceRefresh, required this.isin, required this.name, this.symbol,
|
||||
this.onForceRefresh,
|
||||
required this.isin,
|
||||
required this.name,
|
||||
this.symbol,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppTheme.activePreset;
|
||||
|
||||
return BlocBuilder<AssetHeaderBloc, AssetHeaderState>(
|
||||
builder: (context, state) {
|
||||
double? price;
|
||||
String currency = 'EUR';
|
||||
List<AssetTickerOption> tickerOptions = [
|
||||
AssetTickerOption(ticker: 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: 0.0)
|
||||
return BlocBuilder<AssetFundamentalsBloc, AssetFundamentalsState>(
|
||||
builder: (context, fundState) {
|
||||
String displayName = name;
|
||||
final String? logoUrl = isin.isNotEmpty ? '/api/v1/logo/$isin' : null;
|
||||
List<TickerModel> tickerOptions = [
|
||||
TickerModel(ticker: symbol ?? 'Loading', exchange: 'Loading', tradingCurrency: '', currentPrice: null)
|
||||
];
|
||||
|
||||
AssetModel? asset;
|
||||
if (state is AssetHeaderLoaded) {
|
||||
asset = state.data;
|
||||
} else if (state is AssetHeaderLoading) {
|
||||
asset = state.previousData;
|
||||
}
|
||||
|
||||
if (asset != null) {
|
||||
//name = asset.name.isNotEmpty ? asset.name : symbol;
|
||||
currency = asset.currency.isNotEmpty ? asset.currency : 'EUR';
|
||||
price = asset.currentPrice;
|
||||
|
||||
if (asset.tickers.isNotEmpty) {
|
||||
tickerOptions = asset.tickers;
|
||||
if (fundState is AssetFundamentalsLoaded && fundState.data != null) {
|
||||
final data = fundState.data!;
|
||||
if (data.companyName.isNotEmpty) {
|
||||
displayName = data.companyName;
|
||||
}
|
||||
if (data.availableTickers.isNotEmpty) {
|
||||
tickerOptions = data.availableTickers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,14 +84,14 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
AssetLogoWidget(symbolOrName: isin, imageUrl: asset?.image, size: 48),
|
||||
AssetLogoWidget(symbolOrName: isin, imageUrl: logoUrl, size: 48),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
name,
|
||||
displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w900,
|
||||
@@ -138,7 +134,7 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
onPressed: onForceRefresh,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FavoriteStarButton(symbol: symbol, identifier: isin, name: name),
|
||||
FavoriteStarButton(symbol: symbol, identifier: isin, name: displayName),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -150,10 +146,8 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
children: [
|
||||
BlocBuilder<AssetTechnicalBloc, AssetTechnicalState>(
|
||||
builder: (context, taState) {
|
||||
double? livePrice = price;
|
||||
String liveCurrency = selectedOption.tradingCurrency.isNotEmpty
|
||||
? selectedOption.tradingCurrency
|
||||
: currency;
|
||||
double? livePrice = selectedOption.currentPrice;
|
||||
String liveCurrency = selectedOption.tradingCurrency ?? 'EUR';
|
||||
|
||||
if (taState is AssetTechnicalLoaded && taState.data != null) {
|
||||
if (taState.data!.candles.isNotEmpty) {
|
||||
@@ -219,12 +213,12 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
(t) => t.ticker == newTicker,
|
||||
orElse: () => tickerOptions.first,
|
||||
);
|
||||
onExchangeChanged!(opt.exchange, opt.ticker);
|
||||
onExchangeChanged!(opt.exchange ?? 'Unknown', opt.ticker);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
return tickerOptions.map((opt) {
|
||||
final ex = opt.exchange;
|
||||
final ex = opt.exchange ?? 'Unknown';
|
||||
final tick = opt.ticker;
|
||||
final label = '$tick ($ex)';
|
||||
final isSelected = tick == symbol || ex == symbol;
|
||||
@@ -263,7 +257,7 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
Icon(Icons.business, size: 14, color: theme.accentColor),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${selectedOption.ticker} (${selectedOption.exchange})',
|
||||
'${selectedOption.ticker} (${selectedOption.exchange ?? 'Unknown'})',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -285,3 +279,4 @@ class AssetHeroHeader extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/widgets/glass_container.dart';
|
||||
import '../../../../core/widgets/status_badge.dart';
|
||||
import '../../../trades/models/trade_model.dart';
|
||||
|
||||
class AssetTradeItemCard extends StatelessWidget {
|
||||
final TradeModel trade;
|
||||
final String defaultSymbol;
|
||||
final VoidCallback? onAccept;
|
||||
final VoidCallback? onSettings;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
const AssetTradeItemCard({
|
||||
super.key,
|
||||
required this.trade,
|
||||
required this.defaultSymbol,
|
||||
this.onAccept,
|
||||
this.onSettings,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
String _fmt(dynamic val) {
|
||||
if (val == null) return 'N/A';
|
||||
final n = (val is num) ? val.toDouble() : double.tryParse(val.toString());
|
||||
return n != null ? n.toStringAsFixed(2) : val.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isin = trade.isin.isNotEmpty ? trade.isin : defaultSymbol;
|
||||
final side = (trade.signalType.isNotEmpty ? trade.signalType : 'BUY').toUpperCase();
|
||||
final status = trade.status.toUpperCase();
|
||||
final isBuy = side == 'BUY' || side == 'LONG';
|
||||
final isActive = status == 'ACTIVE';
|
||||
final sideColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
|
||||
final entryZoneMin = trade.entryZoneMin;
|
||||
final entryZoneMax = trade.entryZoneMax;
|
||||
final entryPrice = trade.entryPrice;
|
||||
final stopLoss = trade.stopLoss;
|
||||
final takeProfit = trade.takeProfit;
|
||||
final takeProfitTargets = trade.takeProfitTargets;
|
||||
final crv = (takeProfit > 0 && stopLoss > 0 && entryPrice > 0)
|
||||
? ((takeProfit - entryPrice).abs() / (entryPrice - stopLoss).abs()).toStringAsFixed(2)
|
||||
: null;
|
||||
final maxLeverage = trade.maxLeverage;
|
||||
|
||||
final actualEntry = trade.actualEntryPrice;
|
||||
final posSize = trade.positionSize;
|
||||
final levUsed = trade.leverageUsed;
|
||||
final qty = trade.positionSize > 0 && trade.actualEntryPrice > 0 ? trade.positionSize / trade.actualEntryPrice : 0;
|
||||
final entryFee = trade.entryFee;
|
||||
final exitFee = trade.exitFee;
|
||||
|
||||
final reasoning = trade.reasoning;
|
||||
final techRationale = trade.technicalRationale;
|
||||
final fundRationale = trade.fundamentalRationale;
|
||||
final riskWarning = trade.riskWarning;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: GlassContainer(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
StatusBadge(label: side, color: sideColor),
|
||||
const SizedBox(width: 8),
|
||||
StatusBadge(
|
||||
label: status,
|
||||
color: isActive ? AppTheme.primaryEmerald : (status == 'PROPOSED' ? AppTheme.accentCyan : AppTheme.textMuted),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (trade.instrumentType.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(trade.instrumentType, style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (isActive) ...[
|
||||
if (onClose != null)
|
||||
ElevatedButton.icon(
|
||||
onPressed: onClose,
|
||||
icon: const Icon(Icons.flag_outlined, size: 14),
|
||||
label: const Text('Schließen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentRed,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (onSettings != null)
|
||||
IconButton(
|
||||
onPressed: onSettings,
|
||||
icon: const Icon(Icons.settings, size: 16, color: Colors.white),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: AppTheme.glassSurface,
|
||||
padding: const EdgeInsets.all(8),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
] else if (status == 'PROPOSED' || status == 'PENDING') ...[
|
||||
if (onAccept != null)
|
||||
ElevatedButton.icon(
|
||||
onPressed: onAccept,
|
||||
icon: const Icon(Icons.check_circle, size: 14),
|
||||
label: const Text('Trade Annehmen'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${trade.companyName.isNotEmpty ? trade.companyName : defaultSymbol} ($isin) | Timeframe: ${trade.timeframe} | Risk: ${trade.riskTolerance}',
|
||||
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.glassSurface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppTheme.glassBorder),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white),
|
||||
_buildTradeStat('Stop-Loss', '€${_fmt(stopLoss)}', AppTheme.accentRed),
|
||||
_buildTradeStat('Take-Profit', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald),
|
||||
],
|
||||
),
|
||||
if (crv != null || maxLeverage > 0) ...[
|
||||
const Divider(color: Colors.white12, height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (crv != null) _buildTradeStat('Chance-Risiko-Verh. (CRV)', '${_fmt(crv)}x', AppTheme.accentCyan),
|
||||
if (maxLeverage > 0) _buildTradeStat('Max. Hebel', '${_fmt(maxLeverage)}x', Colors.orangeAccent),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (actualEntry > 0 || posSize > 0 || levUsed > 0 || qty > 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryEmerald.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppTheme.primaryEmerald.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person_pin_outlined, size: 14, color: AppTheme.primaryEmerald),
|
||||
const SizedBox(width: 6),
|
||||
const Text('Ihre Tatsächlichen Ausführungsdaten:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTradeStat('Tatsächl. Einstieg', '€${_fmt(actualEntry > 0 ? actualEntry : entryPrice)}', Colors.white),
|
||||
_buildTradeStat('Investition', posSize > 0 ? '€${_fmt(posSize)}' : 'N/A', Colors.white),
|
||||
_buildTradeStat('Genutzter Hebel', levUsed > 0 ? '${_fmt(levUsed)}x' : '1x', AppTheme.primaryEmerald),
|
||||
_buildTradeStat('Stückzahl', qty > 0 ? '${_fmt(qty)} Stk.' : 'N/A', Colors.white70),
|
||||
],
|
||||
),
|
||||
if (entryFee > 0 || exitFee > 0) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text('Gebühren: Einstieg €${_fmt(entryFee)} | Ausstieg €${_fmt(exitFee)}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (status == 'CLOSED' || trade.pnlAbsolute != 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final pnlVal = trade.calculatedPnlAbs;
|
||||
final pnlPctVal = trade.calculatedPnlPct;
|
||||
final isWin = pnlVal >= 0;
|
||||
final color = isWin ? AppTheme.primaryEmerald : AppTheme.accentRed;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: color),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(isWin ? Icons.trending_up : Icons.trending_down, size: 16, color: color),
|
||||
const SizedBox(width: 6),
|
||||
const Text('Trade Ergebnis & Realisierter PnL:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildTradeStat('Ausstiegskurs', trade.actualExitPrice > 0 ? '€${_fmt(trade.actualExitPrice)}' : 'N/A', Colors.white),
|
||||
_buildTradeStat('Realisierter PnL (€)', '${(isWin ? "+€" : "-€")}${_fmt(pnlVal.abs())}', color),
|
||||
_buildTradeStat('Rendite (%)', '${(pnlPctVal >= 0 ? "+" : "")}${_fmt(pnlPctVal)}%', isWin ? AppTheme.primaryEmerald : AppTheme.accentRed),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: Text('KI-Analyse & Begründung anzeigen', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
children: [
|
||||
if (reasoning.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Gesamt-Strategie & KI-Entscheidung', reasoning, Colors.white70),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (techRationale.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Technische Analyse', techRationale, Colors.white70),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (fundRationale.isNotEmpty) ...[
|
||||
_buildRationaleBlock('Fundamentale Analyse', fundRationale, Colors.white70),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (riskWarning.isNotEmpty) _buildRationaleBlock('Risikowarnung', riskWarning, AppTheme.accentRed),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTradeStat(String title, String val, Color col) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
|
||||
const SizedBox(height: 2),
|
||||
Text(val, style: TextStyle(color: col, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRationaleBlock(String title, String text, Color col) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white, fontSize: 12)),
|
||||
const SizedBox(height: 2),
|
||||
Text(text, style: TextStyle(color: col, fontSize: 12, height: 1.4)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../trades/models/trade_model.dart';
|
||||
import '../../../trades/models/close_trade_request_dto.dart';
|
||||
|
||||
class CloseTradeDialog {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required TradeModel trade,
|
||||
required String defaultSymbol,
|
||||
required void Function(CloseTradeRequestDto) onClose,
|
||||
}) {
|
||||
final entry = trade.actualEntryPrice > 0 ? trade.actualEntryPrice : trade.entryPrice;
|
||||
final exitController = TextEditingController(text: entry.toStringAsFixed(2));
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.flag_outlined, color: AppTheme.accentRed, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text('Trade Position Schließen', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Trade-ID: ${trade.id} | Asset: ${trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol}', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: exitController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Tatsächlicher Ausstiegskurs (€)',
|
||||
hintText: 'Z.B. 105.50',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final exitPrice = double.tryParse(exitController.text) ?? entry;
|
||||
Navigator.pop(dialogContext);
|
||||
onClose(CloseTradeRequestDto(userExitPrice: exitPrice));
|
||||
},
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Position Schließen & Buchen'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentRed, foregroundColor: Colors.white),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
|
||||
class LiveTradeSettings {
|
||||
final double defaultPositionSize;
|
||||
final double defaultLeverage;
|
||||
final double defaultRiskScore;
|
||||
final double defaultOrderFee;
|
||||
final bool autoAcceptSignals;
|
||||
|
||||
const LiveTradeSettings({
|
||||
required this.defaultPositionSize,
|
||||
required this.defaultLeverage,
|
||||
required this.defaultRiskScore,
|
||||
required this.defaultOrderFee,
|
||||
required this.autoAcceptSignals,
|
||||
});
|
||||
}
|
||||
|
||||
class LiveTradeSettingsDialog {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required LiveTradeSettings currentSettings,
|
||||
required ValueChanged<LiveTradeSettings> onSave,
|
||||
}) {
|
||||
double tempPos = currentSettings.defaultPositionSize;
|
||||
double tempLev = currentSettings.defaultLeverage;
|
||||
double tempRisk = currentSettings.defaultRiskScore;
|
||||
double tempFee = currentSettings.defaultOrderFee;
|
||||
bool tempAuto = currentSettings.autoAcceptSignals;
|
||||
|
||||
final posController = TextEditingController(text: tempPos.toStringAsFixed(0));
|
||||
final levController = TextEditingController(text: tempLev.toStringAsFixed(1));
|
||||
final feeController = TextEditingController(text: tempFee.toStringAsFixed(2));
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (builderContext, setModalState) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.settings, color: AppTheme.accentCyan, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Live Trade Einstellungen',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 440,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Standard Trade-Vorgaben für Ihr Depot:', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: posController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Standard Investment (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
onChanged: (v) => tempPos = double.tryParse(v) ?? tempPos,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: levController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Standard Hebel (x)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
onChanged: (v) => tempLev = double.tryParse(v) ?? tempLev,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: feeController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Standard Ordergebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
onChanged: (v) => tempFee = double.tryParse(v) ?? tempFee,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Standard Risiko-Toleranz:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Text('${tempRisk.toInt()}/100', style: TextStyle(color: AppTheme.primaryEmerald, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: tempRisk,
|
||||
min: 0,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
activeColor: AppTheme.primaryEmerald,
|
||||
inactiveColor: AppTheme.glassSurface,
|
||||
onChanged: (val) => setModalState(() => tempRisk = val),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SwitchListTile(
|
||||
value: tempAuto,
|
||||
activeThumbColor: AppTheme.primaryEmerald,
|
||||
title: const Text('KI-Signale automatisch annehmen', style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Führt eingehende Signale direkt im Depot aus', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
|
||||
onChanged: (val) => setModalState(() => tempAuto = val),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
onSave(LiveTradeSettings(
|
||||
defaultPositionSize: tempPos,
|
||||
defaultLeverage: tempLev,
|
||||
defaultRiskScore: tempRisk,
|
||||
defaultOrderFee: tempFee,
|
||||
autoAcceptSignals: tempAuto,
|
||||
));
|
||||
Navigator.pop(dialogContext);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Live Trade Einstellungen gespeichert.'),
|
||||
backgroundColor: AppTheme.primaryEmerald,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Einstellungen Speichern'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../models/manual_analysis_request_dto.dart';
|
||||
|
||||
class ManualAnalysisDialog {
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String symbol,
|
||||
required double initialRiskScore,
|
||||
required void Function(ManualAnalysisRequestDto) onTrigger,
|
||||
}) {
|
||||
double riskScore = initialRiskScore;
|
||||
final minTimeframeController = TextEditingController(text: '1');
|
||||
final maxTimeframeController = TextEditingController(text: '14');
|
||||
String timeframeUnit = 'Tage';
|
||||
String instrumentType = 'Knock-Out Zertifikat (Turbo)';
|
||||
final notesController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (builderContext, setModalState) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppTheme.cardSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: AppTheme.glassBorder),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_awesome, color: AppTheme.accentCyan, size: 22),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text('KI-Analyse für $symbol', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 440,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Wählen Sie Ihre Zielparameter für die Trade-Evaluierung:', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Zeithorizont (Timeframe):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: minTimeframeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Von', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: maxTimeframeController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Bis', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: timeframeUnit,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(labelText: 'Einheit', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Stunden', child: Text('Stunden')),
|
||||
DropdownMenuItem(value: 'Tage', child: Text('Tage')),
|
||||
DropdownMenuItem(value: 'Wochen', child: Text('Wochen')),
|
||||
DropdownMenuItem(value: 'Monate', child: Text('Monate')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setModalState(() => timeframeUnit = val);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Risikobereitschaft:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Text(
|
||||
'${riskScore.toInt()}/100 (${riskScore < 30 ? "Konservativ" : (riskScore < 70 ? "Ausgewogen" : "Spekulativ")})',
|
||||
style: TextStyle(
|
||||
color: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: riskScore,
|
||||
min: 0,
|
||||
max: 100,
|
||||
divisions: 100,
|
||||
activeColor: riskScore < 30 ? AppTheme.primaryEmerald : (riskScore < 70 ? Colors.orangeAccent : AppTheme.accentRed),
|
||||
inactiveColor: AppTheme.glassSurface,
|
||||
onChanged: (val) => setModalState(() => riskScore = val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Instrumententyp (Trade Republic):', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: instrumentType,
|
||||
dropdownColor: AppTheme.cardSurface,
|
||||
decoration: const InputDecoration(contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Aktie / ETF (Direktinvestment)', child: Text('Aktie / ETF (Direktinvestment)')),
|
||||
DropdownMenuItem(value: 'Optionsschein (Warrant)', child: Text('Optionsschein (Warrant)')),
|
||||
DropdownMenuItem(value: 'Knock-Out Zertifikat (Turbo)', child: Text('Knock-Out Zertifikat (Turbo)')),
|
||||
DropdownMenuItem(value: 'Faktor-Zertifikat', child: Text('Faktor-Zertifikat')),
|
||||
DropdownMenuItem(value: 'Krypto (Crypto)', child: Text('Krypto (Crypto)')),
|
||||
],
|
||||
onChanged: (val) {
|
||||
if (val != null) setModalState(() => instrumentType = val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Anmerkung für die KI:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: notesController,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Z.B. Besonderes Augenmerk auf Hebelprodukte legen, enge Stopps berücksichtigen...',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final payload = ManualAnalysisRequestDto(
|
||||
isin: symbol,
|
||||
symbol: symbol,
|
||||
riskScore: riskScore.toInt(),
|
||||
minTimeframeValue: int.tryParse(minTimeframeController.text) ?? 1,
|
||||
maxTimeframeValue: int.tryParse(maxTimeframeController.text) ?? 14,
|
||||
timeframeUnit: timeframeUnit,
|
||||
instrumentType: instrumentType,
|
||||
userNotes: notesController.text,
|
||||
headline: 'Manuelle KI-Analyse für $symbol',
|
||||
);
|
||||
Navigator.pop(dialogContext);
|
||||
onTrigger(payload);
|
||||
},
|
||||
icon: const Icon(Icons.flash_on),
|
||||
label: const Text('Analyse Jetzt Ausführen'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user