@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:personal_finance_frontend_core_services/services/stock_service.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
4
|
+
|
|
5
|
+
class InvestmentAccountViewModel extends ChangeNotifier {
|
|
6
|
+
final TransactionService _transactionService = TransactionService();
|
|
7
|
+
final StockService _stockService = StockService();
|
|
8
|
+
|
|
9
|
+
final String accountName;
|
|
10
|
+
final bool showDividends;
|
|
11
|
+
String? _token;
|
|
12
|
+
|
|
13
|
+
List<Map<String, dynamic>> _trades = [];
|
|
14
|
+
List<Map<String, dynamic>> _dividends = [];
|
|
15
|
+
List<Map<String, dynamic>> _assets = [];
|
|
16
|
+
List<Map<String, dynamic>> _portfolioSummary = [];
|
|
17
|
+
Map<String, double> _livePrices = {};
|
|
18
|
+
double _cashBalance = 0.0;
|
|
19
|
+
double _accountTotalValue = 0.0;
|
|
20
|
+
double _totalPortfolioBookCost = 0.0;
|
|
21
|
+
bool _isLoading = true;
|
|
22
|
+
bool _isFetchingPrices = false;
|
|
23
|
+
String? _selectedSymbolForFilter;
|
|
24
|
+
|
|
25
|
+
// Getters
|
|
26
|
+
List<Map<String, dynamic>> get trades => _trades;
|
|
27
|
+
List<Map<String, dynamic>> get dividends => _dividends;
|
|
28
|
+
List<Map<String, dynamic>> get assets => _assets;
|
|
29
|
+
List<Map<String, dynamic>> get portfolioSummary => _portfolioSummary;
|
|
30
|
+
Map<String, double> get livePrices => _livePrices;
|
|
31
|
+
double get cashBalance => _cashBalance;
|
|
32
|
+
double get accountTotalValue => _accountTotalValue;
|
|
33
|
+
bool get isLoading => _isLoading;
|
|
34
|
+
bool get isFetchingPrices => _isFetchingPrices;
|
|
35
|
+
String? get selectedSymbolForFilter => _selectedSymbolForFilter;
|
|
36
|
+
String? get token => _token;
|
|
37
|
+
|
|
38
|
+
InvestmentAccountViewModel({required this.accountName, required this.showDividends, required String? token}) {
|
|
39
|
+
_token = token;
|
|
40
|
+
fetchData();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void setTokenAndFetch(String? token) {
|
|
44
|
+
_token = token;
|
|
45
|
+
fetchData();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void setSymbolFilter(String? symbol) {
|
|
49
|
+
_selectedSymbolForFilter = symbol;
|
|
50
|
+
notifyListeners();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Future<void> fetchData() async {
|
|
54
|
+
if (_token == null) return;
|
|
55
|
+
_isLoading = true;
|
|
56
|
+
notifyListeners();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
final futures = <Future>[
|
|
60
|
+
_transactionService.getTrades(investmentAccount: accountName, token: _token),
|
|
61
|
+
_transactionService.getAccountBalance(accountName, token: _token),
|
|
62
|
+
_transactionService.getAssets(investmentAccount: accountName, token: _token),
|
|
63
|
+
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
64
|
+
];
|
|
65
|
+
if (showDividends) {
|
|
66
|
+
futures.add(_transactionService.getDividends(investmentAccount: accountName, token: _token));
|
|
67
|
+
}
|
|
68
|
+
final results = await Future.wait(futures);
|
|
69
|
+
|
|
70
|
+
_trades = results[0] as List<Map<String, dynamic>>;
|
|
71
|
+
_cashBalance = results[1] as double;
|
|
72
|
+
_assets = results[2] as List<Map<String, dynamic>>;
|
|
73
|
+
_totalPortfolioBookCost = results[3] as double;
|
|
74
|
+
_dividends = showDividends ? results[4] as List<Map<String, dynamic>> : <Map<String, dynamic>>[];
|
|
75
|
+
|
|
76
|
+
_calculateAndApplyPortfolioSummary();
|
|
77
|
+
|
|
78
|
+
await _fetchLivePrices();
|
|
79
|
+
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Handle error appropriately
|
|
82
|
+
debugPrint('Error fetching data: $e');
|
|
83
|
+
} finally {
|
|
84
|
+
_isLoading = false;
|
|
85
|
+
notifyListeners();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Future<void> _fetchLivePrices() async {
|
|
90
|
+
if (_portfolioSummary.isEmpty) return;
|
|
91
|
+
_isFetchingPrices = true;
|
|
92
|
+
notifyListeners();
|
|
93
|
+
|
|
94
|
+
final symbolsToFetch = _portfolioSummary
|
|
95
|
+
.map((p) => p['symbol'] as String)
|
|
96
|
+
.where((s) => s.isNotEmpty)
|
|
97
|
+
.toList();
|
|
98
|
+
|
|
99
|
+
if (symbolsToFetch.isEmpty) {
|
|
100
|
+
_isFetchingPrices = false;
|
|
101
|
+
notifyListeners();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
final livePrices = await _stockService.getLivePrices(symbolsToFetch);
|
|
106
|
+
_livePrices = livePrices;
|
|
107
|
+
|
|
108
|
+
_recalculateMarketValues();
|
|
109
|
+
|
|
110
|
+
_isFetchingPrices = false;
|
|
111
|
+
notifyListeners();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
void _calculateAndApplyPortfolioSummary() {
|
|
115
|
+
Map<String, dynamic> summary = {};
|
|
116
|
+
double accountTotalBookCost = 0;
|
|
117
|
+
|
|
118
|
+
Map<String, double> dividendSummary = {};
|
|
119
|
+
if (showDividends) {
|
|
120
|
+
for (var dividend in _dividends) {
|
|
121
|
+
final asset = dividend['asset'];
|
|
122
|
+
if (asset != null && asset['symbol'] != null) {
|
|
123
|
+
String symbol = asset['symbol'];
|
|
124
|
+
double amount = double.parse(dividend['amount'].toString());
|
|
125
|
+
dividendSummary.update(symbol, (value) => value + amount, ifAbsent: () => amount);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (var trade in _trades) {
|
|
131
|
+
final asset = trade['asset'];
|
|
132
|
+
if (asset == null || asset['symbol'] == null) continue;
|
|
133
|
+
String symbol = asset['symbol'];
|
|
134
|
+
|
|
135
|
+
double shares = double.parse(trade['shares'].toString());
|
|
136
|
+
double price = double.parse(trade['price'].toString());
|
|
137
|
+
String tradeType = trade['trade_type'];
|
|
138
|
+
|
|
139
|
+
if (!summary.containsKey(symbol)) {
|
|
140
|
+
summary[symbol] = {
|
|
141
|
+
'symbol': symbol,
|
|
142
|
+
'name': asset['name'] ?? symbol,
|
|
143
|
+
'industry': asset['industry'] ?? 'N/A',
|
|
144
|
+
'shares': 0.0,
|
|
145
|
+
'book_cost': 0.0,
|
|
146
|
+
'total_dividends': dividendSummary[symbol] ?? 0.0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (tradeType == 'buy') {
|
|
151
|
+
summary[symbol]['shares'] += shares;
|
|
152
|
+
summary[symbol]['book_cost'] += shares * price;
|
|
153
|
+
} else if (tradeType == 'sell') {
|
|
154
|
+
double originalShares = summary[symbol]['shares'];
|
|
155
|
+
if (originalShares > 0) {
|
|
156
|
+
double avgPrice = summary[symbol]['book_cost'] / originalShares;
|
|
157
|
+
summary[symbol]['book_cost'] -= shares * avgPrice;
|
|
158
|
+
}
|
|
159
|
+
summary[symbol]['shares'] -= shares;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
164
|
+
|
|
165
|
+
accountTotalBookCost = summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
166
|
+
|
|
167
|
+
List<Map<String, dynamic>> result = [];
|
|
168
|
+
summary.forEach((symbol, data) {
|
|
169
|
+
double shares = data['shares'];
|
|
170
|
+
double bookCost = data['book_cost'];
|
|
171
|
+
|
|
172
|
+
data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
|
|
173
|
+
data['account_allocation_percent'] = (accountTotalBookCost > 0) ? (bookCost / accountTotalBookCost) * 100 : 0.0;
|
|
174
|
+
data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0) ? (bookCost / _totalPortfolioBookCost) * 100 : 0.0;
|
|
175
|
+
|
|
176
|
+
result.add(data);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
_portfolioSummary = result;
|
|
180
|
+
_recalculateMarketValues(); // Initial calculation with book cost as fallback
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void _recalculateMarketValues() {
|
|
184
|
+
double newTotalValue = _cashBalance;
|
|
185
|
+
for (final position in _portfolioSummary) {
|
|
186
|
+
final symbol = position['symbol'];
|
|
187
|
+
final shares = position['shares'] as double;
|
|
188
|
+
final bookCost = position['book_cost'] as double;
|
|
189
|
+
final totalDividends = position['total_dividends'] as double;
|
|
190
|
+
|
|
191
|
+
final livePrice = _livePrices[symbol];
|
|
192
|
+
double marketValue = livePrice != null ? shares * livePrice : bookCost;
|
|
193
|
+
double unrealizedPL = marketValue - bookCost;
|
|
194
|
+
double totalReturn = unrealizedPL + totalDividends;
|
|
195
|
+
|
|
196
|
+
position['market_value'] = marketValue;
|
|
197
|
+
position['unrealized_pl'] = unrealizedPL;
|
|
198
|
+
position['total_return'] = totalReturn;
|
|
199
|
+
position['percent_unrealized_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
200
|
+
position['percent_total_return'] = (bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
|
|
201
|
+
|
|
202
|
+
newTotalValue += marketValue;
|
|
203
|
+
}
|
|
204
|
+
_accountTotalValue = newTotalValue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
Future<bool> deleteTrade(int tradeId) async {
|
|
208
|
+
if (_token == null) return false;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
final success = await _transactionService.deleteTrade(tradeId, token: _token);
|
|
212
|
+
if (success) {
|
|
213
|
+
fetchData(); // Refresh data
|
|
214
|
+
}
|
|
215
|
+
return success;
|
|
216
|
+
} catch (e) {
|
|
217
|
+
debugPrint("Error deleting trade: $e");
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
3
|
+
|
|
4
|
+
class RrspSunLifeViewModel extends ChangeNotifier {
|
|
5
|
+
final TransactionService _transactionService = TransactionService();
|
|
6
|
+
String? _token;
|
|
7
|
+
|
|
8
|
+
List<Map<String, dynamic>> _contributions = [];
|
|
9
|
+
List<Map<String, dynamic>> _sinteticoSummary = [];
|
|
10
|
+
double _rrspTotalValue = 0.0;
|
|
11
|
+
double _rrspCashBalance = 0.0;
|
|
12
|
+
double _totalPortfolioBookCost = 0.0;
|
|
13
|
+
bool _isLoading = true;
|
|
14
|
+
|
|
15
|
+
// Getters
|
|
16
|
+
List<Map<String, dynamic>> get contributions => _contributions;
|
|
17
|
+
List<Map<String, dynamic>> get sinteticoSummary => _sinteticoSummary;
|
|
18
|
+
double get rrspTotalValue => _rrspTotalValue;
|
|
19
|
+
double get rrspCashBalance => _rrspCashBalance;
|
|
20
|
+
double get totalPortfolioBookCost => _totalPortfolioBookCost;
|
|
21
|
+
bool get isLoading => _isLoading;
|
|
22
|
+
String? get token => _token;
|
|
23
|
+
|
|
24
|
+
RrspSunLifeViewModel({required String? token}) {
|
|
25
|
+
_token = token;
|
|
26
|
+
fetchData();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void setTokenAndFetch(String? token) {
|
|
30
|
+
_token = token;
|
|
31
|
+
fetchData();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Future<void> fetchData() async {
|
|
35
|
+
if (_token == null) return;
|
|
36
|
+
_isLoading = true;
|
|
37
|
+
notifyListeners();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
final contributionsFuture = _transactionService.getRrspContributions(
|
|
41
|
+
investmentAccount: 'RRSP Sun Life', token: _token);
|
|
42
|
+
final balanceFuture = _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
|
|
43
|
+
final totalPortfolioBookCostFuture = _transactionService.getTotalPortfolioBookCost(token: _token);
|
|
44
|
+
|
|
45
|
+
final results = await Future.wait([contributionsFuture, balanceFuture, totalPortfolioBookCostFuture]);
|
|
46
|
+
|
|
47
|
+
_contributions = results[0] as List<Map<String, dynamic>>;
|
|
48
|
+
_rrspCashBalance = results[1] as double;
|
|
49
|
+
_totalPortfolioBookCost = results[2] as double;
|
|
50
|
+
|
|
51
|
+
_calculateAndApplySinteticoSummary();
|
|
52
|
+
|
|
53
|
+
} catch (e) {
|
|
54
|
+
debugPrint('Error fetching RRSP data: $e');
|
|
55
|
+
} finally {
|
|
56
|
+
_isLoading = false;
|
|
57
|
+
notifyListeners();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void _calculateAndApplySinteticoSummary() {
|
|
62
|
+
double totalUserContribution = 0;
|
|
63
|
+
double totalCompanyContribution = 0;
|
|
64
|
+
double totalUnrealizedPL = 0;
|
|
65
|
+
|
|
66
|
+
for (var c in _contributions) {
|
|
67
|
+
totalUserContribution += double.parse(c['rrsp_amount'].toString());
|
|
68
|
+
totalCompanyContribution += double.parse(c['dpsp_amount'].toString());
|
|
69
|
+
totalUnrealizedPL += double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
final totalContributed = totalUserContribution + totalCompanyContribution;
|
|
73
|
+
final marketValue = totalContributed + totalUnrealizedPL;
|
|
74
|
+
|
|
75
|
+
final summaryData = {
|
|
76
|
+
'user_contribution': totalUserContribution,
|
|
77
|
+
'company_contribution': totalCompanyContribution,
|
|
78
|
+
'total_contributed': totalContributed,
|
|
79
|
+
'unrealized_pl': totalUnrealizedPL,
|
|
80
|
+
'percent_return': (totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
|
|
81
|
+
'market_value': marketValue,
|
|
82
|
+
'portfolio_allocation_percent': (_totalPortfolioBookCost > 0) ? (marketValue / _totalPortfolioBookCost) * 100 : 0.0,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
_sinteticoSummary = [summaryData];
|
|
86
|
+
_rrspTotalValue = marketValue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Future<bool> deleteContribution(int contributionId) async {
|
|
90
|
+
if (_token == null) return false;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
|
|
94
|
+
if (success) {
|
|
95
|
+
fetchData();
|
|
96
|
+
}
|
|
97
|
+
return success;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
debugPrint("Error deleting contribution: $e");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|