@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.
@@ -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'; // Added import for AppDialogs
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 StatefulWidget {
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
- return Scaffold(
122
- appBar: AppBar(
123
- title: const Text('RRSP Sun Life Portfolio'),
124
- actions: [
125
- Center(
126
- child: Padding(
127
- padding: const EdgeInsets.only(right: 16.0),
128
- child: Column(
129
- mainAxisAlignment: MainAxisAlignment.center,
130
- crossAxisAlignment: CrossAxisAlignment.end,
131
- children: [
132
- Text(
133
- 'Total Portfolio: ${_formatCurrency(_rrspTotalValue)}',
134
- style: const TextStyle(
135
- fontSize: 16, fontWeight: FontWeight.bold),
136
- ),
137
- Text(
138
- 'Account Balance: ${_formatCurrency(_rrspCashBalance)}',
139
- style: const TextStyle(fontSize: 12),
140
- ),
141
- ],
142
- ),
143
- ),
144
- ),
145
- IconButton(
146
- icon: const Icon(Icons.refresh),
147
- onPressed: _fetchData,
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
- const SizedBox(height: 8),
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
- Widget _buildSinteticoTable() {
186
- if (_sinteticoSummary.isEmpty) {
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 = _sinteticoSummary.first;
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['total_contributed'] as double))),
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
- '${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%')),
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
- if (_contributions.isEmpty) {
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: _contributions.map((c) {
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
- totalContributed > 0 ? (returnAmount / totalContributed) * 100 : 0.0;
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 = _totalPortfolioBookCost > 0
260
- ? (cumulativeValue / _totalPortfolioBookCost) * 100
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(contributionId),
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(int contributionId) async {
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', // A generic string for the item being deleted
218
+ 'this contribution',
300
219
  );
301
220
 
302
221
  if (confirm == true) {
303
- if (_token == null) {
222
+ final success = await viewModel.deleteContribution(contributionId);
223
+ if (ScaffoldMessenger.of(context).mounted) {
304
224
  ScaffoldMessenger.of(context).showSnackBar(
305
- const SnackBar(content: Text('Authentication token not available.')),
306
- );
307
- return;
308
- }
309
- try {
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
+ }