@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
|
@@ -1,197 +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';
|
|
7
|
-
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.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';
|
|
8
8
|
|
|
9
|
-
class RrspSunLifeScreen extends
|
|
9
|
+
class RrspSunLifeScreen extends StatelessWidget {
|
|
10
10
|
const RrspSunLifeScreen({Key? key}) : super(key: key);
|
|
11
11
|
|
|
12
|
-
@override
|
|
13
|
-
_RrspSunLifeScreenState createState() => _RrspSunLifeScreenState();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
17
|
-
final TransactionService _transactionService = TransactionService();
|
|
18
|
-
List<Map<String, dynamic>> _contributions = [];
|
|
19
|
-
List<Map<String, dynamic>> _sinteticoSummary = [];
|
|
20
|
-
double _rrspTotalValue = 0.0;
|
|
21
|
-
double _rrspCashBalance = 0.0;
|
|
22
|
-
double _totalPortfolioBookCost = 0.0;
|
|
23
|
-
bool _isLoading = true;
|
|
24
|
-
String? _token;
|
|
25
|
-
|
|
26
|
-
@override
|
|
27
|
-
void initState() {
|
|
28
|
-
super.initState();
|
|
29
|
-
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
30
|
-
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
|
31
|
-
setState(() {
|
|
32
|
-
_token = authProvider.token;
|
|
33
|
-
});
|
|
34
|
-
_fetchData();
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
String currencySymbol = r'$'; // ou poderia vir de configuração, API, etc.
|
|
39
|
-
|
|
40
|
-
String _formatCurrency(double value) {
|
|
41
|
-
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
Future<void> _fetchData() async {
|
|
45
|
-
if (_token == null) return;
|
|
46
|
-
if (mounted) {
|
|
47
|
-
setState(() {
|
|
48
|
-
_isLoading = true;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
final contributions = await _transactionService.getRrspContributions(
|
|
54
|
-
investmentAccount: 'RRSP Sun Life', token: _token);
|
|
55
|
-
final balance =
|
|
56
|
-
await _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
|
|
57
|
-
final totalPortfolioBookCost =
|
|
58
|
-
await _transactionService.getTotalPortfolioBookCost(token: _token);
|
|
59
|
-
|
|
60
|
-
final sinteticoSummaryData =
|
|
61
|
-
_calculateSinteticoSummary(contributions, totalPortfolioBookCost);
|
|
62
|
-
|
|
63
|
-
if (!mounted) return;
|
|
64
|
-
setState(() {
|
|
65
|
-
_contributions = contributions;
|
|
66
|
-
_sinteticoSummary = sinteticoSummaryData['summary'];
|
|
67
|
-
_rrspCashBalance = balance;
|
|
68
|
-
_rrspTotalValue = sinteticoSummaryData['total_value'];
|
|
69
|
-
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
70
|
-
});
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (!mounted) return;
|
|
73
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
74
|
-
SnackBar(content: Text('Error fetching RRSP data: $e')),
|
|
75
|
-
);
|
|
76
|
-
} finally {
|
|
77
|
-
if (mounted) {
|
|
78
|
-
setState(() {
|
|
79
|
-
_isLoading = false;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
Map<String, dynamic> _calculateSinteticoSummary(
|
|
86
|
-
List<Map<String, dynamic>> contributions, double totalPortfolioBookCost) {
|
|
87
|
-
double totalUserContribution = 0;
|
|
88
|
-
double totalCompanyContribution = 0;
|
|
89
|
-
double totalUnrealizedPL = 0;
|
|
90
|
-
|
|
91
|
-
for (var c in contributions) {
|
|
92
|
-
totalUserContribution += double.parse(c['rrsp_amount'].toString());
|
|
93
|
-
totalCompanyContribution += double.parse(c['dpsp_amount'].toString());
|
|
94
|
-
totalUnrealizedPL += double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
final totalContributed = totalUserContribution + totalCompanyContribution;
|
|
98
|
-
final marketValue = totalContributed + totalUnrealizedPL;
|
|
99
|
-
|
|
100
|
-
final summaryData = {
|
|
101
|
-
'user_contribution': totalUserContribution,
|
|
102
|
-
'company_contribution': totalCompanyContribution,
|
|
103
|
-
'total_contributed': totalContributed,
|
|
104
|
-
'unrealized_pl': totalUnrealizedPL,
|
|
105
|
-
'percent_return':
|
|
106
|
-
(totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
|
|
107
|
-
'market_value': marketValue,
|
|
108
|
-
'portfolio_allocation_percent': (totalPortfolioBookCost > 0)
|
|
109
|
-
? (marketValue / totalPortfolioBookCost) * 100
|
|
110
|
-
: 0.0,
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
'summary': [summaryData],
|
|
115
|
-
'total_value': marketValue
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
12
|
@override
|
|
120
13
|
Widget build(BuildContext context) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
tooltip: 'Refresh Data',
|
|
149
|
-
),
|
|
150
|
-
],
|
|
151
|
-
),
|
|
152
|
-
body: _isLoading
|
|
153
|
-
? const Center(child: CircularProgressIndicator())
|
|
154
|
-
: RefreshIndicator(
|
|
155
|
-
onRefresh: _fetchData,
|
|
156
|
-
child: SingleChildScrollView(
|
|
157
|
-
padding: const EdgeInsets.all(16.0),
|
|
158
|
-
child: Column(
|
|
159
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
160
|
-
children: [
|
|
161
|
-
Text('Summary',
|
|
162
|
-
style: Theme.of(context).textTheme.titleLarge),
|
|
163
|
-
const SizedBox(height: 8),
|
|
164
|
-
_buildSinteticoTable(),
|
|
165
|
-
const SizedBox(height: 24),
|
|
166
|
-
RrspContributionForm(
|
|
167
|
-
investmentAccount: 'RRSP Sun Life',
|
|
168
|
-
onContributionLogged: () => _fetchData(),
|
|
169
|
-
token: _token,
|
|
170
|
-
),
|
|
171
|
-
const SizedBox(height: 24),
|
|
172
|
-
Text(
|
|
173
|
-
'Contribution History',
|
|
174
|
-
style: Theme.of(context).textTheme.titleLarge,
|
|
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
|
+
],
|
|
175
41
|
),
|
|
176
|
-
|
|
177
|
-
_buildContributionTable(),
|
|
178
|
-
],
|
|
42
|
+
),
|
|
179
43
|
),
|
|
180
|
-
|
|
44
|
+
IconButton(
|
|
45
|
+
icon: const Icon(Icons.refresh),
|
|
46
|
+
onPressed: viewModel.fetchData,
|
|
47
|
+
tooltip: 'Refresh Data',
|
|
48
|
+
),
|
|
49
|
+
],
|
|
181
50
|
),
|
|
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
|
+
),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
),
|
|
182
84
|
);
|
|
183
85
|
}
|
|
184
86
|
|
|
185
|
-
|
|
186
|
-
|
|
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) {
|
|
187
95
|
return const SizedBox.shrink();
|
|
188
96
|
}
|
|
189
97
|
|
|
190
|
-
final summary =
|
|
98
|
+
final summary = viewModel.sinteticoSummary.first;
|
|
191
99
|
final double unrealizedPL = summary['unrealized_pl'];
|
|
192
100
|
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
193
101
|
|
|
194
|
-
|
|
195
102
|
return DataTable(
|
|
196
103
|
columns: const [
|
|
197
104
|
DataColumn(label: Text('User Contr.')),
|
|
@@ -205,24 +112,33 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
205
112
|
rows: [
|
|
206
113
|
DataRow(
|
|
207
114
|
cells: [
|
|
208
|
-
DataCell(Text(_formatCurrency(summary['user_contribution'] as double))),
|
|
209
|
-
DataCell(Text(_formatCurrency(summary['company_contribution'] as double))),
|
|
210
115
|
DataCell(
|
|
211
|
-
Text(_formatCurrency(summary['
|
|
212
|
-
DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
|
|
213
|
-
DataCell(Text(
|
|
214
|
-
'${(summary['percent_return'] as double).toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
|
|
215
|
-
DataCell(Text(_formatCurrency(summary['market_value'] as double))),
|
|
116
|
+
Text(_formatCurrency(summary['user_contribution'] as double))),
|
|
216
117
|
DataCell(Text(
|
|
217
|
-
|
|
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
|
+
),
|
|
218
133
|
],
|
|
219
134
|
),
|
|
220
135
|
],
|
|
221
136
|
);
|
|
222
137
|
}
|
|
223
138
|
|
|
224
|
-
Widget _buildContributionTable(
|
|
225
|
-
|
|
139
|
+
Widget _buildContributionTable(
|
|
140
|
+
BuildContext context, RrspSunLifeViewModel viewModel) {
|
|
141
|
+
if (viewModel.contributions.isEmpty) {
|
|
226
142
|
return const Center(
|
|
227
143
|
child: Padding(
|
|
228
144
|
padding: EdgeInsets.all(16.0),
|
|
@@ -243,7 +159,7 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
243
159
|
DataColumn(label: Text('Portfolio %')),
|
|
244
160
|
DataColumn(label: Text('Actions')),
|
|
245
161
|
],
|
|
246
|
-
rows:
|
|
162
|
+
rows: viewModel.contributions.map((c) {
|
|
247
163
|
final contributionId = c['id'] as int;
|
|
248
164
|
final rrspAmount = double.parse(c['rrsp_amount'].toString());
|
|
249
165
|
final dpspAmount = double.parse(c['dpsp_amount'].toString());
|
|
@@ -251,13 +167,14 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
251
167
|
|
|
252
168
|
final returnAmount =
|
|
253
169
|
double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
254
|
-
final percentReturn =
|
|
255
|
-
|
|
170
|
+
final percentReturn = totalContributed > 0
|
|
171
|
+
? (returnAmount / totalContributed) * 100
|
|
172
|
+
: 0.0;
|
|
256
173
|
|
|
257
174
|
final cumulativeValue = totalContributed + returnAmount;
|
|
258
175
|
|
|
259
|
-
final portfolioAllocation =
|
|
260
|
-
? (cumulativeValue /
|
|
176
|
+
final portfolioAllocation = viewModel.totalPortfolioBookCost > 0
|
|
177
|
+
? (cumulativeValue / viewModel.totalPortfolioBookCost) * 100
|
|
261
178
|
: 0.0;
|
|
262
179
|
|
|
263
180
|
String dateStr;
|
|
@@ -283,7 +200,8 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
283
200
|
DataCell(
|
|
284
201
|
IconButton(
|
|
285
202
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
286
|
-
onPressed: () => _confirmAndDeleteContribution(
|
|
203
|
+
onPressed: () => _confirmAndDeleteContribution(
|
|
204
|
+
context, viewModel, contributionId),
|
|
287
205
|
tooltip: 'Delete Contribution',
|
|
288
206
|
),
|
|
289
207
|
),
|
|
@@ -293,34 +211,22 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
293
211
|
);
|
|
294
212
|
}
|
|
295
213
|
|
|
296
|
-
Future<void> _confirmAndDeleteContribution(
|
|
214
|
+
Future<void> _confirmAndDeleteContribution(BuildContext context,
|
|
215
|
+
RrspSunLifeViewModel viewModel, int contributionId) async {
|
|
297
216
|
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(
|
|
298
217
|
context,
|
|
299
|
-
'this contribution',
|
|
218
|
+
'this contribution',
|
|
300
219
|
);
|
|
301
220
|
|
|
302
221
|
if (confirm == true) {
|
|
303
|
-
|
|
222
|
+
final success = await viewModel.deleteContribution(contributionId);
|
|
223
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
304
224
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
|
|
311
|
-
if (success) {
|
|
312
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
313
|
-
const SnackBar(content: Text('Contribution deleted successfully.')),
|
|
314
|
-
);
|
|
315
|
-
_fetchData(); // Refresh data after deletion
|
|
316
|
-
} else {
|
|
317
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
318
|
-
const SnackBar(content: Text('Failed to delete contribution.')),
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
} catch (e) {
|
|
322
|
-
ScaffoldMessenger.of(context).showSnackBar(
|
|
323
|
-
SnackBar(content: Text('Error deleting contribution: $e')),
|
|
225
|
+
SnackBar(
|
|
226
|
+
content: Text(success
|
|
227
|
+
? 'Contribution deleted successfully.'
|
|
228
|
+
: 'Failed to delete contribution.'),
|
|
229
|
+
),
|
|
324
230
|
);
|
|
325
231
|
}
|
|
326
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
|
+
}
|