import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:finlytic_app/core/theme/app_theme.dart'; import 'package:finlytic_app/core/widgets/status_badge.dart'; import 'package:finlytic_app/core/network/api_client.dart'; import '../../../../features/trades/models/trade_model.dart'; import '../../../../features/trades/models/trade_acceptance_dto.dart'; class TradeExecutionDialog { static const double _defaultPositionSize = 1000.0; static const double _defaultLeverage = 1.0; static const List _allowedInstruments = ['Stock', 'KnockOut', 'Option', 'CFD', 'Crypto']; /// Normalisiert beliebige Freitexte/Bezeichnungen auf die erlaubten Dropdown-Werte static String _normalizeInstrumentType(String raw) { final clean = raw.toLowerCase().trim(); if (clean.contains('knock') || clean.contains('zertifikat') || clean.contains('turbo')) { return 'KnockOut'; } if (clean.contains('option')) { return 'Option'; } if (clean.contains('cfd')) { return 'CFD'; } if (clean.contains('crypto') || clean.contains('krypto')) { return 'Crypto'; } if (clean.contains('stock') || clean.contains('aktie') || clean.contains('etf')) { return 'Stock'; } return 'KnockOut'; // Fallback } static void show( BuildContext context, { required TradeModel trade, required String defaultSymbol, bool isActive = false, required Function(TradeAcceptanceDto dto) onAccept, Function(String tradeId)? onReject, }) { final initEntry = trade.actualEntryPrice > 0 ? trade.actualEntryPrice : (trade.entryPrice > 0 ? trade.entryPrice : 100.0); final initPos = trade.positionSize > 0 ? trade.positionSize : _defaultPositionSize; final initLev = trade.leverageUsed > 0 ? trade.leverageUsed : _defaultLeverage; final calcQty = (initEntry > 0 && initPos > 0) ? (initPos / initEntry) : 10.0; final initQty = trade.quantity > 0 ? trade.quantity : calcQty; final actualEntryController = TextEditingController(text: initEntry.toStringAsFixed(2)); final positionSizeController = TextEditingController(text: initPos.toStringAsFixed(2)); final leverageController = TextEditingController(text: initLev.toStringAsFixed(1)); final quantityController = TextEditingController(text: initQty.toStringAsFixed(4)); final entryFeeController = TextEditingController(text: trade.entryFee.toStringAsFixed(2)); final exitFeeController = TextEditingController(text: trade.exitFee.toStringAsFixed(2)); final slController = TextEditingController(text: trade.stopLoss.toString()); final tpController = TextEditingController(text: trade.takeProfit.toString()); final derivativeIsinController = TextEditingController(text: trade.derivativeIsin); // Normalisierte Zuweisung verhindert den DropdownButton Assertion-Error String selectedInstrumentType = _normalizeInstrumentType( trade.instrumentType.isNotEmpty ? trade.instrumentType : 'KnockOut', ); bool isFetchingDerivativePrice = false; void recalculateQuantity() { final entryStr = actualEntryController.text.replaceAll(',', '.').trim(); final posStr = positionSizeController.text.replaceAll(',', '.').trim(); final entry = double.tryParse(entryStr) ?? 0.0; final posSize = double.tryParse(posStr) ?? 0.0; if (entry > 0 && posSize > 0) { final q = posSize / entry; quantityController.text = q.toStringAsFixed(4); } } TradeAcceptanceDto buildDto() { final isinVal = trade.isin.isNotEmpty ? trade.isin : (trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol); final symbolVal = trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol; double parseNum(String text, double fallback) { final clean = text.replaceAll(',', '.').trim(); return double.tryParse(clean) ?? fallback; } return TradeAcceptanceDto( userId: trade.userId, tradeId: trade.id, analysisId: trade.analysisId, isin: isinVal, symbol: symbolVal, actualEntryPrice: parseNum(actualEntryController.text, trade.actualEntryPrice > 0 ? trade.actualEntryPrice : trade.entryPrice), positionSize: parseNum(positionSizeController.text, trade.positionSize > 0 ? trade.positionSize : 1000.0), leverageUsed: parseNum(leverageController.text, trade.leverageUsed > 0 ? trade.leverageUsed : 1.0), entryFee: parseNum(entryFeeController.text, trade.entryFee), exitFee: parseNum(exitFeeController.text, trade.exitFee), quantity: parseNum(quantityController.text, trade.quantity), executionTimestamp: DateTime.now().toUtc(), signalType: trade.signalType, entryPrice: trade.entryPrice, stopLoss: parseNum(slController.text, trade.stopLoss), takeProfit: parseNum(tpController.text, trade.takeProfit), instrumentType: selectedInstrumentType, derivativeIsin: derivativeIsinController.text.trim(), timeframe: trade.timeframe, reasoning: trade.reasoning, ); } Future fetchDerivativePrice(StateSetter setModalState, String inputIsin) async { final cleanIsin = inputIsin.trim().toUpperCase(); if (cleanIsin.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Bitte gib eine gültige Derivat/Knock-Out ISIN ein.'), backgroundColor: Colors.amber, behavior: SnackBarBehavior.floating, ), ); return; } setModalState(() => isFetchingDerivativePrice = true); try { final apiClient = context.read(); final res = await apiClient.get('/api/v1/assets/$cleanIsin/technicals?forceRefresh=true'); if (res.statusCode == 200 && res.data != null) { final Map data = res.data; double? fetchedPrice; if (data['candles'] is List && (data['candles'] as List).isNotEmpty) { fetchedPrice = ((data['candles'] as List).last['close'] as num?)?.toDouble(); } else if (data['currentPrice'] != null) { fetchedPrice = (data['currentPrice'] as num?)?.toDouble(); } if (fetchedPrice != null && fetchedPrice > 0) { actualEntryController.text = fetchedPrice.toStringAsFixed(2); recalculateQuantity(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Live-Kurs für Derivat $cleanIsin abgerufen: €${fetchedPrice.toStringAsFixed(2)}'), backgroundColor: AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Kein Kurs für Derivat ISIN $cleanIsin gefunden.'), backgroundColor: Colors.amber, behavior: SnackBarBehavior.floating, ), ); } } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Fehler beim Abrufen des Kurses für $cleanIsin via tr_GetPrice: $e'), backgroundColor: AppTheme.accentRed, behavior: SnackBarBehavior.floating, ), ); } finally { setModalState(() => isFetchingDerivativePrice = false); } } actualEntryController.addListener(recalculateQuantity); positionSizeController.addListener(recalculateQuantity); showDialog( context: context, builder: (dialogContext) { return StatefulBuilder( builder: (stfContext, setModalState) { final isKnockout = selectedInstrumentType.toLowerCase().contains('knock') || selectedInstrumentType.toLowerCase().contains('zertifikat') || selectedInstrumentType.toLowerCase().contains('option') || selectedInstrumentType.toLowerCase().contains('cfd'); // Absicherung gegen Assertion-Errors: Stellt sicher, dass der selektierte Wert in der Liste existiert final safeInstrumentValue = _allowedInstruments.contains(selectedInstrumentType) ? selectedInstrumentType : 'KnockOut'; return AlertDialog( backgroundColor: AppTheme.cardSurface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: AppTheme.glassBorder), ), title: Row( children: [ Icon(isActive ? Icons.tune : Icons.edit_note_outlined, color: AppTheme.primaryEmerald, size: 22), const SizedBox(width: 8), Expanded( child: Text( isActive ? 'Einstellungen für Trade #${trade.id}' : 'Trade-Ausführung & Parameter', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), ), ), ], ), content: SizedBox( width: 580, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Trade-ID: ${trade.id} | Symbol/ISIN: ${trade.symbol.isNotEmpty ? trade.symbol : defaultSymbol}', style: TextStyle(color: AppTheme.textSecondary, fontSize: 12), ), const SizedBox(height: 12), Builder( builder: (context) { final signal = trade.signalType.toUpperCase(); final isLong = signal == 'BUY' || signal == 'LONG'; final signalColor = isLong ? 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 - entryPrice) / (entryPrice - stopLoss).abs(); final maxLeverage = trade.maxLeverage; final reasoning = trade.reasoning; final techRationale = trade.technicalRationale; final fundRationale = trade.fundamentalRationale; final riskWarning = trade.riskWarning; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.glassSurface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.glassBorder), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ StatusBadge(label: isLong ? 'LONG / KAUFEN' : 'SHORT / VERKAUFEN', color: signalColor), 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: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), ), ], ), if (trade.winRate > 0) Row( children: [ Icon(Icons.bolt, size: 14, color: AppTheme.accentCyan), Text('Win-Rate: ${trade.winRate}%', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)), ], ), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Haltedauer: ${trade.timeframe.isNotEmpty ? trade.timeframe : '1-14 Tage'}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), Text('Risiko: ${trade.riskTolerance}', style: TextStyle(color: AppTheme.textMuted, fontSize: 11)), if (trade.vixValue > 0) Text('VIX: ${_fmt(trade.vixValue)} (${trade.vixRegime})', style: const TextStyle(color: Colors.orangeAccent, fontSize: 11, fontWeight: FontWeight.bold)), ], ), const Divider(color: Colors.white12, height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildTradeStat('Einstiegszone', entryZoneMin > 0 && entryZoneMax > 0 ? '€${_fmt(entryZoneMin)} - €${_fmt(entryZoneMax)}' : '€${_fmt(entryPrice)}', Colors.white), _buildTradeStat('Stop-Loss Target', '€${_fmt(stopLoss)}', AppTheme.accentRed), _buildTradeStat('Take-Profit Target', takeProfitTargets.isNotEmpty ? takeProfitTargets.map((t) => '€${_fmt(t)}').join(' / ') : '€${_fmt(takeProfit)}', AppTheme.primaryEmerald), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (crv > 0) _buildTradeStat('Chance-Risiko-Verh. (CRV)', '${_fmt(crv)}x', AppTheme.accentCyan), if (maxLeverage > 0) _buildTradeStat('Empf. Max Hebel', '${_fmt(maxLeverage)}x', Colors.orangeAccent), _buildTradeStat('Signal Typ', isLong ? 'LONG / BULLISH' : 'SHORT / BEARISH', signalColor), ], ), if (reasoning.isNotEmpty || techRationale.isNotEmpty || fundRationale.isNotEmpty || riskWarning.isNotEmpty) ...[ const SizedBox(height: 10), ExpansionTile( tilePadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero, dense: true, title: Text('Ausführliche KI-Analyse & Begründung anzeigen', style: TextStyle(color: AppTheme.accentCyan, fontWeight: FontWeight.bold, fontSize: 12)), children: [ if (reasoning.isNotEmpty) ...[ _buildRationaleBlock('Gesamt-Strategie & KI-Entscheidung', reasoning, Colors.white70), const SizedBox(height: 6), ], if (techRationale.isNotEmpty) ...[ _buildRationaleBlock('Technische Analyse', techRationale, Colors.white70), const SizedBox(height: 6), ], if (fundRationale.isNotEmpty) ...[ _buildRationaleBlock('Fundamentale Analyse', fundRationale, Colors.white70), const SizedBox(height: 6), ], if (riskWarning.isNotEmpty) _buildRationaleBlock('Risikowarnung', riskWarning, AppTheme.accentRed), ], ), ], ], ), ); }, ), const SizedBox(height: 16), const Text('Ihre Ausführungsdaten für das Depot:', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(height: 10), // Instrument-Type Dropdown mit abgesichertem Value DropdownButtonFormField( value: safeInstrumentValue, dropdownColor: AppTheme.cardSurface, decoration: const InputDecoration( labelText: 'Finanzinstrument Typ', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), items: const [ DropdownMenuItem(value: 'Stock', child: Text('Aktie / ETF (Direktinvestment)', style: TextStyle(color: Colors.white, fontSize: 13))), DropdownMenuItem(value: 'KnockOut', child: Text('Knock-Out Zertifikat', style: TextStyle(color: Colors.white, fontSize: 13))), DropdownMenuItem(value: 'Option', child: Text('Optionsschein / Derivat', style: TextStyle(color: Colors.white, fontSize: 13))), DropdownMenuItem(value: 'CFD', child: Text('CFD (Hebel-Derivat)', style: TextStyle(color: Colors.white, fontSize: 13))), DropdownMenuItem(value: 'Crypto', child: Text('Krypto', style: TextStyle(color: Colors.white, fontSize: 13))), ], onChanged: (val) { if (val != null) { setModalState(() { selectedInstrumentType = val; }); } }, ), const SizedBox(height: 10), if (isKnockout) ...[ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: TextField( controller: derivativeIsinController, decoration: const InputDecoration( labelText: 'Knock-Out / Derivat ISIN (z.B. DE000...)', hintText: 'ISIN des Hebels eingeben...', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), ), ), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: isFetchingDerivativePrice ? null : () => fetchDerivativePrice(setModalState, derivativeIsinController.text), icon: isFetchingDerivativePrice ? const SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black)) : const Icon(Icons.bolt, size: 16), label: const Text('tr_GetPrice'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), ), ], ), const SizedBox(height: 10), ], Row( children: [ Expanded( child: TextField( controller: actualEntryController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Tatsächlicher Einstiegskurs (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: positionSizeController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Investitionsvolumen (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), ], ), const SizedBox(height: 10), Row( children: [ Expanded( child: TextField( controller: leverageController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Genutzter Hebel (x)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: quantityController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Stückzahl (Invest. / Einstieg)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), ], ), const SizedBox(height: 10), Row( children: [ Expanded( child: TextField( controller: entryFeeController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Einstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: exitFeeController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Ausstiegsgebühr (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), ], ), const SizedBox(height: 10), Row( children: [ Expanded( child: TextField( controller: slController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Stop-Loss (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: tpController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(labelText: 'Take-Profit (€)', contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8)), ), ), ], ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)), ), if (!isActive && onReject != null) OutlinedButton.icon( onPressed: () { onReject(trade.id); Navigator.pop(dialogContext); }, icon: Icon(Icons.cancel, color: AppTheme.accentRed, size: 16), label: Text('Trade Ablehnen', style: TextStyle(color: AppTheme.accentRed)), style: OutlinedButton.styleFrom( side: BorderSide(color: AppTheme.accentRed), ), ), if (!isActive && onReject != null) const SizedBox(width: 8), ElevatedButton.icon( onPressed: () { final dto = buildDto(); onAccept(dto); Navigator.of(dialogContext).pop(); }, icon: Icon(isActive ? Icons.save : Icons.check_circle, size: 16), label: Text(isActive ? 'Einstellungen Speichern' : 'Trade Annehmen & Ausführen'), style: ElevatedButton.styleFrom( backgroundColor: isActive ? AppTheme.accentCyan : AppTheme.primaryEmerald, foregroundColor: Colors.black, ), ), ], ); }, ); }, ); } static String _fmt(dynamic val) { if (val == null) return '0.00'; if (val is double) { if (val > 100) return val.toStringAsFixed(1); return val.toStringAsFixed(2); } return val.toString(); } static Widget _buildTradeStat(String label, String value, Color color) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(color: Colors.white54, fontSize: 11)), const SizedBox(height: 2), Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)), ], ); } static Widget _buildRationaleBlock(String title, String content, Color color) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold)), const SizedBox(height: 4), Text(content, style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.4)), ], ), ); } }