import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/network/api_client.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/glass_container.dart'; import '../../../../core/widgets/shimmer_loading.dart'; import '../../../../core/widgets/status_badge.dart'; import '../../../../shared/widgets/evaluation_score_breakdown_sheet.dart'; import '../../../bot/repositories/bot_repository.dart'; import '../../../proposals/views/proposal_decision_screen.dart'; import '../../../trades/models/trade_model.dart'; import '../../../trades/widgets/trade_execution_cockpit.dart'; import '../../../trades/widgets/trade_closing_cockpit.dart'; import '../../bloc/trades/asset_trades_bloc.dart'; import '../../bloc/trades/asset_trades_event.dart'; import '../../bloc/trades/asset_trades_state.dart'; import '../../widgets/trades/manual_analysis_dialog.dart'; import '../../widgets/trades/asset_trade_item_card.dart'; class TradesTab extends StatefulWidget { final String symbol; const TradesTab({super.key, required this.symbol}); @override State createState() => _TradesTabState(); } class _TradesTabState extends State { late final BotRepository _botRepository; @override void initState() { super.initState(); _botRepository = BotRepository(apiClient: context.read()); context.read().add(LoadAssetTrades(widget.symbol)); } void _showEditTradeExecutionDialog(BuildContext context, TradeModel trade, {bool isActive = false}) { final tradesBloc = context.read(); TradeExecutionCockpit.show( context, trade: trade, defaultSymbol: widget.symbol, isActive: isActive, onAccept: (dto) { final tId = trade.id; // `TradeExecutionCockpit._buildDto()` already picks the right identifier // (trade.id for isActive, trade.proposalId otherwise) and always fills // actualEntryPrice/quantity from the two fields the dialog actually // collects — but the two identifiers target different server-side // operations: accepting a *proposal* vs. recording a fill against an // already-*existing* trade (`UserTradesController.AcceptTrade` looks // `dto.tradeId` up as a proposal id, which fails for an active trade's // own id). Route accordingly instead of always calling AcceptTradeEvent. if (isActive) { final price = dto.actualEntryPrice ?? dto.entryPrice; final qty = dto.quantity ?? dto.positionSize; if (price == null || qty == null) return; tradesBloc.add(AddTradeFillEvent(tId, widget.symbol, price, qty)); } else { tradesBloc.add(AcceptTradeEvent(dto, widget.symbol)); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(isActive ? 'Einstellungen für Trade $tId gespeichert!' : 'Trade $tId angenommen & Position eröffnet!'), backgroundColor: AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, ), ); }, onReject: (tId) { // Purely local dismissal — there is no server-side rejection (a // proposal is a system-wide opportunity anyone may still accept). // Wording must not claim a permanence the backend doesn't provide. tradesBloc.add(DismissTradeEvent(tId)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text( 'Vorschlag ausgeblendet – er kann beim nächsten Neuladen erneut erscheinen, bis er serverseitig abläuft.', ), backgroundColor: AppTheme.textSecondary, behavior: SnackBarBehavior.floating, ), ); }, ); } Future _executeProposalViaBot(BuildContext context, TradeProposalModel proposal) async { Navigator.of(context).pop(); try { await _botRepository.executeProposal(proposal.proposalId); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Vorschlag für ${proposal.symbol} an den Bot übergeben.'), backgroundColor: AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, ), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Fehler bei der Bot-Übergabe: $e'), backgroundColor: AppTheme.accentRed, behavior: SnackBarBehavior.floating, ), ); } } void _showProposalDecision(BuildContext context, TradeProposalModel proposal) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ProposalDecisionScreen( proposal: proposal, onExecuteBot: () => _executeProposalViaBot(context, proposal), onManualTrade: () { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Manuelle Eröffnung: Bitte über die Order-Maske deines Brokers ausführen.'), behavior: SnackBarBehavior.floating, ), ); }, ), ), ); } /// Shows the real, already-computed score breakdown and AI reasoning for a /// manual analysis that ran but did not produce a trade proposal /// ([AssetEvaluationResultModel.proposal] is `null`). Replaces the old bare /// "kein Vorschlag" snackbar: the user gets to see *why* the opportunity /// was rejected, not just *that* it was (Rules.md §4). Every value shown /// here comes straight from the server response — nothing is invented, and /// [AssetEvaluationResultModel.daysToNextEarnings] is only rendered when /// the server actually sent a value. void _showEvaluationRejectedSheet(BuildContext context, AssetEvaluationResultModel result) { EvaluationScoreBreakdownSheet.show( context, title: 'Analyse abgeschlossen – kein Vorschlag', subtitle: 'Für ${widget.symbol} wurde keine aktive Trade-Empfehlung erzeugt. Die berechneten Werte und die KI-Begründung stehen unten.', headerIcon: result.aiApproved ? Icons.psychology_outlined : Icons.block_outlined, headerColor: result.aiApproved ? AppTheme.primaryEmerald : AppTheme.accentRed, compositeScore: result.compositeScore, technicalScore: result.technicalScore, sentimentScore: result.sentimentScore, fundamentalScore: result.fundamentalScore, passedEarningsLockout: result.passedEarningsLockout, daysToNextEarnings: result.daysToNextEarnings, passedDividendGate: result.passedDividendGate, daysToNextExDividend: result.daysToNextExDividend, reasoningLabel: result.aiApproved ? 'KI-These' : 'Ablehnungsgrund', reasoningText: result.aiThesisSummary, identifiedRisks: result.aiIdentifiedRisks, ); } @override Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { if (state is! AssetTradesLoaded) return; final result = state.manualAnalysisResult; if (result == null) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (result.hasProposal) { _showProposalDecision(context, result.proposal!); } else { // Rejected (or no technical setup at all) - show the real, already // computed scores and AI reasoning instead of a bare "no proposal" // snackbar, so the user understands *why*, not just *that* // (Rules.md §4). _showEvaluationRejectedSheet(context, result); } }); }, builder: (context, state) { final List tradesList = (state is AssetTradesLoaded) ? state.data : []; return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GlassContainer( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Trade & Signal Management', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: Colors.white)), const SizedBox(height: 4), Text('KI-gestützte technische & fundamentale Trade-Analyse anfordern', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), const SizedBox(width: 8), StatusBadge(label: widget.symbol, color: AppTheme.accentCyan), ], ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { ManualAnalysisDialog.show( context, symbol: widget.symbol, initialRiskScore: 50.0, onTrigger: (payload) { context.read().add(TriggerManualAnalysis(widget.symbol, payload: payload)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('KI-Analyse für ${widget.symbol} wird ausgeführt...'), backgroundColor: AppTheme.accentCyan, behavior: SnackBarBehavior.floating, ), ); }, ); }, icon: const Icon(Icons.auto_awesome, size: 18), label: const Text('KI-Analyse Starten', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.accentCyan, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ], ), ), const SizedBox(height: 20), if (state is AssetTradesLoading) _buildTradesShimmer(context) else if (state is AssetTradesError) GlassContainer( padding: const EdgeInsets.all(16), child: Text('Fehler: ${state.message}', style: TextStyle(color: AppTheme.accentRed)), ) else if (state is AssetTradesLoaded) ...[ _buildTradeList( 'Aktive Trade-Signale & Positionen', tradesList.where((t) => t.isActive || t.isProposed).toList(), ), const SizedBox(height: 20), _buildTradeList( 'Historische Trades & KI-Bewertungen', tradesList.where((t) => t.isClosed || t.isRejected).toList(), ), ], ], ), ); }, ); } Widget _buildTradesShimmer(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const ShimmerLoading(width: 260, height: 20, borderRadius: 6), const SizedBox(height: 12), for (int i = 0; i < 3; i++) ...[ const ShimmerLoading(width: double.infinity, height: 105, borderRadius: 14), const SizedBox(height: 12), ], ], ); } Widget _buildTradeList(String title, List trades) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)), const SizedBox(height: 10), if (trades.isEmpty) GlassContainer( padding: const EdgeInsets.all(16), child: Center( child: Text('Keine Trades in dieser Kategorie vorhanden.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12)), ), ) else ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: trades.length, itemBuilder: (context, index) { final trade = trades[index]; final isActive = trade.isActive; return AssetTradeItemCard( trade: trade, defaultSymbol: widget.symbol, onAccept: () => _showEditTradeExecutionDialog(context, trade), onSettings: () => _showEditTradeExecutionDialog(context, trade, isActive: true), onClose: isActive ? () { TradeClosingCockpit.show( context, trade: trade, defaultSymbol: widget.symbol, onClose: (dto) { final isinVal = trade.underlyingIsin.isNotEmpty ? trade.underlyingIsin : widget.symbol; context.read().add(CloseTradeEvent(trade.id, isinVal, dto.userExitPrice)); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Trade ${trade.id} geschlossen! Realisierter Ausstiegskurs: €${dto.userExitPrice.toStringAsFixed(2)}'), backgroundColor: AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, ), ); }, ); } : null, ); }, ), ], ); } }