feat(app): clean architecture with typed repositories, DTO models and calendar event logos

This commit is contained in:
2026-08-15 01:03:22 +02:00
parent f08fecde23
commit 15f8f7896e
34 changed files with 1435 additions and 1420 deletions
@@ -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: [