import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/status_badge.dart'; import '../models/trade_model.dart'; import '../models/trade_acceptance_dto.dart'; import 'trade_execution_ai_plan_card.dart'; import 'trade_calculation_card.dart'; /// Dialog for either (a) confirming the real execution of a trade proposal /// (the AI-computed plan — instrument, exit strategy, stop-loss, take-profit /// stages — is shown read-only via [TradeExecutionAiPlanCard]/ /// [TradeCalculationCard]; only the price/quantity the user actually got /// filled at are editable), or (b) — when [isActive] is `true` — reviewing an /// already-open trade's execution numbers. /// /// This used to also let the user redesign the whole trade client-side /// (instrument type, derivative pick, leverage, entry/exit fees, a /// client-computed CRV). None of that survived the `ActiveTradeDto` / /// `TradeAcceptanceDto` migration: the engine now picks the instrument and /// derivative itself (`TradeProposalDto.selectedDerivative`), computes the /// full exit plan server-side (`ExitPlan`), and /// `UserTradesController.AcceptTrade` only ever reads /// `ActualEntryPrice`/`EntryPrice` and `Quantity`/`PositionSize` from the /// payload — every other field the old cockpit built (leverage, fees, /// instrument type, derivative ISIN, timeframe, reasoning) is silently /// ignored server-side today. Rebuilding client-side leverage/fee math for /// fields the backend no longer accepts would be exactly the kind of /// disconnected-from-reality UI Rules.md §4 forbids, so that whole surface /// was removed rather than kept alive against fields that don't exist /// anymore (Rules.md §3/§4). class TradeExecutionCockpit extends StatefulWidget { final TradeModel trade; final String defaultSymbol; final bool isActive; final Function(TradeAcceptanceDto dto) onAccept; final Function(String tradeId)? onReject; const TradeExecutionCockpit({ super.key, required this.trade, required this.defaultSymbol, this.isActive = false, required this.onAccept, this.onReject, }); static Future show( BuildContext context, { required TradeModel trade, required String defaultSymbol, bool isActive = false, required Function(TradeAcceptanceDto dto) onAccept, Function(String tradeId)? onReject, }) { return showDialog( context: context, barrierDismissible: true, builder: (dialogContext) => TradeExecutionCockpit( trade: trade, defaultSymbol: defaultSymbol, isActive: isActive, onAccept: onAccept, onReject: onReject, ), ); } @override State createState() => _TradeExecutionCockpitState(); } class _TradeExecutionCockpitState extends State { late TextEditingController _entryPriceCtrl; late TextEditingController _quantityCtrl; @override void initState() { super.initState(); // Both fields are prefilled from real, already-known server data where // available — never from an invented placeholder (Rules.md §4). For an // already-active trade both numbers are simply the engine's own record // of what happened (`averageBuyIn`/`totalQuantity`). For a still-proposed // trade there is no filled quantity yet, so that field starts empty and // the user must supply the real number — showing e.g. a default of 1000 // would look like a real suggested size when it is not. final prefillPrice = widget.isActive ? widget.trade.averageBuyIn : (widget.trade.currentPrice > 0 ? widget.trade.currentPrice : widget.trade.averageBuyIn); _entryPriceCtrl = TextEditingController(text: prefillPrice > 0 ? prefillPrice.toStringAsFixed(2) : ''); _quantityCtrl = TextEditingController( text: widget.isActive && widget.trade.totalQuantity > 0 ? widget.trade.totalQuantity.toString() : '', ); _entryPriceCtrl.addListener(() => setState(() {})); _quantityCtrl.addListener(() => setState(() {})); } @override void dispose() { _entryPriceCtrl.dispose(); _quantityCtrl.dispose(); super.dispose(); } double? _parsePositive(TextEditingController ctrl) { final clean = ctrl.text.replaceAll(',', '.').trim(); final val = double.tryParse(clean); return (val != null && val > 0) ? val : null; } double? get _entryPrice => _parsePositive(_entryPriceCtrl); double? get _quantity => _parsePositive(_quantityCtrl); /// Straightforward price × quantity of the two values the user is about to /// submit — not a P&L figure and not derived from any server P&L field, so /// recomputing it client-side does not create a second, competing P&L /// calculation (Rules.md's "don't recompute P&L" concern is about /// `unrealizedPnlEur`/`realizedPnlEur`, which this never touches). double? get _investedCapital { final p = _entryPrice; final q = _quantity; return (p != null && q != null) ? p * q : null; } bool get _canSubmit => _entryPrice != null && _quantity != null; TradeAcceptanceDto _buildDto() { // For an accept action the payload identifier must be the *proposal* id // (`UserTradesController.AcceptTrade` parses `tradeId` as a proposal // Guid and calls `engine_AcceptProposal`), which is `trade.proposalId` // here — not `trade.id` (the already-existing `ActiveTradeDto.TradeId`). // For the settings-review path on an already-active trade there is no // separate proposal to target, so the trade's own id is used instead. final identifier = widget.isActive ? widget.trade.id : widget.trade.proposalId; final isinVal = widget.trade.underlyingIsin.isNotEmpty ? widget.trade.underlyingIsin : widget.defaultSymbol; final symbolVal = widget.trade.symbol.isNotEmpty ? widget.trade.symbol : widget.defaultSymbol; return TradeAcceptanceDto( userId: '', // Discarded server-side in favor of the JWT identity anyway. tradeId: identifier, isin: isinVal, symbol: symbolVal, actualEntryPrice: _entryPrice, quantity: _quantity, executionTimestamp: DateTime.now().toUtc(), ); } @override Widget build(BuildContext context) { final isLong = widget.trade.direction.isLong; final signalColor = isLong ? AppTheme.primaryEmerald : AppTheme.accentRed; return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: Container( width: 560, constraints: const BoxConstraints(maxHeight: 780), decoration: BoxDecoration( color: AppTheme.cardSurface, borderRadius: BorderRadius.circular(24), border: Border.all(color: AppTheme.glassBorder), boxShadow: [ BoxShadow(color: Colors.black.withValues(alpha: 0.6), blurRadius: 30, offset: const Offset(0, 10)), ], ), child: Column( children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(20, 18, 16, 14), child: Row( children: [ StatusBadge(label: widget.trade.direction.label, color: signalColor), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.isActive ? 'Ausführungsdetails – Trade #${widget.trade.id}' : '1-Click Trade Execution Cockpit', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), ), Text( '${widget.trade.symbol.isNotEmpty ? widget.trade.symbol : widget.defaultSymbol} (${widget.trade.underlyingIsin.isNotEmpty ? widget.trade.underlyingIsin : widget.defaultSymbol})', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ], ), ), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, color: Colors.white54), ), ], ), ), const Divider(color: Colors.white12, height: 1), // Scrollable Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Read-only AI-computed plan (instrument, exit strategy, // stop-loss, take-profit stages — all real server fields). TradeExecutionAiPlanCard(trade: widget.trade), const SizedBox(height: 12), TradeCalculationCard(trade: widget.trade, isCollapsible: false), const SizedBox(height: 18), Text( widget.isActive ? 'AUSFÜHRUNGSDATEN DIESES TRADES' : 'DEINE TATSÄCHLICHE AUSFÜHRUNG', style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 0.5), ), const SizedBox(height: 10), Row( children: [ Expanded(child: _buildInput('Ausführungskurs (€)', _entryPriceCtrl, Icons.price_change)), const SizedBox(width: 12), Expanded(child: _buildInput('Stückzahl', _quantityCtrl, Icons.numbers)), ], ), const SizedBox(height: 10), Text( _investedCapital != null ? 'Eingesetztes Kapital: €${_investedCapital!.toStringAsFixed(2)}' : 'Bitte Ausführungskurs und Stückzahl angeben.', style: TextStyle(color: AppTheme.textMuted, fontSize: 11), ), const SizedBox(height: 18), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.amber.withValues(alpha: 0.25)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.gavel_outlined, size: 16, color: Colors.amber.withValues(alpha: 0.85)), const SizedBox(width: 10), Expanded( child: Text( 'Rechtlicher Hinweis: Keine Anlageberatung. Sämtliche Analysen, Kennzahlen und Simulationen dienen ausschließlich Informations- und Bildungszwecken. Der Handel mit Hebelprodukten (Derivate, CFDs) birgt erhebliche Risiken bis hin zum Totalverlust des eingesetzten Kapitals.', style: TextStyle(color: AppTheme.textMuted, fontSize: 10.5, height: 1.35), ), ), ], ), ), ], ), ), ), // Actions Footer Padding( padding: const EdgeInsets.all(16), child: Row( children: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Abbrechen', style: TextStyle(color: AppTheme.textMuted)), ), const Spacer(), if (!widget.isActive && widget.onReject != null) ...[ // This only hides the card locally — the proposal is a // system-wide opportunity anyone may still accept, and // there is no server-side rejection anymore. Label and // color must not suggest an irreversible/destructive // action that doesn't actually happen (Rules.md §4). OutlinedButton.icon( onPressed: () { widget.onReject!(widget.trade.id); Navigator.pop(context); }, icon: Icon(Icons.visibility_off_outlined, color: AppTheme.textMuted, size: 16), label: Text('Verwerfen', style: TextStyle(color: AppTheme.textMuted)), style: OutlinedButton.styleFrom(side: BorderSide(color: AppTheme.textMuted.withValues(alpha: 0.5))), ), const SizedBox(width: 10), ], ElevatedButton.icon( onPressed: _canSubmit ? () { final dto = _buildDto(); widget.onAccept(dto); Navigator.pop(context); } : null, icon: Icon(widget.isActive ? Icons.save : Icons.check_circle, size: 18), label: Text( widget.isActive ? 'Ausführung Speichern' : 'Trade Jetzt Eröffnen', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), style: ElevatedButton.styleFrom( backgroundColor: widget.isActive ? AppTheme.accentCyan : AppTheme.primaryEmerald, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ], ), ), ], ), ), ); } Widget _buildInput(String label, TextEditingController controller, IconData icon) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(color: AppTheme.textSecondary, fontSize: 11, fontWeight: FontWeight.bold)), const SizedBox(height: 6), TextField( controller: controller, keyboardType: const TextInputType.numberWithOptions(decimal: true), style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), decoration: InputDecoration( prefixIcon: Icon(icon, size: 16, color: Colors.white54), filled: true, fillColor: AppTheme.glassSurface, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.glassBorder)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: AppTheme.accentCyan)), ), ), ], ); } }