Files
Finlytic/FinlyticApp/lib/features/trades/views/trades_feed_screen.dart
T

407 lines
17 KiB
Dart

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,
),
);
},
);
}
}