@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.0 → 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 +14 -0
- package/lib/screens/crypto_account_screen.dart +171 -281
- package/lib/screens/investment_account_screen.dart +217 -402
- package/lib/screens/rrsp_sun_life_screen.dart +119 -224
- 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,17 @@
|
|
|
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
|
+
|
|
8
|
+
## [1.1.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.0...v1.1.1) (2025-12-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add AppDialogs.showDeleteConfirmationDialog ([29c41f8](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/29c41f80530906111efe5fac1c3c493c78a29a62))
|
|
14
|
+
|
|
1
15
|
# [1.1.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.1...v1.1.0) (2025-11-28)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,239 +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';
|
|
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';
|
|
9
8
|
|
|
10
|
-
class CryptoAccountScreen extends
|
|
9
|
+
class CryptoAccountScreen extends StatelessWidget {
|
|
11
10
|
final String accountName;
|
|
12
11
|
|
|
13
|
-
const CryptoAccountScreen({Key? key, required this.accountName})
|
|
14
|
-
|
|
15
|
-
@override
|
|
16
|
-
_CryptoAccountScreenState createState() => _CryptoAccountScreenState();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
20
|
-
final TransactionService _transactionService = TransactionService();
|
|
21
|
-
final CryptoService _cryptoService = CryptoService();
|
|
22
|
-
|
|
23
|
-
List<Map<String, dynamic>> _trades = [];
|
|
24
|
-
List<Map<String, dynamic>> _assets = [];
|
|
25
|
-
List<Map<String, dynamic>> _portfolioSummary = [];
|
|
26
|
-
Map<String, double> _livePrices = {};
|
|
27
|
-
double _cashBalance = 0.0;
|
|
28
|
-
double _accountTotalValue = 0.0;
|
|
29
|
-
double _totalPortfolioBookCost = 0.0;
|
|
30
|
-
bool _isLoading = true;
|
|
31
|
-
bool _isFetchingPrices = false;
|
|
32
|
-
String? _selectedSymbolForFilter;
|
|
33
|
-
String? _token;
|
|
34
|
-
|
|
35
|
-
@override
|
|
36
|
-
void initState() {
|
|
37
|
-
super.initState();
|
|
38
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
39
|
-
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
40
|
-
setState(() {
|
|
41
|
-
_token = authProvider.token;
|
|
42
|
-
});
|
|
43
|
-
_fetchData();
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
String currencySymbol = r'$';
|
|
48
|
-
|
|
49
|
-
String _formatCurrency(double value) {
|
|
50
|
-
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
Future<void> _fetchData() async {
|
|
54
|
-
if (_token == null) return;
|
|
55
|
-
setState(() => _isLoading = true);
|
|
56
|
-
try {
|
|
57
|
-
final futures = <Future>[
|
|
58
|
-
_transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
|
|
59
|
-
_transactionService.getAccountBalance(widget.accountName, token: _token),
|
|
60
|
-
_transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
|
|
61
|
-
_transactionService.getTotalPortfolioBookCost(token: _token), // Fetch total cost
|
|
62
|
-
];
|
|
63
|
-
final results = await Future.wait(futures);
|
|
64
|
-
|
|
65
|
-
final trades = results[0] as List<Map<String, dynamic>>;
|
|
66
|
-
final balance = results[1] as double;
|
|
67
|
-
final assets = results[2] as List<Map<String, dynamic>>;
|
|
68
|
-
final totalPortfolioBookCost = results[3] as double;
|
|
69
|
-
|
|
70
|
-
final summaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
71
|
-
|
|
72
|
-
setState(() {
|
|
73
|
-
_trades = trades;
|
|
74
|
-
_cashBalance = balance;
|
|
75
|
-
_assets = assets;
|
|
76
|
-
_portfolioSummary = summaryData['summary'];
|
|
77
|
-
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
78
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (mounted) await _fetchLivePrices();
|
|
82
|
-
} catch (e) {
|
|
83
|
-
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
84
|
-
} finally {
|
|
85
|
-
if (mounted) setState(() => _isLoading = false);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
Future<void> _fetchLivePrices() async {
|
|
90
|
-
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
91
|
-
setState(() => _isFetchingPrices = true);
|
|
92
|
-
|
|
93
|
-
final idsToFetch = _portfolioSummary.map((p) => p['id_crypto'] as String? ?? p['symbol'] as String).where((s) => s.isNotEmpty).toList();
|
|
94
|
-
|
|
95
|
-
if (idsToFetch.isEmpty) {
|
|
96
|
-
setState(() => _isFetchingPrices = false);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
final livePrices = await _cryptoService.getLiveCryptoPrices(idsToFetch);
|
|
101
|
-
|
|
102
|
-
if (!mounted) return;
|
|
103
|
-
|
|
104
|
-
double newTotalValue = _cashBalance;
|
|
105
|
-
for (final position in _portfolioSummary) {
|
|
106
|
-
final shares = double.parse(position['shares'].toString());
|
|
107
|
-
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
108
|
-
final livePrice = livePrices[idForLookup];
|
|
109
|
-
if (livePrice != null) {
|
|
110
|
-
newTotalValue += shares * livePrice;
|
|
111
|
-
} else {
|
|
112
|
-
newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
setState(() {
|
|
117
|
-
_livePrices = livePrices;
|
|
118
|
-
_accountTotalValue = newTotalValue;
|
|
119
|
-
_isFetchingPrices = false;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
|
|
124
|
-
Map<String, dynamic> summary = {};
|
|
125
|
-
double accountPortfolioBookCost = 0;
|
|
126
|
-
|
|
127
|
-
for (var trade in trades) {
|
|
128
|
-
final asset = trade['asset'];
|
|
129
|
-
if (asset == null || asset['symbol'] == null) continue;
|
|
130
|
-
String symbol = asset['symbol'];
|
|
131
|
-
String idCrypto = asset['id_crypto'] ?? symbol;
|
|
132
|
-
|
|
133
|
-
double shares = double.parse(trade['shares'].toString());
|
|
134
|
-
double price = double.parse(trade['price'].toString());
|
|
135
|
-
String tradeType = trade['trade_type'];
|
|
136
|
-
|
|
137
|
-
if (!summary.containsKey(symbol)) {
|
|
138
|
-
summary[symbol] = {
|
|
139
|
-
'symbol': symbol,
|
|
140
|
-
'id_crypto': idCrypto,
|
|
141
|
-
'name': asset['name'] ?? symbol,
|
|
142
|
-
'shares': 0.0,
|
|
143
|
-
'total_cost': 0.0,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (tradeType == 'buy') {
|
|
148
|
-
summary[symbol]['shares'] += shares;
|
|
149
|
-
summary[symbol]['total_cost'] += shares * price;
|
|
150
|
-
} else if (tradeType == 'sell') {
|
|
151
|
-
double originalShares = summary[symbol]['shares'];
|
|
152
|
-
if (originalShares > 0) {
|
|
153
|
-
double avgPrice = summary[symbol]['total_cost'] / originalShares;
|
|
154
|
-
summary[symbol]['total_cost'] -= shares * avgPrice;
|
|
155
|
-
}
|
|
156
|
-
summary[symbol]['shares'] -= shares;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
161
|
-
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
162
|
-
|
|
163
|
-
List<Map<String, dynamic>> result = [];
|
|
164
|
-
summary.forEach((symbol, data) {
|
|
165
|
-
double shares = data['shares'];
|
|
166
|
-
double totalCost = data['total_cost'];
|
|
167
|
-
data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
|
|
168
|
-
data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
|
|
169
|
-
data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
170
|
-
result.add(data);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return {'summary': result, 'total_value': accountPortfolioBookCost};
|
|
174
|
-
}
|
|
175
|
-
|
|
12
|
+
const CryptoAccountScreen({Key? key, required this.accountName})
|
|
13
|
+
: super(key: key);
|
|
176
14
|
|
|
177
15
|
@override
|
|
178
16
|
Widget build(BuildContext context) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
padding: const EdgeInsets.only(right: 16.0),
|
|
186
|
-
child: Column(
|
|
187
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
188
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
189
|
-
children: [
|
|
190
|
-
Text('Total Portfolio: ${_formatCurrency(_accountTotalValue)}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
191
|
-
Text('Account Balance: ${_formatCurrency(_cashBalance)}', style: const TextStyle(fontSize: 12)),
|
|
192
|
-
],
|
|
193
|
-
),
|
|
194
|
-
),
|
|
195
|
-
),
|
|
196
|
-
IconButton(icon: const Icon(Icons.refresh), onPressed: _fetchData, tooltip: 'Refresh Data'),
|
|
197
|
-
],
|
|
17
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
18
|
+
|
|
19
|
+
return ChangeNotifierProvider(
|
|
20
|
+
create: (_) => CryptoAccountViewModel(
|
|
21
|
+
accountName: accountName,
|
|
22
|
+
token: authProvider.token,
|
|
198
23
|
),
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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,
|
|
211
36
|
children: [
|
|
212
|
-
Text(
|
|
213
|
-
|
|
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
|
+
),
|
|
214
46
|
],
|
|
215
47
|
),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
token: _token),
|
|
223
|
-
const SizedBox(height: 24),
|
|
224
|
-
Text('Trade History', style: Theme.of(context).textTheme.titleLarge),
|
|
225
|
-
const SizedBox(height: 8),
|
|
226
|
-
_buildAnaliticoTable(),
|
|
227
|
-
],
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
IconButton(
|
|
51
|
+
icon: const Icon(Icons.refresh),
|
|
52
|
+
onPressed: viewModel.fetchData,
|
|
53
|
+
tooltip: 'Refresh Data',
|
|
228
54
|
),
|
|
229
|
-
|
|
55
|
+
],
|
|
230
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
|
+
),
|
|
231
102
|
);
|
|
232
103
|
}
|
|
233
104
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
+
|
|
237
121
|
return SingleChildScrollView(
|
|
238
122
|
scrollDirection: Axis.horizontal,
|
|
239
123
|
child: DataTable(
|
|
@@ -250,51 +134,85 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
250
134
|
DataColumn(label: Text('Return')),
|
|
251
135
|
DataColumn(label: Text('% Return')),
|
|
252
136
|
],
|
|
253
|
-
rows:
|
|
137
|
+
rows: viewModel.portfolioSummary.map((position) {
|
|
254
138
|
final symbol = position['symbol'] as String;
|
|
255
|
-
final idForLookup =
|
|
256
|
-
|
|
139
|
+
final idForLookup =
|
|
140
|
+
(position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
141
|
+
final livePrice = viewModel.livePrices[idForLookup];
|
|
257
142
|
final shares = double.parse(position['shares'].toString());
|
|
258
143
|
final avgPrice = double.parse(position['avg_price'].toString());
|
|
259
|
-
final totalPurchased =
|
|
260
|
-
|
|
261
|
-
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;
|
|
262
150
|
|
|
263
151
|
double? totalReturnValue, percentageReturn;
|
|
264
152
|
|
|
265
153
|
if (livePrice != null && shares > 0) {
|
|
266
154
|
final currentMarketValue = livePrice * shares;
|
|
267
155
|
totalReturnValue = currentMarketValue - totalPurchased;
|
|
268
|
-
if (totalPurchased > 0)
|
|
156
|
+
if (totalPurchased > 0) {
|
|
157
|
+
percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
158
|
+
}
|
|
269
159
|
}
|
|
270
160
|
|
|
271
|
-
final returnColor =
|
|
161
|
+
final returnColor =
|
|
162
|
+
(totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
272
163
|
|
|
273
164
|
return DataRow(cells: [
|
|
274
|
-
DataCell(SizedBox(
|
|
165
|
+
DataCell(SizedBox(
|
|
166
|
+
width: 150,
|
|
167
|
+
child: Text(position['name'] as String,
|
|
168
|
+
overflow: TextOverflow.ellipsis))),
|
|
275
169
|
DataCell(Text(symbol)),
|
|
276
170
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
277
171
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
278
|
-
DataCell(livePrice != null
|
|
172
|
+
DataCell(livePrice != null
|
|
173
|
+
? Text(_formatCurrency(livePrice))
|
|
174
|
+
: const Text('N/A')),
|
|
279
175
|
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
280
176
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
281
177
|
DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
|
|
282
|
-
DataCell(
|
|
283
|
-
|
|
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
|
+
),
|
|
284
190
|
]);
|
|
285
191
|
}).toList(),
|
|
286
192
|
),
|
|
287
193
|
);
|
|
288
194
|
}
|
|
289
195
|
|
|
290
|
-
Widget _buildAnaliticoTable(
|
|
291
|
-
|
|
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
|
+
}
|
|
292
206
|
|
|
293
|
-
final sortedTrades = List<Map<String, dynamic>>.from(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
});
|
|
298
216
|
|
|
299
217
|
return DataTable(
|
|
300
218
|
columns: const [
|
|
@@ -304,14 +222,14 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
304
222
|
DataColumn(label: Text('Quantity')),
|
|
305
223
|
DataColumn(label: Text('Price')),
|
|
306
224
|
DataColumn(label: Text('Total')),
|
|
307
|
-
DataColumn(label: Text('Actions')),
|
|
225
|
+
DataColumn(label: Text('Actions')),
|
|
308
226
|
],
|
|
309
227
|
rows: sortedTrades.map((trade) {
|
|
310
228
|
final double shares = double.parse(trade['shares'].toString());
|
|
311
229
|
final double price = double.parse(trade['price'].toString());
|
|
312
230
|
final double total = shares * price;
|
|
313
231
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
314
|
-
final int tradeId = trade['id'] as int;
|
|
232
|
+
final int tradeId = trade['id'] as int;
|
|
315
233
|
|
|
316
234
|
return DataRow(cells: [
|
|
317
235
|
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
@@ -320,10 +238,11 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
320
238
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
321
239
|
DataCell(Text(_formatCurrency(price))),
|
|
322
240
|
DataCell(Text(_formatCurrency(total))),
|
|
323
|
-
DataCell(
|
|
241
|
+
DataCell(
|
|
324
242
|
IconButton(
|
|
325
243
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
326
|
-
onPressed: () =>
|
|
244
|
+
onPressed: () =>
|
|
245
|
+
_confirmAndDeleteTrade(context, viewModel, tradeId),
|
|
327
246
|
tooltip: 'Delete Trade',
|
|
328
247
|
),
|
|
329
248
|
),
|
|
@@ -332,51 +251,22 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
332
251
|
);
|
|
333
252
|
}
|
|
334
253
|
|
|
335
|
-
Future<void> _confirmAndDeleteTrade(
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return AlertDialog(
|
|
340
|
-
title: const Text('Confirm Deletion'),
|
|
341
|
-
content: const Text('Are you sure you want to delete this trade? This action cannot be undone.'),
|
|
342
|
-
actions: <Widget>[
|
|
343
|
-
TextButton(
|
|
344
|
-
onPressed: () => Navigator.of(context).pop(false),
|
|
345
|
-
child: const Text('Cancel'),
|
|
346
|
-
),
|
|
347
|
-
TextButton(
|
|
348
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
349
|
-
child: const Text('Delete'),
|
|
350
|
-
),
|
|
351
|
-
],
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
);
|
|
254
|
+
Future<void> _confirmAndDeleteTrade(BuildContext context,
|
|
255
|
+
CryptoAccountViewModel viewModel, int tradeId) async {
|
|
256
|
+
final bool? confirm =
|
|
257
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
355
258
|
|
|
356
259
|
if (confirm == true) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const SnackBar(content: Text('Authentication token not available.')),
|
|
360
|
-
);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
try {
|
|
364
|
-
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
365
|
-
if (success) {
|
|
366
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
367
|
-
const SnackBar(content: Text('Trade deleted successfully.')),
|
|
368
|
-
);
|
|
369
|
-
_fetchData(); // Refresh data after deletion
|
|
370
|
-
} else {
|
|
371
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
372
|
-
const SnackBar(content: Text('Failed to delete trade.')),
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
} catch (e) {
|
|
260
|
+
final success = await viewModel.deleteTrade(tradeId);
|
|
261
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
376
262
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
377
|
-
SnackBar(
|
|
263
|
+
SnackBar(
|
|
264
|
+
content: Text(success
|
|
265
|
+
? 'Trade deleted successfully.'
|
|
266
|
+
: 'Failed to delete trade.'),
|
|
267
|
+
),
|
|
378
268
|
);
|
|
379
269
|
}
|
|
380
270
|
}
|
|
381
271
|
}
|
|
382
|
-
}
|
|
272
|
+
}
|