336 lines
14 KiB
Dart
336 lines
14 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 '../../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';
|
|
import '../widgets/trade_performance_bar.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 = 'Offen';
|
|
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: [
|
|
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),
|
|
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;
|
|
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();
|
|
|
|
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: [
|
|
TradePerformanceBar(
|
|
activeTrades: activeTrades,
|
|
allTrades: allTrades,
|
|
proposals: proposals,
|
|
),
|
|
ProposedAutoTradesCard(
|
|
proposals: proposals,
|
|
onAcceptProposal: (trade) => _handleAcceptProposal(context, trade),
|
|
),
|
|
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),
|
|
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),
|
|
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 _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,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|