feat(app): clean architecture with typed repositories, DTO models and calendar event logos
This commit is contained in:
@@ -3,9 +3,7 @@ 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';
|
||||
@@ -16,6 +14,7 @@ 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;
|
||||
@@ -46,7 +45,7 @@ class _TradesFeedScreenContent extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
String _selectedFilter = 'Offen'; // 'Alle', 'Offen', 'Vorschläge', 'Geschlossen'
|
||||
String _selectedFilter = 'Offen';
|
||||
String _searchQuery = '';
|
||||
final TextEditingController _searchCtrl = TextEditingController();
|
||||
|
||||
@@ -96,7 +95,6 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title & Reload Row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -115,25 +113,18 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.read<TradeBloc>().add(const FetchTrades());
|
||||
},
|
||||
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),
|
||||
);
|
||||
return Center(child: CircularProgressIndicator(color: AppTheme.primaryEmerald));
|
||||
}
|
||||
|
||||
if (state is TradeError) {
|
||||
@@ -157,21 +148,11 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
|
||||
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.calculatedPnlAbs);
|
||||
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();
|
||||
@@ -195,42 +176,21 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
TradePerformanceBar(
|
||||
activeTrades: activeTrades,
|
||||
allTrades: allTrades,
|
||||
proposals: proposals,
|
||||
),
|
||||
|
||||
// 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;
|
||||
});
|
||||
},
|
||||
onChanged: (val) => setState(() => _searchQuery = val),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Suche nach Symbol, ISIN oder Name...',
|
||||
@@ -239,23 +199,14 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
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)),
|
||||
),
|
||||
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(
|
||||
@@ -268,10 +219,7 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. Trades List
|
||||
if (filteredList.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||
@@ -280,10 +228,7 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
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),
|
||||
),
|
||||
Text('Keine Trades in der Kategorie "$_selectedFilter" gefunden.', style: TextStyle(color: AppTheme.textMuted, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -324,33 +269,17 @@ class _TradesFeedScreenContentState extends State<_TradesFeedScreenContent> {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
},
|
||||
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),
|
||||
),
|
||||
border: Border.all(color: isSelected ? AppTheme.primaryEmerald : Colors.white.withValues(alpha: 0.12)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user