@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
|
@@ -1,193 +1,104 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:intl/intl.dart';
|
|
3
|
-
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
4
|
-
import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
|
|
5
3
|
import 'package:provider/provider.dart';
|
|
6
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/rrsp_contribution_form.dart';
|
|
7
|
+
import '../viewmodels/rrsp_sun_life_viewmodel.dart';
|
|
7
8
|
|
|
8
|
-
class RrspSunLifeScreen extends
|
|
9
|
+
class RrspSunLifeScreen extends StatelessWidget {
|
|
9
10
|
const RrspSunLifeScreen({Key? key}) : super(key: key);
|
|
10
11
|
|
|
11
|
-
@override
|
|
12
|
-
_RrspSunLifeScreenState createState() => _RrspSunLifeScreenState();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
16
|
-
final TransactionService _transactionService = TransactionService();
|
|
17
|
-
List<Map<String, dynamic>> _contributions = [];
|
|
18
|
-
List<Map<String, dynamic>> _sinteticoSummary = [];
|
|
19
|
-
double _rrspTotalValue = 0.0;
|
|
20
|
-
double _rrspCashBalance = 0.0;
|
|
21
|
-
double _totalPortfolioBookCost = 0.0;
|
|
22
|
-
bool _isLoading = true;
|
|
23
|
-
String? _token;
|
|
24
|
-
|
|
25
|
-
@override
|
|
26
|
-
void initState() {
|
|
27
|
-
super.initState();
|
|
28
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
29
|
-
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
30
|
-
setState(() {
|
|
31
|
-
_token = authProvider.token;
|
|
32
|
-
});
|
|
33
|
-
_fetchData();
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
String currencySymbol = r'$'; // ou poderia vir de configuração, API, etc.
|
|
38
|
-
|
|
39
|
-
String _formatCurrency(double value) {
|
|
40
|
-
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
Future<void> _fetchData() async {
|
|
44
|
-
if (_token == null) return;
|
|
45
|
-
if (mounted) {
|
|
46
|
-
setState(() {
|
|
47
|
-
_isLoading = true;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
final contributions = await _transactionService.getRrspContributions(
|
|
53
|
-
investmentAccount: 'RRSP Sun Life', token: _token);
|
|
54
|
-
final balance =
|
|
55
|
-
await _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
|
|
56
|
-
final totalPortfolioBookCost =
|
|
57
|
-
await _transactionService.getTotalPortfolioBookCost(token: _token);
|
|
58
|
-
|
|
59
|
-
final sinteticoSummaryData =
|
|
60
|
-
_calculateSinteticoSummary(contributions, totalPortfolioBookCost);
|
|
61
|
-
|
|
62
|
-
if (!mounted) return;
|
|
63
|
-
setState(() {
|
|
64
|
-
_contributions = contributions;
|
|
65
|
-
_sinteticoSummary = sinteticoSummaryData['summary'];
|
|
66
|
-
_rrspCashBalance = balance;
|
|
67
|
-
_rrspTotalValue = sinteticoSummaryData['total_value'];
|
|
68
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
69
|
-
});
|
|
70
|
-
} catch (e) {
|
|
71
|
-
if (!mounted) return;
|
|
72
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
73
|
-
SnackBar(content: Text('Error fetching RRSP data: $e')),
|
|
74
|
-
);
|
|
75
|
-
} finally {
|
|
76
|
-
if (mounted) {
|
|
77
|
-
setState(() {
|
|
78
|
-
_isLoading = false;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
Map<String, dynamic> _calculateSinteticoSummary(
|
|
85
|
-
List<Map<String, dynamic>> contributions, double totalPortfolioBookCost) {
|
|
86
|
-
double totalUserContribution = 0;
|
|
87
|
-
double totalCompanyContribution = 0;
|
|
88
|
-
double totalUnrealizedPL = 0;
|
|
89
|
-
|
|
90
|
-
for (var c in contributions) {
|
|
91
|
-
totalUserContribution += double.parse(c['rrsp_amount'].toString());
|
|
92
|
-
totalCompanyContribution += double.parse(c['dpsp_amount'].toString());
|
|
93
|
-
totalUnrealizedPL += double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
final totalContributed = totalUserContribution + totalCompanyContribution;
|
|
97
|
-
final marketValue = totalContributed + totalUnrealizedPL;
|
|
98
|
-
|
|
99
|
-
final summaryData = {
|
|
100
|
-
'user_contribution': totalUserContribution,
|
|
101
|
-
'company_contribution': totalCompanyContribution,
|
|
102
|
-
'total_contributed': totalContributed,
|
|
103
|
-
'unrealized_pl': totalUnrealizedPL,
|
|
104
|
-
'percent_return':
|
|
105
|
-
(totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
|
|
106
|
-
'market_value': marketValue,
|
|
107
|
-
'portfolio_allocation_percent': (totalPortfolioBookCost > 0)
|
|
108
|
-
? (marketValue / totalPortfolioBookCost) * 100
|
|
109
|
-
: 0.0,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
'summary': [summaryData],
|
|
114
|
-
'total_value': marketValue
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
12
|
@override
|
|
119
13
|
Widget build(BuildContext context) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
14
|
+
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
15
|
+
|
|
16
|
+
return ChangeNotifierProvider(
|
|
17
|
+
create: (_) => RrspSunLifeViewModel(token: authProvider.token),
|
|
18
|
+
child: Consumer<RrspSunLifeViewModel>(
|
|
19
|
+
builder: (context, viewModel, child) {
|
|
20
|
+
return Scaffold(
|
|
21
|
+
appBar: AppBar(
|
|
22
|
+
title: const Text('RRSP Sun Life Portfolio'),
|
|
23
|
+
actions: [
|
|
24
|
+
Center(
|
|
25
|
+
child: Padding(
|
|
26
|
+
padding: const EdgeInsets.only(right: 16.0),
|
|
27
|
+
child: Column(
|
|
28
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
29
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
30
|
+
children: [
|
|
31
|
+
Text(
|
|
32
|
+
'Total Portfolio: ${_formatCurrency(viewModel.rrspTotalValue)}',
|
|
33
|
+
style: const TextStyle(
|
|
34
|
+
fontSize: 16, fontWeight: FontWeight.bold),
|
|
35
|
+
),
|
|
36
|
+
Text(
|
|
37
|
+
'Account Balance: ${_formatCurrency(viewModel.rrspCashBalance)}',
|
|
38
|
+
style: const TextStyle(fontSize: 12),
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
),
|
|
135
42
|
),
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
43
|
+
),
|
|
44
|
+
IconButton(
|
|
45
|
+
icon: const Icon(Icons.refresh),
|
|
46
|
+
onPressed: viewModel.fetchData,
|
|
47
|
+
tooltip: 'Refresh Data',
|
|
48
|
+
),
|
|
49
|
+
],
|
|
142
50
|
),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
51
|
+
body: viewModel.isLoading
|
|
52
|
+
? const Center(child: CircularProgressIndicator())
|
|
53
|
+
: RefreshIndicator(
|
|
54
|
+
onRefresh: viewModel.fetchData,
|
|
55
|
+
child: SingleChildScrollView(
|
|
56
|
+
padding: const EdgeInsets.all(16.0),
|
|
57
|
+
child: Column(
|
|
58
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
59
|
+
children: [
|
|
60
|
+
Text('Summary',
|
|
61
|
+
style: Theme.of(context).textTheme.titleLarge),
|
|
62
|
+
const SizedBox(height: 8),
|
|
63
|
+
_buildSinteticoTable(context, viewModel),
|
|
64
|
+
const SizedBox(height: 24),
|
|
65
|
+
RrspContributionForm(
|
|
66
|
+
investmentAccount: 'RRSP Sun Life',
|
|
67
|
+
onContributionLogged: viewModel.fetchData,
|
|
68
|
+
token: viewModel.token,
|
|
69
|
+
),
|
|
70
|
+
const SizedBox(height: 24),
|
|
71
|
+
Text(
|
|
72
|
+
'Contribution History',
|
|
73
|
+
style: Theme.of(context).textTheme.titleLarge,
|
|
74
|
+
),
|
|
75
|
+
const SizedBox(height: 8),
|
|
76
|
+
_buildContributionTable(context, viewModel),
|
|
77
|
+
],
|
|
78
|
+
),
|
|
79
|
+
),
|
|
172
80
|
),
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
),
|
|
177
|
-
),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
),
|
|
178
84
|
);
|
|
179
85
|
}
|
|
180
86
|
|
|
181
|
-
|
|
182
|
-
|
|
87
|
+
String _formatCurrency(double value) {
|
|
88
|
+
String currencySymbol = r'$';
|
|
89
|
+
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Widget _buildSinteticoTable(
|
|
93
|
+
BuildContext context, RrspSunLifeViewModel viewModel) {
|
|
94
|
+
if (viewModel.sinteticoSummary.isEmpty) {
|
|
183
95
|
return const SizedBox.shrink();
|
|
184
96
|
}
|
|
185
97
|
|
|
186
|
-
final summary =
|
|
98
|
+
final summary = viewModel.sinteticoSummary.first;
|
|
187
99
|
final double unrealizedPL = summary['unrealized_pl'];
|
|
188
100
|
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
189
101
|
|
|
190
|
-
|
|
191
102
|
return DataTable(
|
|
192
103
|
columns: const [
|
|
193
104
|
DataColumn(label: Text('User Contr.')),
|
|
@@ -201,24 +112,33 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
201
112
|
rows: [
|
|
202
113
|
DataRow(
|
|
203
114
|
cells: [
|
|
204
|
-
DataCell(Text(_formatCurrency(summary['user_contribution'] as double))),
|
|
205
|
-
DataCell(Text(_formatCurrency(summary['company_contribution'] as double))),
|
|
206
115
|
DataCell(
|
|
207
|
-
Text(_formatCurrency(summary['
|
|
208
|
-
DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
|
|
209
|
-
DataCell(Text(
|
|
210
|
-
'${(summary['percent_return'] as double).toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
|
|
211
|
-
DataCell(Text(_formatCurrency(summary['market_value'] as double))),
|
|
116
|
+
Text(_formatCurrency(summary['user_contribution'] as double))),
|
|
212
117
|
DataCell(Text(
|
|
213
|
-
|
|
118
|
+
_formatCurrency(summary['company_contribution'] as double))),
|
|
119
|
+
DataCell(
|
|
120
|
+
Text(_formatCurrency(summary['total_contributed'] as double))),
|
|
121
|
+
DataCell(Text(_formatCurrency(unrealizedPL),
|
|
122
|
+
style: TextStyle(color: plColor))),
|
|
123
|
+
DataCell(
|
|
124
|
+
Text('${(summary['percent_return'] as double).toStringAsFixed(2)}%',
|
|
125
|
+
style: TextStyle(color: plColor)),
|
|
126
|
+
),
|
|
127
|
+
DataCell(
|
|
128
|
+
Text(_formatCurrency(summary['market_value'] as double))),
|
|
129
|
+
DataCell(
|
|
130
|
+
Text(
|
|
131
|
+
'${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%'),
|
|
132
|
+
),
|
|
214
133
|
],
|
|
215
134
|
),
|
|
216
135
|
],
|
|
217
136
|
);
|
|
218
137
|
}
|
|
219
138
|
|
|
220
|
-
Widget _buildContributionTable(
|
|
221
|
-
|
|
139
|
+
Widget _buildContributionTable(
|
|
140
|
+
BuildContext context, RrspSunLifeViewModel viewModel) {
|
|
141
|
+
if (viewModel.contributions.isEmpty) {
|
|
222
142
|
return const Center(
|
|
223
143
|
child: Padding(
|
|
224
144
|
padding: EdgeInsets.all(16.0),
|
|
@@ -239,7 +159,7 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
239
159
|
DataColumn(label: Text('Portfolio %')),
|
|
240
160
|
DataColumn(label: Text('Actions')),
|
|
241
161
|
],
|
|
242
|
-
rows:
|
|
162
|
+
rows: viewModel.contributions.map((c) {
|
|
243
163
|
final contributionId = c['id'] as int;
|
|
244
164
|
final rrspAmount = double.parse(c['rrsp_amount'].toString());
|
|
245
165
|
final dpspAmount = double.parse(c['dpsp_amount'].toString());
|
|
@@ -247,13 +167,14 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
247
167
|
|
|
248
168
|
final returnAmount =
|
|
249
169
|
double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
250
|
-
final percentReturn =
|
|
251
|
-
|
|
170
|
+
final percentReturn = totalContributed > 0
|
|
171
|
+
? (returnAmount / totalContributed) * 100
|
|
172
|
+
: 0.0;
|
|
252
173
|
|
|
253
174
|
final cumulativeValue = totalContributed + returnAmount;
|
|
254
175
|
|
|
255
|
-
final portfolioAllocation =
|
|
256
|
-
? (cumulativeValue /
|
|
176
|
+
final portfolioAllocation = viewModel.totalPortfolioBookCost > 0
|
|
177
|
+
? (cumulativeValue / viewModel.totalPortfolioBookCost) * 100
|
|
257
178
|
: 0.0;
|
|
258
179
|
|
|
259
180
|
String dateStr;
|
|
@@ -279,7 +200,8 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
279
200
|
DataCell(
|
|
280
201
|
IconButton(
|
|
281
202
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
282
|
-
onPressed: () => _confirmAndDeleteContribution(
|
|
203
|
+
onPressed: () => _confirmAndDeleteContribution(
|
|
204
|
+
context, viewModel, contributionId),
|
|
283
205
|
tooltip: 'Delete Contribution',
|
|
284
206
|
),
|
|
285
207
|
),
|
|
@@ -289,49 +211,22 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
289
211
|
);
|
|
290
212
|
}
|
|
291
213
|
|
|
292
|
-
Future<void> _confirmAndDeleteContribution(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
title: const Text('Confirm Deletion'),
|
|
298
|
-
content: const Text('Are you sure you want to delete this contribution? This action cannot be undone.'),
|
|
299
|
-
actions: <Widget>[
|
|
300
|
-
TextButton(
|
|
301
|
-
onPressed: () => Navigator.of(context).pop(false),
|
|
302
|
-
child: const Text('Cancel'),
|
|
303
|
-
),
|
|
304
|
-
TextButton(
|
|
305
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
306
|
-
child: const Text('Delete'),
|
|
307
|
-
),
|
|
308
|
-
],
|
|
309
|
-
);
|
|
310
|
-
},
|
|
214
|
+
Future<void> _confirmAndDeleteContribution(BuildContext context,
|
|
215
|
+
RrspSunLifeViewModel viewModel, int contributionId) async {
|
|
216
|
+
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(
|
|
217
|
+
context,
|
|
218
|
+
'this contribution',
|
|
311
219
|
);
|
|
312
220
|
|
|
313
221
|
if (confirm == true) {
|
|
314
|
-
|
|
222
|
+
final success = await viewModel.deleteContribution(contributionId);
|
|
223
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
315
224
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
|
|
322
|
-
if (success) {
|
|
323
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
324
|
-
const SnackBar(content: Text('Contribution deleted successfully.')),
|
|
325
|
-
);
|
|
326
|
-
_fetchData(); // Refresh data after deletion
|
|
327
|
-
} else {
|
|
328
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
329
|
-
const SnackBar(content: Text('Failed to delete contribution.')),
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
} catch (e) {
|
|
333
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
334
|
-
SnackBar(content: Text('Error deleting contribution: $e')),
|
|
225
|
+
SnackBar(
|
|
226
|
+
content: Text(success
|
|
227
|
+
? 'Contribution deleted successfully.'
|
|
228
|
+
: 'Failed to delete contribution.'),
|
|
229
|
+
),
|
|
335
230
|
);
|
|
336
231
|
}
|
|
337
232
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:personal_finance_frontend_core_services/services/crypto_service.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
4
|
+
|
|
5
|
+
class CryptoAccountViewModel extends ChangeNotifier {
|
|
6
|
+
final TransactionService _transactionService = TransactionService();
|
|
7
|
+
final CryptoService _cryptoService = CryptoService();
|
|
8
|
+
|
|
9
|
+
final String accountName;
|
|
10
|
+
String? _token;
|
|
11
|
+
|
|
12
|
+
List<Map<String, dynamic>> _trades = [];
|
|
13
|
+
List<Map<String, dynamic>> _assets = [];
|
|
14
|
+
List<Map<String, dynamic>> _portfolioSummary = [];
|
|
15
|
+
Map<String, double> _livePrices = {};
|
|
16
|
+
double _cashBalance = 0.0;
|
|
17
|
+
double _accountTotalValue = 0.0;
|
|
18
|
+
bool _isLoading = true;
|
|
19
|
+
bool _isFetchingPrices = false;
|
|
20
|
+
|
|
21
|
+
// Getters
|
|
22
|
+
List<Map<String, dynamic>> get trades => _trades;
|
|
23
|
+
List<Map<String, dynamic>> get assets => _assets;
|
|
24
|
+
List<Map<String, dynamic>> get portfolioSummary => _portfolioSummary;
|
|
25
|
+
Map<String, double> get livePrices => _livePrices;
|
|
26
|
+
double get cashBalance => _cashBalance;
|
|
27
|
+
double get accountTotalValue => _accountTotalValue;
|
|
28
|
+
bool get isLoading => _isLoading;
|
|
29
|
+
bool get isFetchingPrices => _isFetchingPrices;
|
|
30
|
+
String? get token => _token;
|
|
31
|
+
|
|
32
|
+
CryptoAccountViewModel({required this.accountName, required String? token}) {
|
|
33
|
+
_token = token;
|
|
34
|
+
fetchData();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void setTokenAndFetch(String? token) {
|
|
38
|
+
_token = token;
|
|
39
|
+
fetchData();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Future<void> fetchData() async {
|
|
43
|
+
if (_token == null) return;
|
|
44
|
+
_isLoading = true;
|
|
45
|
+
notifyListeners();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
final futures = <Future>[
|
|
49
|
+
_transactionService.getTrades(investmentAccount: accountName, token: _token),
|
|
50
|
+
_transactionService.getAccountBalance(accountName, token: _token),
|
|
51
|
+
_transactionService.getAssets(investmentAccount: accountName, token: _token),
|
|
52
|
+
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
53
|
+
];
|
|
54
|
+
final results = await Future.wait(futures);
|
|
55
|
+
|
|
56
|
+
_trades = results[0] as List<Map<String, dynamic>>;
|
|
57
|
+
_cashBalance = results[1] as double;
|
|
58
|
+
_assets = results[2] as List<Map<String, dynamic>>;
|
|
59
|
+
final totalPortfolioBookCost = results[3] as double;
|
|
60
|
+
|
|
61
|
+
_calculateAndApplyPortfolioSummary(totalPortfolioBookCost);
|
|
62
|
+
|
|
63
|
+
await _fetchLivePrices();
|
|
64
|
+
|
|
65
|
+
} catch (e) {
|
|
66
|
+
debugPrint('Error fetching data: $e');
|
|
67
|
+
} finally {
|
|
68
|
+
_isLoading = false;
|
|
69
|
+
notifyListeners();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Future<void> _fetchLivePrices() async {
|
|
74
|
+
if (_portfolioSummary.isEmpty) return;
|
|
75
|
+
_isFetchingPrices = true;
|
|
76
|
+
notifyListeners();
|
|
77
|
+
|
|
78
|
+
final idsToFetch = _portfolioSummary
|
|
79
|
+
.map((p) => p['id_crypto'] as String? ?? p['symbol'] as String)
|
|
80
|
+
.where((s) => s.isNotEmpty)
|
|
81
|
+
.toList();
|
|
82
|
+
|
|
83
|
+
if (idsToFetch.isEmpty) {
|
|
84
|
+
_isFetchingPrices = false;
|
|
85
|
+
notifyListeners();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
final livePrices = await _cryptoService.getLiveCryptoPrices(idsToFetch);
|
|
90
|
+
_livePrices = livePrices;
|
|
91
|
+
|
|
92
|
+
_recalculateMarketValue();
|
|
93
|
+
|
|
94
|
+
_isFetchingPrices = false;
|
|
95
|
+
notifyListeners();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void _calculateAndApplyPortfolioSummary(double totalPortfolioBookCost) {
|
|
99
|
+
Map<String, dynamic> summary = {};
|
|
100
|
+
double accountPortfolioBookCost = 0;
|
|
101
|
+
|
|
102
|
+
for (var trade in _trades) {
|
|
103
|
+
final asset = trade['asset'];
|
|
104
|
+
if (asset == null || asset['symbol'] == null) continue;
|
|
105
|
+
String symbol = asset['symbol'];
|
|
106
|
+
String idCrypto = asset['id_crypto'] ?? symbol;
|
|
107
|
+
double shares = double.parse(trade['shares'].toString());
|
|
108
|
+
double price = double.parse(trade['price'].toString());
|
|
109
|
+
String tradeType = trade['trade_type'];
|
|
110
|
+
|
|
111
|
+
if (!summary.containsKey(symbol)) {
|
|
112
|
+
summary[symbol] = {
|
|
113
|
+
'symbol': symbol,
|
|
114
|
+
'id_crypto': idCrypto,
|
|
115
|
+
'name': asset['name'] ?? symbol,
|
|
116
|
+
'shares': 0.0,
|
|
117
|
+
'total_cost': 0.0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (tradeType == 'buy') {
|
|
122
|
+
summary[symbol]['shares'] += shares;
|
|
123
|
+
summary[symbol]['total_cost'] += shares * price;
|
|
124
|
+
} else if (tradeType == 'sell') {
|
|
125
|
+
double originalShares = summary[symbol]['shares'];
|
|
126
|
+
if (originalShares > 0) {
|
|
127
|
+
double avgPrice = summary[symbol]['total_cost'] / originalShares;
|
|
128
|
+
summary[symbol]['total_cost'] -= shares * avgPrice;
|
|
129
|
+
}
|
|
130
|
+
summary[symbol]['shares'] -= shares;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
135
|
+
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
136
|
+
|
|
137
|
+
List<Map<String, dynamic>> result = [];
|
|
138
|
+
summary.forEach((symbol, data) {
|
|
139
|
+
double shares = data['shares'];
|
|
140
|
+
double totalCost = data['total_cost'];
|
|
141
|
+
data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
|
|
142
|
+
data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
|
|
143
|
+
data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
144
|
+
result.add(data);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
_portfolioSummary = result;
|
|
148
|
+
_recalculateMarketValue();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
void _recalculateMarketValue() {
|
|
152
|
+
double newTotalValue = _cashBalance;
|
|
153
|
+
for (final position in _portfolioSummary) {
|
|
154
|
+
final shares = double.parse(position['shares'].toString());
|
|
155
|
+
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
156
|
+
final livePrice = _livePrices[idForLookup];
|
|
157
|
+
if (livePrice != null) {
|
|
158
|
+
newTotalValue += shares * livePrice;
|
|
159
|
+
} else {
|
|
160
|
+
newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
_accountTotalValue = newTotalValue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Future<bool> deleteTrade(int tradeId) async {
|
|
167
|
+
if (_token == null) return false;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
171
|
+
if (success) {
|
|
172
|
+
fetchData();
|
|
173
|
+
}
|
|
174
|
+
return success;
|
|
175
|
+
} catch (e) {
|
|
176
|
+
debugPrint("Error deleting trade: $e");
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|