@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
|
@@ -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 =
|
|
191
|
-
final double unrealizedPL = summary['unrealized_pl'];
|
|
98
|
+
final summary = viewModel.sinteticoSummary.first;
|
|
99
|
+
final double unrealizedPL = double.tryParse(summary['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
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))),
|
|
116
|
+
Text(_formatCurrency(double.tryParse(summary['user_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
213
117
|
DataCell(Text(
|
|
214
|
-
|
|
215
|
-
DataCell(
|
|
216
|
-
|
|
217
|
-
|
|
118
|
+
_formatCurrency(double.tryParse(summary['company_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
119
|
+
DataCell(
|
|
120
|
+
Text(_formatCurrency(double.tryParse(summary['total_contributed']?.toString() ?? '0.0') ?? 0.0))),
|
|
121
|
+
DataCell(Text(_formatCurrency(unrealizedPL),
|
|
122
|
+
style: TextStyle(color: plColor))),
|
|
123
|
+
DataCell(
|
|
124
|
+
Text('${(double.tryParse(summary['percent_return']?.toString() ?? '0.0') ?? 0.0).toStringAsFixed(2)}%',
|
|
125
|
+
style: TextStyle(color: plColor)),
|
|
126
|
+
),
|
|
127
|
+
DataCell(
|
|
128
|
+
Text(_formatCurrency(double.tryParse(summary['market_value']?.toString() ?? '0.0') ?? 0.0))),
|
|
129
|
+
DataCell(
|
|
130
|
+
Text(
|
|
131
|
+
'${(double.tryParse(summary['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0).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,21 +159,21 @@ 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
|
-
final rrspAmount = double.
|
|
249
|
-
final dpspAmount = double.
|
|
164
|
+
final rrspAmount = double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
165
|
+
final dpspAmount = double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
250
166
|
final totalContributed = rrspAmount + dpspAmount;
|
|
251
167
|
|
|
252
|
-
final returnAmount =
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
168
|
+
final returnAmount = double.tryParse(c['return_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
169
|
+
final percentReturn = totalContributed > 0
|
|
170
|
+
? (returnAmount / totalContributed) * 100
|
|
171
|
+
: 0.0;
|
|
256
172
|
|
|
257
173
|
final cumulativeValue = totalContributed + returnAmount;
|
|
258
174
|
|
|
259
|
-
final portfolioAllocation =
|
|
260
|
-
? (cumulativeValue /
|
|
175
|
+
final portfolioAllocation = viewModel.totalPortfolioBookCost > 0
|
|
176
|
+
? (cumulativeValue / viewModel.totalPortfolioBookCost) * 100
|
|
261
177
|
: 0.0;
|
|
262
178
|
|
|
263
179
|
String dateStr;
|
|
@@ -283,7 +199,8 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
283
199
|
DataCell(
|
|
284
200
|
IconButton(
|
|
285
201
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
286
|
-
onPressed: () => _confirmAndDeleteContribution(
|
|
202
|
+
onPressed: () => _confirmAndDeleteContribution(
|
|
203
|
+
context, viewModel, contributionId),
|
|
287
204
|
tooltip: 'Delete Contribution',
|
|
288
205
|
),
|
|
289
206
|
),
|
|
@@ -293,34 +210,22 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
293
210
|
);
|
|
294
211
|
}
|
|
295
212
|
|
|
296
|
-
Future<void> _confirmAndDeleteContribution(
|
|
213
|
+
Future<void> _confirmAndDeleteContribution(BuildContext context,
|
|
214
|
+
RrspSunLifeViewModel viewModel, int contributionId) async {
|
|
297
215
|
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(
|
|
298
216
|
context,
|
|
299
|
-
'this contribution',
|
|
217
|
+
'this contribution',
|
|
300
218
|
);
|
|
301
219
|
|
|
302
220
|
if (confirm == true) {
|
|
303
|
-
|
|
221
|
+
final success = await viewModel.deleteContribution(contributionId);
|
|
222
|
+
if (ScaffoldMessenger.of(context).mounted) {
|
|
304
223
|
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')),
|
|
224
|
+
SnackBar(
|
|
225
|
+
content: Text(success
|
|
226
|
+
? 'Contribution deleted successfully.'
|
|
227
|
+
: 'Failed to delete contribution.'),
|
|
228
|
+
),
|
|
324
229
|
);
|
|
325
230
|
}
|
|
326
231
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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 = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
|
|
58
|
+
_assets = results[2] as List<Map<String, dynamic>>;
|
|
59
|
+
final totalPortfolioBookCost = double.tryParse(results[3]?.toString() ?? '0.0') ?? 0.0;
|
|
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.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
108
|
+
double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
|
|
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.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
155
|
+
final totalCost = double.tryParse(position['total_cost']?.toString() ?? '0.0') ?? 0.0;
|
|
156
|
+
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
157
|
+
final livePrice = _livePrices[idForLookup];
|
|
158
|
+
|
|
159
|
+
double currentMarketValue;
|
|
160
|
+
if (livePrice != null) {
|
|
161
|
+
currentMarketValue = shares * livePrice;
|
|
162
|
+
newTotalValue += currentMarketValue;
|
|
163
|
+
} else {
|
|
164
|
+
currentMarketValue = totalCost;
|
|
165
|
+
newTotalValue += totalCost;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
double totalReturnValue = currentMarketValue - totalCost;
|
|
169
|
+
double percentageReturn = (totalCost > 0) ? (totalReturnValue / totalCost) * 100 : 0.0;
|
|
170
|
+
|
|
171
|
+
position['market_value'] = currentMarketValue;
|
|
172
|
+
position['total_return_value'] = totalReturnValue;
|
|
173
|
+
position['percentage_return'] = percentageReturn;
|
|
174
|
+
}
|
|
175
|
+
_accountTotalValue = newTotalValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Future<bool> deleteTrade(int tradeId) async {
|
|
179
|
+
if (_token == null) return false;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
183
|
+
if (success) {
|
|
184
|
+
fetchData();
|
|
185
|
+
}
|
|
186
|
+
return success;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
debugPrint("Error deleting trade: $e");
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|