feat(App): update Finlytic Flutter app UI and blocs

This commit is contained in:
2026-08-09 21:01:46 +02:00
parent e7427b7464
commit a708d2977c
591 changed files with 1095105 additions and 0 deletions
@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:finlytic_app/features/trades/repositories/trade_repository.dart';
import 'trade_event.dart';
import 'trade_state.dart';
class TradeBloc extends Bloc<TradeEvent, TradeState> {
final TradeRepository repository;
TradeBloc({required this.repository}) : super(TradeInitial()) {
on<FetchTrades>(_onFetchTrades);
on<CloseTrade>(_onCloseTrade);
on<AcceptTradeProposalEvent>(_onAcceptTradeProposal);
}
Future<void> _onFetchTrades(FetchTrades event, Emitter<TradeState> emit) async {
emit(TradeLoading());
try {
final trades = await repository.fetchTrades(isin: event.isin, status: event.status);
emit(TradeLoaded(trades));
} catch (e) {
emit(const TradeError("Trades konnten nicht geladen werden"));
}
}
Future<void> _onCloseTrade(CloseTrade event, Emitter<TradeState> emit) async {
emit(TradeLoading());
try {
await repository.closeTrade(event.tradeId);
final trades = await repository.fetchTrades();
emit(TradeLoaded(trades));
} catch (e) {
emit(const TradeError("Position konnte nicht geschlossen werden"));
}
}
Future<void> _onAcceptTradeProposal(AcceptTradeProposalEvent event, Emitter<TradeState> emit) async {
emit(TradeLoading());
try {
await repository.acceptTrade(event.dto);
final trades = await repository.fetchTrades();
emit(TradeLoaded(trades));
} catch (e) {
emit(const TradeError("Trade konnte nicht akzeptiert werden"));
}
}
}
@@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';
import '../models/trade_acceptance_dto.dart';
abstract class TradeEvent extends Equatable {
const TradeEvent();
@override
List<Object?> get props => [];
}
class FetchTrades extends TradeEvent {
final String? status;
final String? isin;
const FetchTrades({this.status, this.isin});
@override
List<Object?> get props => [status, isin];
}
class CloseTrade extends TradeEvent {
final String tradeId;
const CloseTrade(this.tradeId);
@override
List<Object?> get props => [tradeId];
}
class AcceptTradeProposalEvent extends TradeEvent {
final TradeAcceptanceDto dto;
const AcceptTradeProposalEvent(this.dto);
@override
List<Object?> get props => [dto];
}
@@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
import 'package:finlytic_app/features/trades/models/trade_model.dart';
abstract class TradeState extends Equatable {
const TradeState();
@override
List<Object?> get props => [];
}
class TradeInitial extends TradeState {}
class TradeLoading extends TradeState {}
class TradeLoaded extends TradeState {
final List<TradeModel> trades;
const TradeLoaded(this.trades);
@override
List<Object?> get props => [trades];
}
class TradeError extends TradeState {
final String message;
const TradeError(this.message);
@override
List<Object?> get props => [message];
}
@@ -0,0 +1,74 @@
class TradeAcceptanceDto {
final String userId;
final String tradeId;
final String analysisId;
final String isin;
final String symbol;
final double? actualEntryPrice;
final double? positionSize;
final double? leverageUsed;
final double? entryFee;
final double? exitFee;
final double? quantity;
final double? knockoutThreshold;
final bool isRecurring;
final DateTime executionTimestamp;
final String? signalType;
final double? entryPrice;
final double? stopLoss;
final double? takeProfit;
final String? instrumentType;
final String? timeframe;
final String? reasoning;
TradeAcceptanceDto({
required this.userId,
required this.tradeId,
this.analysisId = '',
required this.isin,
required this.symbol,
this.actualEntryPrice,
this.positionSize,
this.leverageUsed,
this.entryFee,
this.exitFee,
this.quantity,
this.knockoutThreshold,
this.isRecurring = false,
required this.executionTimestamp,
this.signalType,
this.entryPrice,
this.stopLoss,
this.takeProfit,
this.instrumentType,
this.timeframe,
this.reasoning,
});
Map<String, dynamic> toJson() {
return {
'userId': userId.isNotEmpty ? userId : 'default_user',
'tradeId': tradeId,
'analysisId': analysisId,
'isin': isin,
'symbol': symbol,
'actualEntryPrice': actualEntryPrice,
'positionSize': positionSize,
'leverageUsed': leverageUsed,
'entryFee': entryFee,
'exitFee': exitFee,
'quantity': quantity,
'knockoutThreshold': knockoutThreshold,
'isRecurring': isRecurring,
'executionTimestamp': executionTimestamp.toIso8601String(),
'signalType': signalType,
'entryPrice': entryPrice,
'stopLoss': stopLoss,
'takeProfit': takeProfit,
'instrumentType': instrumentType,
'timeframe': timeframe,
'reasoning': reasoning,
};
}
}
@@ -0,0 +1,237 @@
import 'package:equatable/equatable.dart';
class TradeModel extends Equatable {
final String id;
final String analysisId;
final String status;
final bool isGlobalProposal;
final String userId;
final String symbol;
final String isin;
final String companyName;
final String sector;
final String signalType;
final double entryPrice;
final double actualEntryPrice;
final double currentPrice;
final double stopLoss;
final double takeProfit;
final double positionSize;
final double leverageUsed;
final double pnlAbsolute;
final double pnlPercent;
final String reasoning;
final String technicalRationale;
final String fundamentalRationale;
final String riskWarning;
final double winRate;
final String timeframe;
final String instrumentType;
final DateTime? createdAt;
final String riskTolerance;
final double vixValue;
final String vixRegime;
final List<double> takeProfitTargets;
final double maxLeverage;
final double entryZoneMin;
final double entryZoneMax;
final double entryFee;
final double exitFee;
final double quantity;
const TradeModel({
required this.id,
this.analysisId = '',
this.status = 'Active',
this.isGlobalProposal = false,
this.userId = '',
required this.symbol,
this.isin = '',
this.companyName = '',
this.sector = '',
required this.signalType,
required this.entryPrice,
this.actualEntryPrice = 0.0,
this.currentPrice = 0.0,
required this.stopLoss,
required this.takeProfit,
this.positionSize = 0.0,
this.leverageUsed = 1.0,
this.pnlAbsolute = 0.0,
this.pnlPercent = 0.0,
this.reasoning = '',
this.technicalRationale = '',
this.fundamentalRationale = '',
this.riskWarning = '',
this.winRate = 50.0,
this.timeframe = '1D',
this.instrumentType = 'Stock',
this.createdAt,
this.riskTolerance = 'Moderate',
this.vixValue = 0.0,
this.vixRegime = 'Normal',
this.takeProfitTargets = const [],
this.maxLeverage = 1.0,
this.entryZoneMin = 0.0,
this.entryZoneMax = 0.0,
this.entryFee = 0.0,
this.exitFee = 0.0,
this.quantity = 0.0,
});
bool get isActive => status.toLowerCase() == 'active';
bool get isClosed => status.toLowerCase() == 'closed';
bool get isRejected => status.toLowerCase() == 'rejected';
bool get isProposed => (status.toLowerCase() == 'proposed' || isGlobalProposal) && !isRejected && !isActive && !isClosed;
double get effectiveCurrentPrice {
if (currentPrice > 0) return currentPrice;
if (actualEntryPrice > 0) return actualEntryPrice;
return entryPrice;
}
double get calculatedPnlAbs {
if (pnlAbsolute != 0) return pnlAbsolute;
final entry = actualEntryPrice > 0 ? actualEntryPrice : entryPrice;
final curr = effectiveCurrentPrice;
if (entry <= 0) return 0.0;
final isShort = signalType == 'SELL' || signalType == 'SHORT';
final rawMove = isShort ? ((entry - curr) / entry) : ((curr - entry) / entry);
final posSize = positionSize > 0 ? positionSize : entry;
final lev = leverageUsed > 0 ? leverageUsed : 1.0;
return (rawMove * posSize * lev);
}
double get calculatedPnlPct {
if (pnlPercent != 0) return pnlPercent;
final pnlAbs = calculatedPnlAbs;
final posSize = positionSize > 0 ? positionSize : (actualEntryPrice > 0 ? actualEntryPrice : entryPrice);
if (posSize <= 0) return 0.0;
return (pnlAbs / posSize) * 100.0;
}
factory TradeModel.fromJson(Map<String, dynamic> json) {
double parseDbl(dynamic val) {
if (val == null) return 0.0;
if (val is num) return val.toDouble();
return double.tryParse(val.toString()) ?? 0.0;
}
final idVal = (json['tradeId'] ?? json['TradeId'] ?? json['id'] ?? json['Id'])?.toString() ?? '';
final sig = (json['signalType'] ?? json['SignalType'] ?? json['side'] ?? json['Side'])?.toString() ?? 'BUY';
final entry = parseDbl(json['entryPrice'] ?? json['EntryPrice']);
final actualEntry = parseDbl(json['actualEntryPrice'] ?? json['ActualEntryPrice']);
final currPrice = parseDbl(json['currentPrice'] ?? json['CurrentPrice'] ?? json['price'] ?? json['Price']);
final sl = parseDbl(json['stopLoss'] ?? json['StopLoss']);
final tp = parseDbl(json['takeProfit'] ?? json['TakeProfit']);
final pnlAbs = parseDbl(json['pnlAbsolute'] ?? json['PnlAbsolute'] ?? json['pnl'] ?? json['Pnl']);
final pnlPct = parseDbl(json['pnlPercent'] ?? json['PnlPercent']);
DateTime? dt;
final createdStr = (json['createdAt'] ?? json['CreatedAt'])?.toString();
if (createdStr != null && createdStr.isNotEmpty) {
dt = DateTime.tryParse(createdStr);
}
return TradeModel(
id: idVal,
analysisId: (json['analysisId'] ?? json['AnalysisId'])?.toString() ?? '',
status: (json['status'] ?? json['Status'])?.toString() ?? 'Active',
isGlobalProposal: json['isGlobalProposal'] == true || json['IsGlobalProposal'] == true,
userId: (json['userId'] ?? json['UserId'])?.toString() ?? '',
symbol: (json['symbol'] ?? json['Symbol'])?.toString() ?? 'UNKNOWN',
isin: (json['isin'] ?? json['Isin'])?.toString() ?? '',
companyName: (json['companyName'] ?? json['CompanyName'])?.toString() ?? '',
sector: (json['sector'] ?? json['Sector'])?.toString() ?? '',
signalType: sig.toUpperCase(),
entryPrice: entry,
actualEntryPrice: actualEntry,
currentPrice: currPrice,
stopLoss: sl,
takeProfit: tp,
positionSize: parseDbl(json['positionSize'] ?? json['PositionSize']),
leverageUsed: parseDbl(json['leverageUsed'] ?? json['LeverageUsed']) == 0 ? 1.0 : parseDbl(json['leverageUsed'] ?? json['LeverageUsed']),
pnlAbsolute: pnlAbs,
pnlPercent: pnlPct,
reasoning: (json['reasoning'] ?? json['Reasoning'])?.toString() ?? '',
technicalRationale: (json['technicalRationale'] ?? json['TechnicalRationale'])?.toString() ?? '',
fundamentalRationale: (json['fundamentalRationale'] ?? json['FundamentalRationale'])?.toString() ?? '',
riskWarning: (json['riskWarning'] ?? json['RiskWarning'])?.toString() ?? '',
winRate: parseDbl(json['winRate'] ?? json['WinRate']),
timeframe: (json['timeframe'] ?? json['Timeframe'])?.toString() ?? '1D',
instrumentType: (json['instrumentType'] ?? json['InstrumentType'])?.toString() ?? 'Stock',
createdAt: dt,
riskTolerance: (json['riskTolerance'] ?? json['RiskTolerance'])?.toString() ?? 'Moderate',
vixValue: parseDbl(json['vixValue'] ?? json['VixValue']),
vixRegime: (json['vixRegime'] ?? json['VixRegime'])?.toString() ?? 'Normal',
takeProfitTargets: (json['takeProfitTargets'] ?? json['TakeProfitTargets']) is List
? ((json['takeProfitTargets'] ?? json['TakeProfitTargets']) as List).map((e) => parseDbl(e)).toList()
: [],
maxLeverage: parseDbl(json['maxLeverage'] ?? json['MaxLeverage']),
entryZoneMin: parseDbl(json['entryZoneMin'] ?? json['EntryZoneMin']),
entryZoneMax: parseDbl(json['entryZoneMax'] ?? json['EntryZoneMax']),
entryFee: parseDbl(json['entryFee'] ?? json['EntryFee']),
exitFee: parseDbl(json['exitFee'] ?? json['ExitFee']),
quantity: parseDbl(json['quantity'] ?? json['Quantity']),
);
}
Map<String, dynamic> toJson() {
return {
'tradeId': id,
'analysisId': analysisId,
'status': status,
'isGlobalProposal': isGlobalProposal,
'userId': userId,
'symbol': symbol,
'isin': isin,
'companyName': companyName,
'sector': sector,
'signalType': signalType,
'entryPrice': entryPrice,
'actualEntryPrice': actualEntryPrice,
'currentPrice': currentPrice,
'stopLoss': stopLoss,
'takeProfit': takeProfit,
'positionSize': positionSize,
'leverageUsed': leverageUsed,
'pnlAbsolute': pnlAbsolute,
'pnlPercent': pnlPercent,
'reasoning': reasoning,
'technicalRationale': technicalRationale,
'fundamentalRationale': fundamentalRationale,
'riskWarning': riskWarning,
'winRate': winRate,
'timeframe': timeframe,
'instrumentType': instrumentType,
'createdAt': createdAt?.toIso8601String(),
'riskTolerance': riskTolerance,
'vixValue': vixValue,
'vixRegime': vixRegime,
'takeProfitTargets': takeProfitTargets,
'maxLeverage': maxLeverage,
'entryZoneMin': entryZoneMin,
'entryZoneMax': entryZoneMax,
'entryFee': entryFee,
'exitFee': exitFee,
'quantity': quantity,
};
}
@override
List<Object?> get props => [
id,
analysisId,
status,
isGlobalProposal,
userId,
symbol,
isin,
signalType,
entryPrice,
currentPrice,
pnlAbsolute,
pnlPercent,
];
}
@@ -0,0 +1,43 @@
import 'dart:async';
import 'package:finlytic_app/core/network/api_client.dart';
import 'package:finlytic_app/features/trades/models/trade_model.dart';
import 'package:finlytic_app/features/trades/models/trade_acceptance_dto.dart';
class TradeRepository {
final ApiClient apiClient;
TradeRepository({required this.apiClient});
Future<List<TradeModel>> fetchTrades({String? isin, String? status}) async {
try {
final queryParams = <String, dynamic>{};
if (isin != null && isin.isNotEmpty) queryParams['isin'] = isin;
if (status != null && status.isNotEmpty) queryParams['status'] = status;
final response = await apiClient.get('/api/v1/trades', queryParameters: queryParams);
if (response.statusCode == 200 && response.data != null) {
final List<dynamic> data = response.data;
return data.map((json) => TradeModel.fromJson(json)).toList();
}
return [];
} catch (e) {
print('Error fetching trades: $e');
throw Exception('Trades konnten nicht geladen werden');
}
}
Future<void> acceptTrade(TradeAcceptanceDto dto) async {
final response = await apiClient.post('/api/v1/user/trades/accept', data: dto.toJson());
if (response.statusCode != 200) {
throw Exception('Trade konnte nicht akzeptiert werden');
}
}
Future<void> closeTrade(String id) async {
final response = await apiClient.post('/api/v1/user/trades/$id/close');
if (response.statusCode != 200) {
throw Exception('Trade konnte nicht geschlossen werden');
}
}
}
@@ -0,0 +1,406 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/network/api_client.dart';
import '../../../core/network/signalr_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../../auth/bloc/auth_bloc.dart';
import '../bloc/trade_bloc.dart';
import '../bloc/trade_event.dart';
import '../bloc/trade_state.dart';
import '../models/trade_model.dart';
import '../models/trade_acceptance_dto.dart';
import '../repositories/trade_repository.dart';
import '../widgets/trade_card.dart';
import '../widgets/proposed_auto_trades_card.dart';
import '../widgets/trade_acceptance_dialog.dart';
import '../widgets/trade_execution_dialog.dart';
class TradesFeedScreen extends StatelessWidget {
final ApiClient apiClient;
final SignalRService signalRService;
const TradesFeedScreen({
super.key,
required this.apiClient,
required this.signalRService,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TradeBloc(
repository: TradeRepository(apiClient: apiClient),
)..add(const FetchTrades()),
child: const _TradesFeedScreenContent(),
);
}
}
class _TradesFeedScreenContent extends StatefulWidget {
const _TradesFeedScreenContent();
@override
State<_TradesFeedScreenContent> createState() => _TradesFeedScreenContentState();
}
class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
String _selectedFilter = 'Alle'; // 'Alle', 'Offen', 'Vorschläge', 'Geschlossen'
String _searchQuery = '';
final TextEditingController _searchCtrl = TextEditingController();
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
Future<void> _handleAcceptProposal(BuildContext context, TradeModel trade) async {
final authState = context.read<AuthBloc>().state;
final currentUserId = (authState is Authenticated) ? authState.user.userId : 'default_user';
final result = await showDialog<TradeAcceptanceDto>(
context: context,
builder: (ctx) => TradeAcceptanceDialog(
trade: trade,
theme: AppTheme.darkClassic,
userId: currentUserId,
),
);
if (result != null && mounted) {
context.read<TradeBloc>().add(AcceptTradeProposalEvent(result));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Trade wird in dein Portfolio übernommen...'),
backgroundColor: AppTheme.primaryEmerald,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.darkBackground,
body: SafeArea(
child: RefreshIndicator(
onRefresh: () async {
context.read<TradeBloc>().add(const FetchTrades());
},
color: AppTheme.primaryEmerald,
backgroundColor: AppTheme.cardSurface,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title & Reload Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Live Portfolio & Trading',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 2),
Text(
'KI-Erkennungen, Vorschläge & Aktive Positionen',
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
),
],
),
IconButton(
onPressed: () {
context.read<TradeBloc>().add(const FetchTrades());
},
icon: const Icon(Icons.refresh, color: Colors.white70),
tooltip: 'Trades Aktualisieren',
),
],
),
const SizedBox(height: 16),
// Main Content Body
Expanded(
child: BlocBuilder<TradeBloc, TradeState>(
builder: (context, state) {
if (state is TradeLoading) {
return Center(
child: CircularProgressIndicator(color: AppTheme.primaryEmerald),
);
}
if (state is TradeError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: AppTheme.accentRed),
const SizedBox(height: 12),
Text(state.message, style: TextStyle(color: AppTheme.textMuted)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<TradeBloc>().add(const FetchTrades()),
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryEmerald),
child: const Text('Erneut Versuchen'),
),
],
),
);
}
if (state is TradeLoaded) {
final allTrades = state.trades;
// Separate proposals, active, closed, and rejected trades
final proposals = allTrades.where((t) => t.isProposed).toList();
final activeTrades = allTrades.where((t) => t.isActive).toList();
final closedTrades = allTrades.where((t) => t.isClosed).toList();
final rejectedTrades = allTrades.where((t) => t.isRejected).toList();
// Performance Header Calculations
final totalOpenPnlAbs = activeTrades.fold<double>(0, (sum, t) => sum + t.pnlAbsolute);
final isPnlPos = totalOpenPnlAbs >= 0;
final winRatePct = allTrades.isNotEmpty
? (allTrades.where((t) => t.pnlAbsolute >= 0).length / allTrades.length * 100)
: 0.0;
// Filter list according to tab & search
List<TradeModel> filteredList = allTrades;
if (_selectedFilter == 'Alle') {
filteredList = allTrades.where((t) => !t.isProposed).toList();
} else if (_selectedFilter == 'Offen') {
filteredList = activeTrades;
} else if (_selectedFilter == 'Vorschläge') {
filteredList = proposals;
} else if (_selectedFilter == 'Geschlossen') {
filteredList = closedTrades;
} else if (_selectedFilter == 'Abgelehnt') {
filteredList = rejectedTrades;
}
if (_searchQuery.trim().isNotEmpty) {
final q = _searchQuery.toLowerCase().trim();
filteredList = filteredList.where((t) =>
t.symbol.toLowerCase().contains(q) ||
t.isin.toLowerCase().contains(q) ||
t.companyName.toLowerCase().contains(q)).toList();
}
return ListView(
children: [
// 1. Performance Overview Bar
GlassContainer(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
margin: const EdgeInsets.only(bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_summaryStat('Offene Trades', '${activeTrades.length}', AppTheme.primaryEmerald),
_summaryStat(
'Offenes PnL',
'${isPnlPos ? '+' : ''}${totalOpenPnlAbs.toStringAsFixed(2)}',
isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed,
),
_summaryStat('Trefferquote', '${winRatePct.toStringAsFixed(0)}%', Colors.amber),
_summaryStat('Auto-Vorschläge', '${proposals.length}', AppTheme.accentCyan),
],
),
),
// 2. Featured Card: Auto KI Trade Proposals
ProposedAutoTradesCard(
proposals: proposals,
onAcceptProposal: (trade) => _handleAcceptProposal(context, trade),
),
// 3. Search & Filter Section
Row(
children: [
Expanded(
child: TextField(
controller: _searchCtrl,
onChanged: (val) {
setState(() {
_searchQuery = val;
});
},
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: InputDecoration(
hintText: 'Suche nach Symbol, ISIN oder Name...',
hintStyle: TextStyle(color: AppTheme.textMuted, fontSize: 13),
prefixIcon: const Icon(Icons.search, size: 18, color: Colors.white54),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
),
),
),
),
],
),
const SizedBox(height: 12),
// Filter Chips Row
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_filterChip('Alle', allTrades.length),
_filterChip('Offen', activeTrades.length),
_filterChip('Vorschläge', proposals.length),
_filterChip('Abgelehnt', rejectedTrades.length),
_filterChip('Geschlossen', closedTrades.length),
],
),
),
const SizedBox(height: 16),
// 4. Trades List
if (filteredList.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: Center(
child: Column(
children: [
Icon(Icons.inbox, size: 40, color: AppTheme.textMuted),
const SizedBox(height: 8),
Text(
'Keine Trades in der Kategorie "$_selectedFilter" gefunden.',
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
),
],
),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final trade = filteredList[index];
return TradeCard(
trade: trade,
onAccept: () => _handleAcceptProposal(context, trade),
onSettings: () => _showTradeSettingsDialog(context, trade),
onClose: () {
context.read<TradeBloc>().add(CloseTrade(trade.id));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Position wird geschlossen...')),
);
},
);
},
),
],
);
}
return const SizedBox.shrink();
},
),
),
],
),
),
),
),
);
}
Widget _summaryStat(String label, String value, Color valColor) {
return Column(
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
const SizedBox(height: 3),
Text(value, style: TextStyle(color: valColor, fontWeight: FontWeight.bold, fontSize: 15)),
],
);
}
Widget _filterChip(String label, int count) {
final isSelected = _selectedFilter == label;
return GestureDetector(
onTap: () {
setState(() {
_selectedFilter = label;
});
},
child: Container(
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryEmerald : Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? AppTheme.primaryEmerald : Colors.white.withValues(alpha: 0.12),
),
),
child: Row(
children: [
Text(
label,
style: TextStyle(
color: isSelected ? Colors.black : Colors.white,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSelected ? Colors.black.withValues(alpha: 0.2) : Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: TextStyle(
color: isSelected ? Colors.black : Colors.white70,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
void _showTradeSettingsDialog(BuildContext context, TradeModel trade) {
final tradeBloc = context.read<TradeBloc>();
TradeExecutionDialog.show(
context,
trade: trade,
defaultSymbol: trade.symbol,
isActive: true,
onAccept: (dto) {
tradeBloc.add(AcceptTradeProposalEvent(dto));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Einstellungen für ${trade.symbol} gespeichert.'),
backgroundColor: AppTheme.primaryEmerald,
),
);
},
);
}
}
@@ -0,0 +1,294 @@
import 'package:flutter/material.dart';
import 'dart:ui';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../models/trade_model.dart';
import 'trade_detail_modal.dart';
/// Premium Design for the AI Auto Screener Recommendations in the Live Trades Feed.
class ProposedAutoTradesCard extends StatelessWidget {
final List<TradeModel> proposals;
final Function(TradeModel) onAcceptProposal;
const ProposedAutoTradesCard({
super.key,
required this.proposals,
required this.onAcceptProposal,
});
@override
Widget build(BuildContext context) {
if (proposals.isEmpty) {
return GlassContainer(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(18),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.primaryEmerald.withValues(alpha: 0.15),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppTheme.primaryEmerald.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Icon(Icons.auto_awesome, color: AppTheme.primaryEmerald, size: 24),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'KI Screener Aktiv',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
),
const SizedBox(height: 4),
Text(
'Die KI durchsucht den Markt kontinuierlich nach hoch-profitablen Mustern. Aktuell keine neuen Empfehlungen.',
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
),
],
),
),
],
),
);
}
// Top proposal
final topProposal = proposals.first;
final isBuy = topProposal.signalType.toUpperCase() == 'BUY' || topProposal.signalType.toUpperCase() == 'LONG';
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
return Container(
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: signalColor.withValues(alpha: 0.15),
blurRadius: 30,
spreadRadius: -5,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1.5,
),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withValues(alpha: 0.08),
signalColor.withValues(alpha: 0.05),
],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Premium Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.primaryEmerald, AppTheme.accentCyan],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: AppTheme.accentCyan.withValues(alpha: 0.4), blurRadius: 12, spreadRadius: 2),
],
),
child: const Icon(Icons.bolt, color: Colors.white, size: 20),
),
const SizedBox(width: 12),
const Text(
'KI Asset Empfehlung',
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 16, color: Colors.white, letterSpacing: 0.5),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.amber.withValues(alpha: 0.5)),
boxShadow: [
BoxShadow(color: Colors.amber.withValues(alpha: 0.2), blurRadius: 8),
],
),
child: Row(
children: [
const Icon(Icons.auto_awesome, size: 14, color: Colors.amber),
const SizedBox(width: 6),
Text(
'Score: ${topProposal.winRate.toStringAsFixed(0)}%',
style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 12),
),
],
),
),
],
),
const SizedBox(height: 24),
// Asset Details with Modern Typography
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: signalColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: signalColor.withValues(alpha: 0.5)),
),
child: Text(
topProposal.signalType.toUpperCase(),
style: TextStyle(color: signalColor, fontWeight: FontWeight.w900, fontSize: 14, letterSpacing: 1),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
topProposal.symbol.isNotEmpty && topProposal.symbol != 'UNKNOWN'
? topProposal.symbol
: (topProposal.companyName.isNotEmpty && topProposal.companyName != 'UNKNOWN' ? topProposal.companyName : (topProposal.isin.isNotEmpty ? topProposal.isin : 'Aktie')),
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 24, color: Colors.white, height: 1.1),
),
if (topProposal.companyName.isNotEmpty && topProposal.companyName != topProposal.symbol && topProposal.companyName != 'UNKNOWN')
Text(
topProposal.companyName,
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
if (topProposal.reasoning.isNotEmpty) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.lightbulb_outline, size: 18, color: AppTheme.accentCyan.withValues(alpha: 0.8)),
const SizedBox(width: 12),
Expanded(
child: Text(
topProposal.reasoning,
style: const TextStyle(color: Colors.white70, fontSize: 13, height: 1.4),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
const SizedBox(height: 24),
// Premium Action Buttons
Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppTheme.accentCyan.withValues(alpha: 0.5)),
color: AppTheme.accentCyan.withValues(alpha: 0.1),
),
child: IconButton(
onPressed: () => TradeDetailModal.show(
context,
trade: topProposal,
onAccept: () => onAcceptProposal(topProposal),
),
icon: Icon(Icons.analytics_outlined, color: AppTheme.accentCyan, size: 22),
tooltip: 'KI-Analyse & Details',
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.primaryEmerald.withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: () => onAcceptProposal(topProposal),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.insights, size: 20, color: Colors.black87),
const SizedBox(width: 8),
Text(
'Trade Setup Erstellen',
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 15, letterSpacing: 0.2),
),
],
),
),
),
),
],
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../models/trade_acceptance_dto.dart';
import '../models/trade_model.dart';
class TradeAcceptanceDialog extends StatefulWidget {
final TradeModel trade;
final ThemePreset theme;
final String userId;
const TradeAcceptanceDialog({
super.key,
required this.trade,
required this.theme,
required this.userId,
});
@override
State<TradeAcceptanceDialog> createState() => _TradeAcceptanceDialogState();
}
class _TradeAcceptanceDialogState extends State<TradeAcceptanceDialog> {
late TextEditingController _entryPriceCtrl;
late TextEditingController _positionSizeCtrl;
late TextEditingController _leverageCtrl;
late TextEditingController _entryFeeCtrl;
late TextEditingController _exitFeeCtrl;
late TextEditingController _quantityCtrl;
late TextEditingController _stopLossCtrl;
late TextEditingController _takeProfitCtrl;
late TextEditingController _notesCtrl;
bool _isRecurring = false;
@override
void initState() {
super.initState();
_entryPriceCtrl = TextEditingController(text: widget.trade.entryPrice.toStringAsFixed(2));
_positionSizeCtrl = TextEditingController(text: '1000');
_leverageCtrl = TextEditingController(text: (widget.trade.maxLeverage > 0 ? widget.trade.maxLeverage : 1).toStringAsFixed(0));
_stopLossCtrl = TextEditingController(text: widget.trade.stopLoss.toStringAsFixed(2));
_takeProfitCtrl = TextEditingController(text: widget.trade.takeProfit.toStringAsFixed(2));
_notesCtrl = TextEditingController();
_entryFeeCtrl = TextEditingController(text: '0.00');
_exitFeeCtrl = TextEditingController(text: '0.00');
double price = widget.trade.entryPrice;
_quantityCtrl = TextEditingController(text: (1000 / (price > 0 ? price : 1)).toStringAsFixed(4));
}
@override
void dispose() {
_entryPriceCtrl.dispose();
_positionSizeCtrl.dispose();
_leverageCtrl.dispose();
_stopLossCtrl.dispose();
_takeProfitCtrl.dispose();
_notesCtrl.dispose();
_entryFeeCtrl.dispose();
_exitFeeCtrl.dispose();
_quantityCtrl.dispose();
super.dispose();
}
double? _parseNum(String text) {
final cleaned = text.replaceAll(',', '.').trim();
return double.tryParse(cleaned);
}
void _recalcQuantity() {
double posSize = _parseNum(_positionSizeCtrl.text) ?? 0;
double price = _parseNum(_entryPriceCtrl.text) ?? 0;
if (price > 0) {
_quantityCtrl.text = (posSize / price).toStringAsFixed(4);
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(16),
child: Container(
width: 450,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: widget.theme.cardSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: widget.theme.glassBorder, width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 20,
offset: const Offset(0, 10),
)
],
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.add_shopping_cart, color: widget.theme.primaryColor, size: 28),
const SizedBox(width: 12),
Expanded(
child: Text(
'Trade Akzeptieren - ${widget.trade.symbol}',
style: TextStyle(
color: widget.theme.textPrimary,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(Icons.close, color: widget.theme.textMuted),
)
],
),
const SizedBox(height: 24),
_buildInputField('Kaufkurs / Entry Price', _entryPriceCtrl, Icons.attach_money, onChanged: (_) => _recalcQuantity()),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildInputField('Kapital (Position Size)', _positionSizeCtrl, Icons.account_balance_wallet, onChanged: (_) => _recalcQuantity())),
const SizedBox(width: 16),
Expanded(child: _buildInputField('Stückzahl (Quantity)', _quantityCtrl, Icons.format_list_numbered)),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildInputField('Stop Loss', _stopLossCtrl, Icons.block)),
const SizedBox(width: 16),
Expanded(child: _buildInputField('Take Profit', _takeProfitCtrl, Icons.trending_up)),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(child: _buildInputField('Hebel (Leverage)', _leverageCtrl, Icons.speed)),
const SizedBox(width: 16),
Expanded(
child: CheckboxListTile(
title: Text('Sparplan', style: TextStyle(color: widget.theme.textPrimary, fontSize: 14)),
value: _isRecurring,
activeColor: widget.theme.primaryColor,
checkColor: Colors.white,
onChanged: (val) {
setState(() {
_isRecurring = val ?? false;
});
},
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
),
),
],
),
const SizedBox(height: 16),
_buildInputField('Notizen', _notesCtrl, Icons.edit),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
final dto = TradeAcceptanceDto(
userId: widget.userId,
tradeId: widget.trade.id,
analysisId: widget.trade.analysisId,
isin: widget.trade.isin,
symbol: widget.trade.symbol,
actualEntryPrice: _parseNum(_entryPriceCtrl.text),
positionSize: _parseNum(_positionSizeCtrl.text),
leverageUsed: _parseNum(_leverageCtrl.text) ?? 1.0,
entryFee: _parseNum(_entryFeeCtrl.text) ?? 0.0,
exitFee: _parseNum(_exitFeeCtrl.text) ?? 0.0,
quantity: _parseNum(_quantityCtrl.text),
isRecurring: _isRecurring,
executionTimestamp: DateTime.now().toUtc(),
signalType: widget.trade.signalType,
entryPrice: widget.trade.entryPrice,
stopLoss: widget.trade.stopLoss,
takeProfit: widget.trade.takeProfit,
instrumentType: widget.trade.instrumentType,
timeframe: widget.trade.timeframe,
reasoning: widget.trade.reasoning,
);
Navigator.of(context).pop(dto);
},
style: ElevatedButton.styleFrom(
backgroundColor: widget.theme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Jetzt Ausführen', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
),
),
),
);
}
Widget _buildInputField(String label, TextEditingController controller, IconData icon, {Function(String)? onChanged}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(color: widget.theme.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
TextField(
controller: controller,
style: TextStyle(color: widget.theme.textPrimary),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
onChanged: onChanged,
decoration: InputDecoration(
prefixIcon: Icon(icon, color: widget.theme.textMuted, size: 18),
filled: true,
fillColor: widget.theme.darkBackground.withValues(alpha: 0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
],
);
}
}
@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../models/trade_model.dart';
import 'trade_detail_modal.dart';
class TradeCard extends StatelessWidget {
final TradeModel trade;
final VoidCallback? onAccept;
final VoidCallback? onClose;
final VoidCallback? onSettings;
const TradeCard({
super.key,
required this.trade,
this.onAccept,
this.onClose,
this.onSettings,
});
@override
Widget build(BuildContext context) {
final isBuy = trade.signalType == 'BUY';
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
final isProposed = trade.isProposed;
final isActive = trade.isActive;
final isClosed = trade.isClosed;
final pnlAbs = trade.calculatedPnlAbs;
final pnlPct = trade.calculatedPnlPct;
final isPnlPos = pnlAbs >= 0;
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
final currPrice = trade.effectiveCurrentPrice;
return GestureDetector(
onTap: () => TradeDetailModal.show(
context,
trade: trade,
onAccept: onAccept,
onClose: onClose,
),
child: GlassContainer(
margin: const EdgeInsets.only(bottom: 14),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Row: Signal, Symbol, Status & Live PnL
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: signalColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: signalColor.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isBuy ? Icons.trending_up : Icons.trending_down,
size: 14,
color: signalColor,
),
const SizedBox(width: 4),
Text(
trade.signalType,
style: TextStyle(color: signalColor, fontWeight: FontWeight.bold, fontSize: 12),
),
],
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
trade.companyName.isNotEmpty && trade.companyName != 'UNKNOWN'
? trade.companyName
: (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' ? trade.symbol : (trade.isin.isNotEmpty ? trade.isin : 'Aktie')),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
),
if (trade.isin.isNotEmpty || (trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN' && trade.symbol != trade.companyName))
Text(
trade.isin.isNotEmpty ? trade.isin : trade.symbol,
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isActive || isClosed)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: pnlColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: pnlColor.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${isPnlPos ? '+' : ''}${pnlAbs.toStringAsFixed(2)}',
style: TextStyle(color: pnlColor, fontWeight: FontWeight.bold, fontSize: 14),
),
Text(
'${isPnlPos ? '+' : ''}${pnlPct.toStringAsFixed(2)}%',
style: TextStyle(color: pnlColor, fontSize: 11),
),
],
),
)
else if (trade.isRejected)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.accentRed.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.4)),
),
child: Text(
'ABGELEHNT',
style: TextStyle(color: AppTheme.accentRed, fontWeight: FontWeight.bold, fontSize: 10),
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
),
child: const Text(
'VORSCHLAG',
style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 10),
),
),
],
),
const SizedBox(height: 14),
// Price Metrics Grid with Live Kurs
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_priceItem(
isActive || isClosed ? 'Ausführung' : 'Ziel-Einstieg',
trade.actualEntryPrice > 0
? '${trade.actualEntryPrice.toStringAsFixed(2)}'
: (trade.entryPrice > 0 ? '${trade.entryPrice.toStringAsFixed(2)}' : '-'),
Colors.white
),
_priceItem('Live-Kurs', '${currPrice.toStringAsFixed(2)}', AppTheme.accentCyan),
_priceItem('Stop-Loss', '${trade.stopLoss.toStringAsFixed(2)}', AppTheme.accentRed),
_priceItem('Take-Profit', '${trade.takeProfit.toStringAsFixed(2)}', AppTheme.primaryEmerald),
],
),
),
if (trade.reasoning.isNotEmpty) ...[
const SizedBox(height: 10),
Text(
trade.reasoning,
style: TextStyle(color: AppTheme.textMuted, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
const SizedBox(height: 12),
// Footer Action Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${trade.instrumentType.isNotEmpty ? trade.instrumentType : "Stock"}${trade.timeframe.isNotEmpty ? trade.timeframe : "1D"}${trade.leverageUsed > 1 ? "${trade.leverageUsed.toStringAsFixed(0)}x Hebel" : ""}',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11),
),
Row(
children: [
IconButton(
onPressed: () => TradeDetailModal.show(
context,
trade: trade,
onAccept: onAccept,
onClose: onClose,
),
icon: Icon(Icons.info_outline, size: 18, color: AppTheme.accentCyan),
tooltip: 'KI-Begründung & Details',
),
if (isProposed && onAccept != null) ...[
const SizedBox(width: 6),
ElevatedButton.icon(
onPressed: onAccept,
icon: const Icon(Icons.check_circle_outline, size: 16),
label: const Text('Trade Übernehmen'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
],
if (isActive && onClose != null) ...[
const SizedBox(width: 6),
OutlinedButton.icon(
onPressed: onClose,
icon: Icon(Icons.close, size: 14, color: AppTheme.accentRed),
label: Text('Position Schließen', style: TextStyle(color: AppTheme.accentRed, fontSize: 12)),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppTheme.accentRed.withValues(alpha: 0.5)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
],
if (isActive && onSettings != null) ...[
const SizedBox(width: 6),
IconButton(
onPressed: onSettings,
icon: const Icon(Icons.settings, size: 16, color: Colors.white),
tooltip: 'Einstellungen',
style: IconButton.styleFrom(
backgroundColor: AppTheme.glassSurface,
),
),
],
],
),
],
),
],
),
),
);
}
Widget _priceItem(String label, String val, Color valColor) {
return Column(
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 10)),
const SizedBox(height: 2),
Text(val, style: TextStyle(color: valColor, fontWeight: FontWeight.bold, fontSize: 12)),
],
);
}
}
@@ -0,0 +1,401 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../models/trade_model.dart';
class TradeDetailModal extends StatelessWidget {
final TradeModel trade;
final VoidCallback? onAccept;
final VoidCallback? onClose;
const TradeDetailModal({
super.key,
required this.trade,
this.onAccept,
this.onClose,
});
static Future<void> show(
BuildContext context, {
required TradeModel trade,
VoidCallback? onAccept,
VoidCallback? onClose,
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => TradeDetailModal(
trade: trade,
onAccept: onAccept,
onClose: onClose,
),
);
}
@override
Widget build(BuildContext context) {
final isBuy = trade.signalType == 'BUY';
final signalColor = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
final pnlAbs = trade.calculatedPnlAbs;
final pnlPct = trade.calculatedPnlPct;
final isPnlPos = pnlAbs >= 0;
final pnlColor = isPnlPos ? AppTheme.primaryEmerald : AppTheme.accentRed;
final currPrice = trade.effectiveCurrentPrice;
return Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(color: Colors.white.withValues(alpha: 0.1)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.6),
blurRadius: 25,
spreadRadius: 5,
)
],
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle Bar
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(2),
),
),
// Modal Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: signalColor.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: signalColor.withValues(alpha: 0.5)),
),
child: Text(
trade.signalType,
style: TextStyle(color: signalColor, fontWeight: FontWeight.bold, fontSize: 14),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
trade.symbol.isNotEmpty && trade.symbol != 'UNKNOWN'
? trade.symbol
: (trade.companyName.isNotEmpty && trade.companyName != 'UNKNOWN' ? trade.companyName : (trade.isin.isNotEmpty ? trade.isin : 'Aktie')),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.white),
),
if (trade.companyName.isNotEmpty && trade.companyName != trade.symbol && trade.companyName != 'UNKNOWN')
Text(
trade.companyName,
style: TextStyle(color: AppTheme.textMuted, fontSize: 13),
),
if (trade.isin.isNotEmpty || trade.sector.isNotEmpty)
Text(
'${trade.isin.isNotEmpty ? trade.isin : ""}${trade.isin.isNotEmpty && trade.sector.isNotEmpty ? "" : ""}${trade.sector.isNotEmpty ? trade.sector : ""}',
style: TextStyle(color: AppTheme.textMuted.withValues(alpha: 0.7), fontSize: 11),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
),
child: Row(
children: [
const Icon(Icons.star, size: 14, color: Colors.amber),
const SizedBox(width: 4),
Text(
'${trade.winRate.toStringAsFixed(0)}% Win-Rate',
style: const TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontSize: 11),
),
],
),
),
],
),
),
const SizedBox(height: 16),
const Divider(color: Colors.white10, height: 1),
// Scrollable Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Price Grid
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_metricItem(
trade.isActive || trade.isClosed ? 'Ausführung' : 'Ziel-Einstieg',
trade.actualEntryPrice > 0
? '${trade.actualEntryPrice.toStringAsFixed(2)}'
: (trade.entryPrice > 0 ? '${trade.entryPrice.toStringAsFixed(2)}' : '-'),
Colors.white
),
_metricItem('Live-Kurs', '${currPrice.toStringAsFixed(2)}', AppTheme.accentCyan),
_metricItem('Stop-Loss', '${trade.stopLoss.toStringAsFixed(2)}', AppTheme.accentRed),
_metricItem('Take-Profit', '${trade.takeProfit.toStringAsFixed(2)}', AppTheme.primaryEmerald),
],
),
),
if (trade.isActive || trade.isClosed) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: pnlColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: pnlColor.withValues(alpha: 0.3)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Aktueller PnL:', style: TextStyle(color: Colors.white70, fontSize: 13)),
Text(
'${isPnlPos ? '+' : ''}${pnlAbs.toStringAsFixed(2)} € (${isPnlPos ? '+' : ''}${pnlPct.toStringAsFixed(2)}%)',
style: TextStyle(color: pnlColor, fontWeight: FontWeight.bold, fontSize: 15),
),
],
),
),
],
const SizedBox(height: 20),
// AI Reasoning Section
if (trade.reasoning.isNotEmpty) ...[
_sectionTitle(Icons.auto_awesome, 'KI-Gesamteinschätzung & Begründung', AppTheme.primaryEmerald),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.primaryEmerald.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.primaryEmerald.withValues(alpha: 0.2)),
),
child: Text(
trade.reasoning,
style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.4),
),
),
const SizedBox(height: 18),
],
// Technical Rationale Section
if (trade.technicalRationale.isNotEmpty) ...[
_sectionTitle(Icons.show_chart, 'Technische Analyse & Indikatoren', AppTheme.accentCyan),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: Text(
trade.technicalRationale,
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
),
),
const SizedBox(height: 18),
],
// Fundamental Rationale Section
if (trade.fundamentalRationale.isNotEmpty) ...[
_sectionTitle(Icons.account_balance, 'Fundamentale Bewertung', Colors.purpleAccent),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.03),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: Text(
trade.fundamentalRationale,
style: TextStyle(color: AppTheme.textMuted, fontSize: 13, height: 1.4),
),
),
const SizedBox(height: 18),
],
// Risk Warning Section
if (trade.riskWarning.isNotEmpty) ...[
_sectionTitle(Icons.warning_amber_rounded, 'Risikohinweis & Marktumfeld', AppTheme.accentRed),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.accentRed.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.accentRed.withValues(alpha: 0.3)),
),
child: Text(
trade.riskWarning,
style: TextStyle(color: AppTheme.accentRed, fontSize: 12, height: 1.4),
),
),
const SizedBox(height: 18),
],
// Trade Parameters Grid
_sectionTitle(Icons.tune, 'Trade-Parameter & Instrument', Colors.white70),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.02),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
),
child: Column(
children: [
_paramRow('Instrument Typ:', trade.instrumentType.isNotEmpty ? trade.instrumentType : 'Stock'),
_paramRow('Zeithorizont:', trade.timeframe.isNotEmpty ? trade.timeframe : '1D'),
if (trade.leverageUsed > 1) _paramRow('Hebel:', '${trade.leverageUsed.toStringAsFixed(0)}x'),
if (trade.positionSize > 0) _paramRow('Positionsgröße:', '${trade.positionSize.toStringAsFixed(2)}'),
],
),
),
],
),
),
),
// Footer Action Bar
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.white.withValues(alpha: 0.2)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Schließen', style: TextStyle(color: Colors.white70)),
),
),
if (trade.isProposed && onAccept != null) ...[
const SizedBox(width: 12),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
onAccept!();
},
icon: const Icon(Icons.check_circle_outline, size: 18),
label: const Text('Trade Übernehmen'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
if (trade.isActive && onClose != null) ...[
const SizedBox(width: 12),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
onClose!();
},
icon: const Icon(Icons.close, size: 18),
label: const Text('Position Schließen'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentRed,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
],
),
),
],
),
),
);
}
Widget _sectionTitle(IconData icon, String title, Color color) {
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13),
),
],
);
}
Widget _metricItem(String label, String value, Color color) {
return Column(
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
const SizedBox(height: 4),
Text(value, style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 13)),
],
);
}
Widget _paramRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 12)),
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
],
),
);
}
}
@@ -0,0 +1,419 @@
import 'package:flutter/material.dart';
import 'package:finlytic_app/core/theme/app_theme.dart';
import 'package:finlytic_app/core/widgets/status_badge.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 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 * initLev) / initEntry : 10.0;
// We don't have a direct quantity field in TradeModel, but we calculate it.
// Let's use the explicit quantity if it exists, otherwise calculate it
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());
void recalculateQuantity() {
final entry = double.tryParse(actualEntryController.text) ?? 0.0;
final posSize = double.tryParse(positionSizeController.text) ?? 0.0;
final lev = double.tryParse(leverageController.text) ?? 1.0;
if (entry > 0 && posSize > 0) {
final q = (posSize * lev) / entry;
quantityController.text = q.toStringAsFixed(4);
}
}
actualEntryController.addListener(recalculateQuantity);
positionSizeController.addListener(recalculateQuantity);
leverageController.addListener(recalculateQuantity);
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(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(14),
decoration: BoxDecoration(
color: signalColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: signalColor, width: 1.5),
),
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.toString(), style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)),
),
],
),
Row(
children: [
if (trade.winRate > 0) ...[
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: 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),
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 (Autom. berechnet)', 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)
ElevatedButton.icon(
onPressed: () {
final dto = TradeAcceptanceDto(
userId: trade.userId,
tradeId: trade.id,
analysisId: trade.analysisId,
isin: trade.isin,
symbol: trade.symbol,
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
quantity: double.tryParse(quantityController.text) ?? 0.0,
executionTimestamp: DateTime.now().toUtc(),
signalType: trade.signalType,
entryPrice: trade.entryPrice,
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
instrumentType: trade.instrumentType,
timeframe: trade.timeframe,
reasoning: trade.reasoning,
);
onAccept(dto);
Navigator.pop(dialogContext);
},
icon: const Icon(Icons.save, size: 16),
label: const Text('Speichern'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.black,
),
)
else ...[
if (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),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
final dto = TradeAcceptanceDto(
userId: trade.userId,
tradeId: trade.id,
analysisId: trade.analysisId,
isin: trade.isin,
symbol: trade.symbol,
actualEntryPrice: double.tryParse(actualEntryController.text) ?? trade.entryPrice,
positionSize: double.tryParse(positionSizeController.text) ?? 1000.0,
leverageUsed: double.tryParse(leverageController.text) ?? 1.0,
entryFee: double.tryParse(entryFeeController.text) ?? 0.0,
exitFee: double.tryParse(exitFeeController.text) ?? 0.0,
quantity: double.tryParse(quantityController.text) ?? 0.0,
executionTimestamp: DateTime.now().toUtc(),
signalType: trade.signalType,
entryPrice: trade.entryPrice,
stopLoss: double.tryParse(slController.text) ?? trade.stopLoss,
takeProfit: double.tryParse(tpController.text) ?? trade.takeProfit,
instrumentType: trade.instrumentType,
timeframe: trade.timeframe,
reasoning: trade.reasoning,
);
onAccept(dto);
Navigator.of(dialogContext).pop();
},
icon: const Icon(Icons.check_circle, size: 16),
label: const Text('Trade Annehmen & Ausführen'),
style: ElevatedButton.styleFrom(
backgroundColor: 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: 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)),
],
),
);
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
/// Trade Performance Metrics header evaluating win rates, CRV, and potential.
class TradePerformanceHeader extends StatelessWidget {
const TradePerformanceHeader({super.key});
@override
Widget build(BuildContext context) {
return GlassContainer(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_metricColumn('Trefferquote (Win Rate)', '78.4%', AppTheme.primaryEmerald),
_metricColumn('Risiko-Ertrag (CRV)', '1 : 2.85', AppTheme.accentCyan),
_metricColumn('Ø Potenzial', '+14.2%', AppTheme.primaryEmerald),
_metricColumn('Signale 30T', '42 Active', AppTheme.textPrimary),
],
),
);
}
Widget _metricColumn(String label, String value, Color valueColor) {
return Column(
children: [
Text(label, style: TextStyle(color: AppTheme.textMuted, fontSize: 11)),
const SizedBox(height: 4),
Text(value, style: TextStyle(color: valueColor, fontWeight: FontWeight.bold, fontSize: 16)),
],
);
}
}