@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/screens/crypto_account_screen.dart +169 -272
- package/lib/screens/investment_account_screen.dart +203 -460
- package/lib/screens/rrsp_sun_life_screen.dart +121 -216
- package/lib/viewmodels/crypto_account_viewmodel.dart +192 -0
- package/lib/viewmodels/investment_account_viewmodel.dart +272 -0
- package/lib/viewmodels/rrsp_sun_life_viewmodel.dart +103 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.2...v1.2.0) (2025-12-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add totals back ([0abb920](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/0abb920257d8988083f08a24f8db90ad9a24d463))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* new dividend logic ([5aed0ed](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/5aed0edd789072894c927192fb2105dd7cf1623b))
|
|
12
|
+
* update dividend ([67c9dc3](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/67c9dc327a15e114c7a4acc590f797e1f89ac2ed))
|
|
13
|
+
|
|
14
|
+
## [1.1.2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.1...v1.1.2) (2025-12-03)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* refatoração das telas InvestmentAccountScreen, CryptoAccountScreen e RrspSunLifeScreen para o padrão ViewModel com ChangeNotifier ([20419ac](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/20419ac0c38259469c69389496ec7e24c6d95e3b))
|
|
20
|
+
|
|
1
21
|
## [1.1.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.0...v1.1.1) (2025-12-02)
|
|
2
22
|
|
|
3
23
|
|
|
@@ -1,240 +1,123 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:intl/intl.dart';
|
|
3
|
-
import 'package:personal_finance_frontend_core_services/services/crypto_service.dart';
|
|
4
|
-
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
5
|
-
import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart';
|
|
6
|
-
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
7
3
|
import 'package:provider/provider.dart';
|
|
8
4
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
9
5
|
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart';
|
|
7
|
+
import '../viewmodels/crypto_account_viewmodel.dart';
|
|
10
8
|
|
|
11
|
-
class CryptoAccountScreen extends
|
|
9
|
+
class CryptoAccountScreen extends StatelessWidget {
|
|
12
10
|
final String accountName;
|
|
13
11
|
|
|
14
|
-
const CryptoAccountScreen({Key? key, required this.accountName})
|
|
15
|
-
|
|
16
|
-
@override
|
|
17
|
-
_CryptoAccountScreenState createState() => _CryptoAccountScreenState();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
21
|
-
final TransactionService _transactionService = TransactionService();
|
|
22
|
-
final CryptoService _cryptoService = CryptoService();
|
|
23
|
-
|
|
24
|
-
List<Map<String, dynamic>> _trades = [];
|
|
25
|
-
List<Map<String, dynamic>> _assets = [];
|
|
26
|
-
List<Map<String, dynamic>> _portfolioSummary = [];
|
|
27
|
-
Map<String, double> _livePrices = {};
|
|
28
|
-
double _cashBalance = 0.0;
|
|
29
|
-
double _accountTotalValue = 0.0;
|
|
30
|
-
double _totalPortfolioBookCost = 0.0;
|
|
31
|
-
bool _isLoading = true;
|
|
32
|
-
bool _isFetchingPrices = false;
|
|
33
|
-
String? _selectedSymbolForFilter;
|
|
34
|
-
String? _token;
|
|
35
|
-
|
|
36
|
-
@override
|
|
37
|
-
void initState() {
|
|
38
|
-
super.initState();
|
|
39
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
40
|
-
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
41
|
-
setState(() {
|
|
42
|
-
_token = authProvider.token;
|
|
43
|
-
});
|
|
44
|
-
_fetchData();
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
String currencySymbol = r'$';
|
|
49
|
-
|
|
50
|
-
String _formatCurrency(double value) {
|
|
51
|
-
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
Future<void> _fetchData() async {
|
|
55
|
-
if (_token == null) return;
|
|
56
|
-
setState(() => _isLoading = true);
|
|
57
|
-
try {
|
|
58
|
-
final futures = <Future>[
|
|
59
|
-
_transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
|
|
60
|
-
_transactionService.getAccountBalance(widget.accountName, token: _token),
|
|
61
|
-
_transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
|
|
62
|
-
_transactionService.getTotalPortfolioBookCost(token: _token), // Fetch total cost
|
|
63
|
-
];
|
|
64
|
-
final results = await Future.wait(futures);
|
|
65
|
-
|
|
66
|
-
final trades = results[0] as List<Map<String, dynamic>>;
|
|
67
|
-
final balance = results[1] as double;
|
|
68
|
-
final assets = results[2] as List<Map<String, dynamic>>;
|
|
69
|
-
final totalPortfolioBookCost = results[3] as double;
|
|
70
|
-
|
|
71
|
-
final summaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
72
|
-
|
|
73
|
-
setState(() {
|
|
74
|
-
_trades = trades;
|
|
75
|
-
_cashBalance = balance;
|
|
76
|
-
_assets = assets;
|
|
77
|
-
_portfolioSummary = summaryData['summary'];
|
|
78
|
-
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
79
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (mounted) await _fetchLivePrices();
|
|
83
|
-
} catch (e) {
|
|
84
|
-
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
85
|
-
} finally {
|
|
86
|
-
if (mounted) setState(() => _isLoading = false);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Future<void> _fetchLivePrices() async {
|
|
91
|
-
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
92
|
-
setState(() => _isFetchingPrices = true);
|
|
93
|
-
|
|
94
|
-
final idsToFetch = _portfolioSummary.map((p) => p['id_crypto'] as String? ?? p['symbol'] as String).where((s) => s.isNotEmpty).toList();
|
|
95
|
-
|
|
96
|
-
if (idsToFetch.isEmpty) {
|
|
97
|
-
setState(() => _isFetchingPrices = false);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
final livePrices = await _cryptoService.getLiveCryptoPrices(idsToFetch);
|
|
102
|
-
|
|
103
|
-
if (!mounted) return;
|
|
104
|
-
|
|
105
|
-
double newTotalValue = _cashBalance;
|
|
106
|
-
for (final position in _portfolioSummary) {
|
|
107
|
-
final shares = double.parse(position['shares'].toString());
|
|
108
|
-
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
109
|
-
final livePrice = livePrices[idForLookup];
|
|
110
|
-
if (livePrice != null) {
|
|
111
|
-
newTotalValue += shares * livePrice;
|
|
112
|
-
} else {
|
|
113
|
-
newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
setState(() {
|
|
118
|
-
_livePrices = livePrices;
|
|
119
|
-
_accountTotalValue = newTotalValue;
|
|
120
|
-
_isFetchingPrices = false;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
|
|
125
|
-
Map<String, dynamic> summary = {};
|
|
126
|
-
double accountPortfolioBookCost = 0;
|
|
127
|
-
|
|
128
|
-
for (var trade in trades) {
|
|
129
|
-
final asset = trade['asset'];
|
|
130
|
-
if (asset == null || asset['symbol'] == null) continue;
|
|
131
|
-
String symbol = asset['symbol'];
|
|
132
|
-
String idCrypto = asset['id_crypto'] ?? symbol;
|
|
133
|
-
|
|
134
|
-
double shares = double.parse(trade['shares'].toString());
|
|
135
|
-
double price = double.parse(trade['price'].toString());
|
|
136
|
-
String tradeType = trade['trade_type'];
|
|
137
|
-
|
|
138
|
-
if (!summary.containsKey(symbol)) {
|
|
139
|
-
summary[symbol] = {
|
|
140
|
-
'symbol': symbol,
|
|
141
|
-
'id_crypto': idCrypto,
|
|
142
|
-
'name': asset['name'] ?? symbol,
|
|
143
|
-
'shares': 0.0,
|
|
144
|
-
'total_cost': 0.0,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (tradeType == 'buy') {
|
|
149
|
-
summary[symbol]['shares'] += shares;
|
|
150
|
-
summary[symbol]['total_cost'] += shares * price;
|
|
151
|
-
} else if (tradeType == 'sell') {
|
|
152
|
-
double originalShares = summary[symbol]['shares'];
|
|
153
|
-
if (originalShares > 0) {
|
|
154
|
-
double avgPrice = summary[symbol]['total_cost'] / originalShares;
|
|
155
|
-
summary[symbol]['total_cost'] -= shares * avgPrice;
|
|
156
|
-
}
|
|
157
|
-
summary[symbol]['shares'] -= shares;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
162
|
-
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
163
|
-
|
|
164
|
-
List<Map<String, dynamic>> result = [];
|
|
165
|
-
summary.forEach((symbol, data) {
|
|
166
|
-
double shares = data['shares'];
|
|
167
|
-
double totalCost = data['total_cost'];
|
|
168
|
-
data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
|
|
169
|
-
data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
|
|
170
|
-
data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
171
|
-
result.add(data);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
return {'summary': result, 'total_value': accountPortfolioBookCost};
|
|
175
|
-
}
|
|
176
|
-
|
|
12
|
+
const CryptoAccountScreen({Key? key, required this.accountName})
|
|
13
|
+
: super(key: key);
|
|
177
14
|
|
|
178
15
|
@override
|
|
179
16
|
Widget build(BuildContext context) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
padding: const EdgeInsets.only(right: 16.0),
|
|
187
|
-
child: Column(
|
|
188
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
189
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
190
|
-
children: [
|
|
191
|
-
Text('Total Portfolio: ${_formatCurrency(_accountTotalValue)}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
192
|
-
Text('Account Balance: ${_formatCurrency(_cashBalance)}', style: const TextStyle(fontSize: 12)),
|
|
193
|
-
],
|
|
194
|
-
),
|
|
195
|
-
),
|
|
196
|
-
),
|
|
197
|
-
IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchData, tooltip: 'Refresh Data'),
|
|
198
|
-
],
|
|
17
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
18
|
+
|
|
19
|
+
return ChangeNotifierProvider(
|
|
20
|
+
create: (_) => CryptoAccountViewModel(
|
|
21
|
+
accountName: accountName,
|
|
22
|
+
token: authProvider.token,
|
|
199
23
|
),
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
24
|
+
child: Consumer<CryptoAccountViewModel>(
|
|
25
|
+
builder: (context, viewModel, child) {
|
|
26
|
+
return Scaffold(
|
|
27
|
+
appBar: AppBar(
|
|
28
|
+
title: Text('$accountName Portfolio'),
|
|
29
|
+
actions: [
|
|
30
|
+
Center(
|
|
31
|
+
child: Padding(
|
|
32
|
+
padding: const EdgeInsets.only(right: 16.0),
|
|
33
|
+
child: Column(
|
|
34
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
35
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
212
36
|
children: [
|
|
213
|
-
Text(
|
|
214
|
-
|
|
37
|
+
Text(
|
|
38
|
+
'Total Portfolio: ${_formatCurrency(viewModel.accountTotalValue)}',
|
|
39
|
+
style: const TextStyle(
|
|
40
|
+
fontSize: 16, fontWeight: FontWeight.bold),
|
|
41
|
+
),
|
|
42
|
+
Text(
|
|
43
|
+
'Account Balance: ${_formatCurrency(viewModel.cashBalance)}',
|
|
44
|
+
style: const TextStyle(fontSize: 12),
|
|
45
|
+
),
|
|
215
46
|
],
|
|
216
47
|
),
|
|
217
|
-
|
|
218
|
-
const SizedBox(height: 24),
|
|
219
|
-
CryptoTradeForm(
|
|
220
|
-
accountName: widget.accountName,
|
|
221
|
-
portfolioSummary: _portfolioSummary,
|
|
222
|
-
onTradeCreated: (_) => _fetchData(),
|
|
223
|
-
token: _token),
|
|
224
|
-
const SizedBox(height: 24),
|
|
225
|
-
Text('Trade History', style: Theme.of(context).textTheme.titleLarge),
|
|
226
|
-
const SizedBox(height: 8),
|
|
227
|
-
_buildAnaliticoTable(),
|
|
228
|
-
],
|
|
48
|
+
),
|
|
229
49
|
),
|
|
230
|
-
|
|
50
|
+
IconButton(
|
|
51
|
+
icon: const Icon(Icons.refresh),
|
|
52
|
+
onPressed: viewModel.fetchData,
|
|
53
|
+
tooltip: 'Refresh Data',
|
|
54
|
+
),
|
|
55
|
+
],
|
|
231
56
|
),
|
|
57
|
+
body: viewModel.isLoading
|
|
58
|
+
? const Center(child: CircularProgressIndicator())
|
|
59
|
+
: RefreshIndicator(
|
|
60
|
+
onRefresh: viewModel.fetchData,
|
|
61
|
+
child: SingleChildScrollView(
|
|
62
|
+
physics: const AlwaysScrollableScrollPhysics(),
|
|
63
|
+
padding: const EdgeInsets.all(16.0),
|
|
64
|
+
child: Column(
|
|
65
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
66
|
+
children: [
|
|
67
|
+
Row(
|
|
68
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
69
|
+
children: [
|
|
70
|
+
Text('Portfolio Summary',
|
|
71
|
+
style:
|
|
72
|
+
Theme.of(context).textTheme.titleLarge),
|
|
73
|
+
if (viewModel.isFetchingPrices)
|
|
74
|
+
const SizedBox(
|
|
75
|
+
height: 20,
|
|
76
|
+
width: 20,
|
|
77
|
+
child: CircularProgressIndicator(
|
|
78
|
+
strokeWidth: 2.0),
|
|
79
|
+
),
|
|
80
|
+
],
|
|
81
|
+
),
|
|
82
|
+
_buildSinteticoTable(context, viewModel),
|
|
83
|
+
const SizedBox(height: 24),
|
|
84
|
+
CryptoTradeForm(
|
|
85
|
+
accountName: accountName,
|
|
86
|
+
portfolioSummary: viewModel.portfolioSummary,
|
|
87
|
+
onTradeCreated: (_) => viewModel.fetchData(),
|
|
88
|
+
token: viewModel.token,
|
|
89
|
+
),
|
|
90
|
+
const SizedBox(height: 24),
|
|
91
|
+
Text('Trade History',
|
|
92
|
+
style: Theme.of(context).textTheme.titleLarge),
|
|
93
|
+
const SizedBox(height: 8),
|
|
94
|
+
_buildAnaliticoTable(context, viewModel),
|
|
95
|
+
],
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
),
|
|
232
102
|
);
|
|
233
103
|
}
|
|
234
104
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
105
|
+
String _formatCurrency(double value) {
|
|
106
|
+
String currencySymbol = r'$';
|
|
107
|
+
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Widget _buildSinteticoTable(
|
|
111
|
+
BuildContext context, CryptoAccountViewModel viewModel) {
|
|
112
|
+
if (viewModel.portfolioSummary.isEmpty) {
|
|
113
|
+
return const Center(
|
|
114
|
+
child: Padding(
|
|
115
|
+
padding: EdgeInsets.all(16.0),
|
|
116
|
+
child: Text('No positions held.'),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
238
121
|
return SingleChildScrollView(
|
|
239
122
|
scrollDirection: Axis.horizontal,
|
|
240
123
|
child: DataTable(
|
|
@@ -251,51 +134,75 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
251
134
|
DataColumn(label: Text('Return')),
|
|
252
135
|
DataColumn(label: Text('% Return')),
|
|
253
136
|
],
|
|
254
|
-
rows:
|
|
137
|
+
rows: viewModel.portfolioSummary.map((position) {
|
|
255
138
|
final symbol = position['symbol'] as String;
|
|
256
|
-
final idForLookup =
|
|
257
|
-
|
|
258
|
-
final
|
|
259
|
-
final
|
|
260
|
-
final
|
|
261
|
-
final
|
|
262
|
-
final
|
|
263
|
-
|
|
264
|
-
double? totalReturnValue, percentageReturn;
|
|
139
|
+
final idForLookup =
|
|
140
|
+
(position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
141
|
+
final livePrice = viewModel.livePrices[idForLookup];
|
|
142
|
+
final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
143
|
+
final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
|
|
144
|
+
final totalPurchased = double.tryParse(position['total_cost']?.toString() ?? '0.0') ?? 0.0;
|
|
145
|
+
final accountAllocation = double.tryParse(position['account_allocation']?.toString() ?? '0.0') ?? 0.0;
|
|
146
|
+
final stocksAllocation = double.tryParse(position['stocks_allocation']?.toString() ?? '0.0') ?? 0.0;
|
|
265
147
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
totalReturnValue = currentMarketValue - totalPurchased;
|
|
269
|
-
if (totalPurchased > 0) percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
270
|
-
}
|
|
148
|
+
final totalReturnValue = double.tryParse(position['total_return_value']?.toString() ?? '0.0') ?? 0.0;
|
|
149
|
+
final percentageReturn = double.tryParse(position['percentage_return']?.toString() ?? '0.0') ?? 0.0;
|
|
271
150
|
|
|
272
|
-
final returnColor =
|
|
151
|
+
final returnColor =
|
|
152
|
+
totalReturnValue >= 0 ? Colors.green : Colors.red;
|
|
273
153
|
|
|
274
154
|
return DataRow(cells: [
|
|
275
|
-
DataCell(SizedBox(
|
|
155
|
+
DataCell(SizedBox(
|
|
156
|
+
width: 150,
|
|
157
|
+
child: Text(position['name'] as String,
|
|
158
|
+
overflow: TextOverflow.ellipsis))),
|
|
276
159
|
DataCell(Text(symbol)),
|
|
277
160
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
278
161
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
279
|
-
DataCell(livePrice != null
|
|
162
|
+
DataCell(livePrice != null
|
|
163
|
+
? Text(_formatCurrency(livePrice))
|
|
164
|
+
: const Text('N/A')),
|
|
280
165
|
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
281
166
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
282
167
|
DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
|
|
283
|
-
DataCell(
|
|
284
|
-
|
|
168
|
+
DataCell(
|
|
169
|
+
totalReturnValue != null
|
|
170
|
+
? Text(_formatCurrency(totalReturnValue),
|
|
171
|
+
style: TextStyle(color: returnColor))
|
|
172
|
+
: const Text('N/A'),
|
|
173
|
+
),
|
|
174
|
+
DataCell(
|
|
175
|
+
percentageReturn != null
|
|
176
|
+
? Text('${percentageReturn.toStringAsFixed(2)}%',
|
|
177
|
+
style: TextStyle(color: returnColor))
|
|
178
|
+
: const Text('N/A'),
|
|
179
|
+
),
|
|
285
180
|
]);
|
|
286
181
|
}).toList(),
|
|
287
182
|
),
|
|
288
183
|
);
|
|
289
184
|
}
|
|
290
185
|
|
|
291
|
-
Widget _buildAnaliticoTable(
|
|
292
|
-
|
|
186
|
+
Widget _buildAnaliticoTable(
|
|
187
|
+
BuildContext context, CryptoAccountViewModel viewModel) {
|
|
188
|
+
if (viewModel.trades.isEmpty) {
|
|
189
|
+
return const Center(
|
|
190
|
+
child: Padding(
|
|
191
|
+
padding: EdgeInsets.all(16.0),
|
|
192
|
+
child: Text('No trades found.'),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
293
196
|
|
|
294
|
-
final sortedTrades = List<Map<String, dynamic>>.from(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
197
|
+
final sortedTrades = List<Map<String, dynamic>>.from(viewModel.trades)
|
|
198
|
+
..sort((a, b) {
|
|
199
|
+
try {
|
|
200
|
+
return DateTime.parse(b['date'] as String)
|
|
201
|
+
.compareTo(DateTime.parse(a['date'] as String));
|
|
202
|
+
} catch (e) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
299
206
|
|
|
300
207
|
return DataTable(
|
|
301
208
|
columns: const [
|
|
@@ -305,14 +212,14 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
305
212
|
DataColumn(label: Text('Quantity')),
|
|
306
213
|
DataColumn(label: Text('Price')),
|
|
307
214
|
DataColumn(label: Text('Total')),
|
|
308
|
-
DataColumn(label: Text('Actions')),
|
|
215
|
+
DataColumn(label: Text('Actions')),
|
|
309
216
|
],
|
|
310
217
|
rows: sortedTrades.map((trade) {
|
|
311
|
-
final double shares = double.
|
|
312
|
-
final double price = double.
|
|
218
|
+
final double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
219
|
+
final double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
|
|
313
220
|
final double total = shares * price;
|
|
314
221
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
315
|
-
final int tradeId = trade['id'] as int;
|
|
222
|
+
final int tradeId = trade['id'] as int;
|
|
316
223
|
|
|
317
224
|
return DataRow(cells: [
|
|
318
225
|
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
@@ -321,10 +228,11 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
321
228
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
322
229
|
DataCell(Text(_formatCurrency(price))),
|
|
323
230
|
DataCell(Text(_formatCurrency(total))),
|
|
324
|
-
DataCell(
|
|
231
|
+
DataCell(
|
|
325
232
|
IconButton(
|
|
326
233
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
327
|
-
onPressed: () =>
|
|
234
|
+
onPressed: () =>
|
|
235
|
+
_confirmAndDeleteTrade(context, viewModel, tradeId),
|
|
328
236
|
tooltip: 'Delete Trade',
|
|
329
237
|
),
|
|
330
238
|
),
|
|
@@ -333,31 +241,20 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
333
241
|
);
|
|
334
242
|
}
|
|
335
243
|
|
|
336
|
-
Future<void> _confirmAndDeleteTrade(
|
|
337
|
-
|
|
244
|
+
Future<void> _confirmAndDeleteTrade(BuildContext context,
|
|
245
|
+
CryptoAccountViewModel viewModel, int tradeId) async {
|
|
246
|
+
final bool? confirm =
|
|
247
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
338
248
|
|
|
339
249
|
if (confirm == true) {
|
|
340
|
-
|
|
250
|
+
final success = await viewModel.deleteTrade(tradeId);
|
|
251
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
341
252
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
348
|
-
if (success) {
|
|
349
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
350
|
-
const SnackBar(content: Text('Trade deleted successfully.')),
|
|
351
|
-
);
|
|
352
|
-
_fetchData(); // Refresh data after deletion
|
|
353
|
-
} else {
|
|
354
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
355
|
-
const SnackBar(content: Text('Failed to delete trade.')),
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
} catch (e) {
|
|
359
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
360
|
-
SnackBar(content: Text('Error deleting trade: $e')),
|
|
253
|
+
SnackBar(
|
|
254
|
+
content: Text(success
|
|
255
|
+
? 'Trade deleted successfully.'
|
|
256
|
+
: 'Failed to delete trade.'),
|
|
257
|
+
),
|
|
361
258
|
);
|
|
362
259
|
}
|
|
363
260
|
}
|