@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 1.1.2
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 +7 -0
- package/lib/screens/crypto_account_screen.dart +169 -262
- package/lib/screens/investment_account_screen.dart +202 -401
- package/lib/screens/rrsp_sun_life_screen.dart +117 -211
- package/lib/viewmodels/crypto_account_viewmodel.dart +180 -0
- package/lib/viewmodels/investment_account_viewmodel.dart +221 -0
- package/lib/viewmodels/rrsp_sun_life_viewmodel.dart +103 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.1.2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.1...v1.1.2) (2025-12-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 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))
|
|
7
|
+
|
|
1
8
|
## [1.1.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.0...v1.1.1) (2025-12-02)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -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,85 @@ 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
|
-
|
|
139
|
+
final idForLookup =
|
|
140
|
+
(position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
141
|
+
final livePrice = viewModel.livePrices[idForLookup];
|
|
258
142
|
final shares = double.parse(position['shares'].toString());
|
|
259
143
|
final avgPrice = double.parse(position['avg_price'].toString());
|
|
260
|
-
final totalPurchased =
|
|
261
|
-
|
|
262
|
-
final
|
|
144
|
+
final totalPurchased =
|
|
145
|
+
double.parse(position['total_cost'].toString());
|
|
146
|
+
final accountAllocation =
|
|
147
|
+
(position['account_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
148
|
+
final stocksAllocation =
|
|
149
|
+
(position['stocks_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
263
150
|
|
|
264
151
|
double? totalReturnValue, percentageReturn;
|
|
265
152
|
|
|
266
153
|
if (livePrice != null && shares > 0) {
|
|
267
154
|
final currentMarketValue = livePrice * shares;
|
|
268
155
|
totalReturnValue = currentMarketValue - totalPurchased;
|
|
269
|
-
if (totalPurchased > 0)
|
|
156
|
+
if (totalPurchased > 0) {
|
|
157
|
+
percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
158
|
+
}
|
|
270
159
|
}
|
|
271
160
|
|
|
272
|
-
final returnColor =
|
|
161
|
+
final returnColor =
|
|
162
|
+
(totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
273
163
|
|
|
274
164
|
return DataRow(cells: [
|
|
275
|
-
DataCell(SizedBox(
|
|
165
|
+
DataCell(SizedBox(
|
|
166
|
+
width: 150,
|
|
167
|
+
child: Text(position['name'] as String,
|
|
168
|
+
overflow: TextOverflow.ellipsis))),
|
|
276
169
|
DataCell(Text(symbol)),
|
|
277
170
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
278
171
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
279
|
-
DataCell(livePrice != null
|
|
172
|
+
DataCell(livePrice != null
|
|
173
|
+
? Text(_formatCurrency(livePrice))
|
|
174
|
+
: const Text('N/A')),
|
|
280
175
|
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
281
176
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
282
177
|
DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
|
|
283
|
-
DataCell(
|
|
284
|
-
|
|
178
|
+
DataCell(
|
|
179
|
+
totalReturnValue != null
|
|
180
|
+
? Text(_formatCurrency(totalReturnValue),
|
|
181
|
+
style: TextStyle(color: returnColor))
|
|
182
|
+
: const Text('N/A'),
|
|
183
|
+
),
|
|
184
|
+
DataCell(
|
|
185
|
+
percentageReturn != null
|
|
186
|
+
? Text('${percentageReturn.toStringAsFixed(2)}%',
|
|
187
|
+
style: TextStyle(color: returnColor))
|
|
188
|
+
: const Text('N/A'),
|
|
189
|
+
),
|
|
285
190
|
]);
|
|
286
191
|
}).toList(),
|
|
287
192
|
),
|
|
288
193
|
);
|
|
289
194
|
}
|
|
290
195
|
|
|
291
|
-
Widget _buildAnaliticoTable(
|
|
292
|
-
|
|
196
|
+
Widget _buildAnaliticoTable(
|
|
197
|
+
BuildContext context, CryptoAccountViewModel viewModel) {
|
|
198
|
+
if (viewModel.trades.isEmpty) {
|
|
199
|
+
return const Center(
|
|
200
|
+
child: Padding(
|
|
201
|
+
padding: EdgeInsets.all(16.0),
|
|
202
|
+
child: Text('No trades found.'),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
293
206
|
|
|
294
|
-
final sortedTrades = List<Map<String, dynamic>>.from(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
207
|
+
final sortedTrades = List<Map<String, dynamic>>.from(viewModel.trades)
|
|
208
|
+
..sort((a, b) {
|
|
209
|
+
try {
|
|
210
|
+
return DateTime.parse(b['date'] as String)
|
|
211
|
+
.compareTo(DateTime.parse(a['date'] as String));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
299
216
|
|
|
300
217
|
return DataTable(
|
|
301
218
|
columns: const [
|
|
@@ -305,14 +222,14 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
305
222
|
DataColumn(label: Text('Quantity')),
|
|
306
223
|
DataColumn(label: Text('Price')),
|
|
307
224
|
DataColumn(label: Text('Total')),
|
|
308
|
-
DataColumn(label: Text('Actions')),
|
|
225
|
+
DataColumn(label: Text('Actions')),
|
|
309
226
|
],
|
|
310
227
|
rows: sortedTrades.map((trade) {
|
|
311
228
|
final double shares = double.parse(trade['shares'].toString());
|
|
312
229
|
final double price = double.parse(trade['price'].toString());
|
|
313
230
|
final double total = shares * price;
|
|
314
231
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
315
|
-
final int tradeId = trade['id'] as int;
|
|
232
|
+
final int tradeId = trade['id'] as int;
|
|
316
233
|
|
|
317
234
|
return DataRow(cells: [
|
|
318
235
|
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
@@ -321,10 +238,11 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
321
238
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
322
239
|
DataCell(Text(_formatCurrency(price))),
|
|
323
240
|
DataCell(Text(_formatCurrency(total))),
|
|
324
|
-
DataCell(
|
|
241
|
+
DataCell(
|
|
325
242
|
IconButton(
|
|
326
243
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
327
|
-
onPressed: () =>
|
|
244
|
+
onPressed: () =>
|
|
245
|
+
_confirmAndDeleteTrade(context, viewModel, tradeId),
|
|
328
246
|
tooltip: 'Delete Trade',
|
|
329
247
|
),
|
|
330
248
|
),
|
|
@@ -333,31 +251,20 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
333
251
|
);
|
|
334
252
|
}
|
|
335
253
|
|
|
336
|
-
Future<void> _confirmAndDeleteTrade(
|
|
337
|
-
|
|
254
|
+
Future<void> _confirmAndDeleteTrade(BuildContext context,
|
|
255
|
+
CryptoAccountViewModel viewModel, int tradeId) async {
|
|
256
|
+
final bool? confirm =
|
|
257
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
338
258
|
|
|
339
259
|
if (confirm == true) {
|
|
340
|
-
|
|
260
|
+
final success = await viewModel.deleteTrade(tradeId);
|
|
261
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
341
262
|
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')),
|
|
263
|
+
SnackBar(
|
|
264
|
+
content: Text(success
|
|
265
|
+
? 'Trade deleted successfully.'
|
|
266
|
+
: 'Failed to delete trade.'),
|
|
267
|
+
),
|
|
361
268
|
);
|
|
362
269
|
}
|
|
363
270
|
}
|