feat(asset_detail): modular fundamentals sections, executive salaries and logo resolution

This commit is contained in:
2026-08-15 01:03:30 +02:00
parent 15f8f7896e
commit 1ccb6b613f
21 changed files with 2011 additions and 2081 deletions
@@ -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),
),
],
);
},
);
},
);
}
}