@marcos_feitoza/personal-finance-frontend-feature-investments 1.0.0 → 1.0.1
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
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [1.0.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.0...v1.0.1) (2025-11-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add crypto table update ([e2e0da7](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/e2e0da77e334dd4416d04770db50be9b91bb0c06))
|
|
7
|
+
* fix and update the tables of investments ([ff5a7b5](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/ff5a7b56d627ebf0ef128e03ff6ad6a57efda042))
|
|
8
|
+
* update rrsp sl table ([107b6dc](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/107b6dce59621e3563e589e00b21f2fbbb529921))
|
|
9
|
+
|
|
1
10
|
# 1.0.0 (2025-11-22)
|
|
2
11
|
|
|
3
12
|
|
|
@@ -58,7 +58,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
58
58
|
_transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
|
|
59
59
|
_transactionService.getAccountBalance(widget.accountName, token: _token),
|
|
60
60
|
_transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
|
|
61
|
-
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
61
|
+
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
62
62
|
];
|
|
63
63
|
final results = await Future.wait(futures);
|
|
64
64
|
|
|
@@ -67,18 +67,21 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
67
67
|
final assets = results[2] as List<Map<String, dynamic>>;
|
|
68
68
|
final totalPortfolioBookCost = results[3] as double;
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
// Initial calculation without live prices
|
|
71
|
+
final initialSummaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
71
72
|
|
|
72
73
|
setState(() {
|
|
73
74
|
_trades = trades;
|
|
74
75
|
_cashBalance = balance;
|
|
75
76
|
_assets = assets;
|
|
76
|
-
_portfolioSummary =
|
|
77
|
-
_accountTotalValue =
|
|
77
|
+
_portfolioSummary = initialSummaryData['summary'];
|
|
78
|
+
_accountTotalValue = initialSummaryData['total_value'] + _cashBalance;
|
|
78
79
|
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// Fetch live prices and recalculate summary
|
|
83
|
+
if (mounted) await _fetchLivePricesAndRecalculate();
|
|
84
|
+
|
|
82
85
|
} catch (e) {
|
|
83
86
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
84
87
|
} finally {
|
|
@@ -86,7 +89,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
Future<void>
|
|
92
|
+
Future<void> _fetchLivePricesAndRecalculate() async {
|
|
90
93
|
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
91
94
|
setState(() => _isFetchingPrices = true);
|
|
92
95
|
|
|
@@ -101,28 +104,22 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
101
104
|
|
|
102
105
|
if (!mounted) return;
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
final livePrice = livePrices[idForLookup];
|
|
109
|
-
if (livePrice != null) {
|
|
110
|
-
newTotalValue += shares * livePrice;
|
|
111
|
-
} else {
|
|
112
|
-
newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
107
|
+
// Recalculate summary with live prices
|
|
108
|
+
final summaryDataWithLivePrices = _calculatePortfolioSummary(_trades, _totalPortfolioBookCost, livePrices: livePrices);
|
|
109
|
+
|
|
110
|
+
double newTotalMarketValue = summaryDataWithLivePrices['total_value'];
|
|
115
111
|
|
|
116
112
|
setState(() {
|
|
117
113
|
_livePrices = livePrices;
|
|
118
|
-
|
|
114
|
+
_portfolioSummary = summaryDataWithLivePrices['summary'];
|
|
115
|
+
_accountTotalValue = newTotalMarketValue + _cashBalance;
|
|
119
116
|
_isFetchingPrices = false;
|
|
120
117
|
});
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
|
|
120
|
+
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost, {Map<String, double>? livePrices}) {
|
|
124
121
|
Map<String, dynamic> summary = {};
|
|
125
|
-
|
|
122
|
+
livePrices ??= _livePrices; // Use state's live prices if not provided
|
|
126
123
|
|
|
127
124
|
for (var trade in trades) {
|
|
128
125
|
final asset = trade['asset'];
|
|
@@ -140,39 +137,53 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
140
137
|
'id_crypto': idCrypto,
|
|
141
138
|
'name': asset['name'] ?? symbol,
|
|
142
139
|
'shares': 0.0,
|
|
143
|
-
'
|
|
140
|
+
'book_cost': 0.0,
|
|
144
141
|
};
|
|
145
142
|
}
|
|
146
143
|
|
|
147
144
|
if (tradeType == 'buy') {
|
|
148
145
|
summary[symbol]['shares'] += shares;
|
|
149
|
-
summary[symbol]['
|
|
146
|
+
summary[symbol]['book_cost'] += shares * price;
|
|
150
147
|
} else if (tradeType == 'sell') {
|
|
151
148
|
double originalShares = summary[symbol]['shares'];
|
|
152
149
|
if (originalShares > 0) {
|
|
153
|
-
double avgPrice = summary[symbol]['
|
|
154
|
-
summary[symbol]['
|
|
150
|
+
double avgPrice = summary[symbol]['book_cost'] / originalShares;
|
|
151
|
+
summary[symbol]['book_cost'] -= shares * avgPrice;
|
|
155
152
|
}
|
|
156
153
|
summary[symbol]['shares'] -= shares;
|
|
157
154
|
}
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
161
|
-
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
162
158
|
|
|
159
|
+
double accountTotalBookCost = summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
160
|
+
|
|
163
161
|
List<Map<String, dynamic>> result = [];
|
|
164
162
|
summary.forEach((symbol, data) {
|
|
165
163
|
double shares = data['shares'];
|
|
166
|
-
double
|
|
167
|
-
|
|
168
|
-
data['
|
|
169
|
-
data['
|
|
164
|
+
double bookCost = data['book_cost'];
|
|
165
|
+
|
|
166
|
+
data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
|
|
167
|
+
data['account_allocation_percent'] = (accountTotalBookCost > 0) ? (bookCost / accountTotalBookCost) * 100 : 0.0;
|
|
168
|
+
data['portfolio_allocation_percent'] = (totalPortfolioBookCost > 0) ? (bookCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
169
|
+
|
|
170
|
+
final idForLookup = (data['id_crypto'] as String? ?? data['symbol'] as String).toLowerCase();
|
|
171
|
+
final livePrice = livePrices![idForLookup];
|
|
172
|
+
double marketValue = livePrice != null ? shares * livePrice : bookCost;
|
|
173
|
+
double unrealizedPL = marketValue - bookCost;
|
|
174
|
+
|
|
175
|
+
data['market_value'] = marketValue;
|
|
176
|
+
data['unrealized_pl'] = unrealizedPL;
|
|
177
|
+
data['percent_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
178
|
+
|
|
170
179
|
result.add(data);
|
|
171
180
|
});
|
|
172
181
|
|
|
173
|
-
|
|
182
|
+
double accountTotalMarketValue = result.fold(0.0, (sum, item) => sum + item['market_value']);
|
|
183
|
+
return {'summary': result, 'total_value': accountTotalMarketValue};
|
|
174
184
|
}
|
|
175
185
|
|
|
186
|
+
|
|
176
187
|
@override
|
|
177
188
|
Widget build(BuildContext context) {
|
|
178
189
|
return Scaffold(
|
|
@@ -238,48 +249,43 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
238
249
|
child: DataTable(
|
|
239
250
|
columnSpacing: 24.0,
|
|
240
251
|
columns: const [
|
|
241
|
-
DataColumn(label: Text('Name')),
|
|
242
252
|
DataColumn(label: Text('Symbol')),
|
|
243
|
-
DataColumn(label: Text('
|
|
253
|
+
DataColumn(label: Text('Shares')),
|
|
244
254
|
DataColumn(label: Text('Avg Price')),
|
|
245
|
-
DataColumn(label: Text('Live')),
|
|
246
255
|
DataColumn(label: Text('Book Cost')),
|
|
247
|
-
DataColumn(label: Text('
|
|
248
|
-
DataColumn(label: Text('
|
|
249
|
-
DataColumn(label: Text('
|
|
250
|
-
DataColumn(label: Text('%
|
|
256
|
+
DataColumn(label: Text('Live Price')),
|
|
257
|
+
DataColumn(label: Text('Market Value')),
|
|
258
|
+
DataColumn(label: Text('Unrealized P/L')),
|
|
259
|
+
DataColumn(label: Text('% P/L')),
|
|
260
|
+
DataColumn(label: Text('Account %')),
|
|
261
|
+
DataColumn(label: Text('Portfolio %')),
|
|
251
262
|
],
|
|
252
263
|
rows: _portfolioSummary.map((position) {
|
|
253
|
-
final symbol = position['symbol']
|
|
254
|
-
final
|
|
255
|
-
final
|
|
256
|
-
final
|
|
257
|
-
final
|
|
258
|
-
final
|
|
259
|
-
final
|
|
260
|
-
final
|
|
261
|
-
|
|
262
|
-
double
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
totalReturnValue = currentMarketValue - totalPurchased;
|
|
267
|
-
if (totalPurchased > 0) percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
final returnColor = (totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
264
|
+
final String symbol = position['symbol'];
|
|
265
|
+
final double shares = position['shares'];
|
|
266
|
+
final double avgPrice = position['avg_price'];
|
|
267
|
+
final double bookCost = position['book_cost'];
|
|
268
|
+
final String idForLookup = (position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
269
|
+
final double? livePrice = _livePrices[idForLookup];
|
|
270
|
+
final double marketValue = position['market_value'];
|
|
271
|
+
final double unrealizedPL = position['unrealized_pl'];
|
|
272
|
+
final double percentPL = position['percent_pl'];
|
|
273
|
+
final double accountAllocation = position['account_allocation_percent'];
|
|
274
|
+
final double portfolioAllocation = position['portfolio_allocation_percent'];
|
|
275
|
+
|
|
276
|
+
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
271
277
|
|
|
272
278
|
return DataRow(cells: [
|
|
273
|
-
DataCell(SizedBox(width: 150, child: Text(position['name'] as String, overflow: TextOverflow.ellipsis))),
|
|
274
279
|
DataCell(Text(symbol)),
|
|
275
280
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
276
281
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
282
|
+
DataCell(Text(_formatCurrency(bookCost))),
|
|
277
283
|
DataCell(livePrice != null ? Text(_formatCurrency(livePrice)) : const Text('N/A')),
|
|
278
|
-
DataCell(Text(_formatCurrency(
|
|
284
|
+
DataCell(Text(_formatCurrency(marketValue))),
|
|
285
|
+
DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
|
|
286
|
+
DataCell(Text('${percentPL.toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
|
|
279
287
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
280
|
-
DataCell(Text('${
|
|
281
|
-
DataCell(totalReturnValue != null ? Text(_formatCurrency(totalReturnValue), style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
282
|
-
DataCell(percentageReturn != null ? Text('${percentageReturn.toStringAsFixed(2)}%', style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
288
|
+
DataCell(Text('${portfolioAllocation.toStringAsFixed(2)}%')),
|
|
283
289
|
]);
|
|
284
290
|
}).toList(),
|
|
285
291
|
),
|
|
@@ -141,7 +141,8 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
141
141
|
if (livePrice != null) {
|
|
142
142
|
newTotalValue += shares * livePrice;
|
|
143
143
|
} else {
|
|
144
|
-
|
|
144
|
+
// If live price is not available, use book cost as a fallback for market value
|
|
145
|
+
newTotalValue += (position['book_cost'] as num?)?.toDouble() ?? 0.0;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -155,7 +156,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
155
156
|
Map<String, dynamic> _calculatePortfolioSummary(
|
|
156
157
|
List<Map<String, dynamic>> trades, List<Map<String, dynamic>> dividends) {
|
|
157
158
|
Map<String, dynamic> summary = {};
|
|
158
|
-
double
|
|
159
|
+
double accountTotalBookCost = 0;
|
|
159
160
|
|
|
160
161
|
Map<String, double> dividendSummary = {};
|
|
161
162
|
if (widget.showDividends) {
|
|
@@ -186,7 +187,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
186
187
|
'name': asset['name'] ?? symbol,
|
|
187
188
|
'industry': asset['industry'] ?? 'N/A',
|
|
188
189
|
'shares': 0.0,
|
|
189
|
-
'
|
|
190
|
+
'book_cost': 0.0, // Renamed from total_cost for clarity
|
|
190
191
|
'total_dividends': dividendSummary[symbol] ?? 0.0,
|
|
191
192
|
'first_trade_date': tradeDate,
|
|
192
193
|
'last_activity_date': tradeDate,
|
|
@@ -202,12 +203,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
202
203
|
|
|
203
204
|
if (tradeType == 'buy') {
|
|
204
205
|
summary[symbol]['shares'] += shares;
|
|
205
|
-
summary[symbol]['
|
|
206
|
+
summary[symbol]['book_cost'] += shares * price;
|
|
206
207
|
} else if (tradeType == 'sell') {
|
|
207
208
|
double originalShares = summary[symbol]['shares'];
|
|
208
209
|
if (originalShares > 0) {
|
|
209
|
-
double avgPrice = summary[symbol]['
|
|
210
|
-
summary[symbol]['
|
|
210
|
+
double avgPrice = summary[symbol]['book_cost'] / originalShares;
|
|
211
|
+
summary[symbol]['book_cost'] -= shares * avgPrice;
|
|
211
212
|
}
|
|
212
213
|
summary[symbol]['shares'] -= shares;
|
|
213
214
|
}
|
|
@@ -215,21 +216,40 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
215
216
|
|
|
216
217
|
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
summary.values.fold(0.0, (sum, item) => sum + item['
|
|
219
|
+
accountTotalBookCost =
|
|
220
|
+
summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
220
221
|
|
|
221
222
|
List<Map<String, dynamic>> result = [];
|
|
222
223
|
summary.forEach((symbol, data) {
|
|
223
224
|
double shares = data['shares'];
|
|
224
|
-
double
|
|
225
|
-
data['
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
double bookCost = data['book_cost'];
|
|
226
|
+
double totalDividends = data['total_dividends'];
|
|
227
|
+
|
|
228
|
+
data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
|
|
229
|
+
data['account_allocation_percent'] = (accountTotalBookCost > 0)
|
|
230
|
+
? (bookCost / accountTotalBookCost) * 100
|
|
231
|
+
: 0.0;
|
|
232
|
+
data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0)
|
|
233
|
+
? (bookCost / _totalPortfolioBookCost) * 100
|
|
228
234
|
: 0.0;
|
|
235
|
+
|
|
236
|
+
// Calculate Market Value, Unrealized P/L, Total Return
|
|
237
|
+
final livePrice = _livePrices[symbol];
|
|
238
|
+
double marketValue = livePrice != null ? shares * livePrice : bookCost; // Fallback to bookCost if no live price
|
|
239
|
+
double unrealizedPL = marketValue - bookCost;
|
|
240
|
+
double totalReturn = unrealizedPL + totalDividends;
|
|
241
|
+
|
|
242
|
+
data['market_value'] = marketValue;
|
|
243
|
+
data['unrealized_pl'] = unrealizedPL;
|
|
244
|
+
data['total_return'] = totalReturn;
|
|
245
|
+
|
|
246
|
+
data['percent_unrealized_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
247
|
+
data['percent_total_return'] = (bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
|
|
248
|
+
|
|
229
249
|
result.add(data);
|
|
230
250
|
});
|
|
231
251
|
|
|
232
|
-
return {'summary': result, 'total_value':
|
|
252
|
+
return {'summary': result, 'total_value': accountTotalBookCost};
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
@override
|
|
@@ -357,111 +377,60 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
357
377
|
columnSpacing: 24.0,
|
|
358
378
|
columns: [
|
|
359
379
|
const DataColumn(label: Text('Symbol')),
|
|
360
|
-
const DataColumn(label: Text('Age')),
|
|
361
|
-
const DataColumn(label: Text('Purchased')),
|
|
362
380
|
const DataColumn(label: Text('Shares')),
|
|
363
381
|
const DataColumn(label: Text('Avg Price')),
|
|
364
|
-
const DataColumn(label: Text('Live')),
|
|
365
|
-
const DataColumn(label: Text('Purchased')),
|
|
366
|
-
const DataColumn(label: Text('Allocation')),
|
|
367
|
-
const DataColumn(label: Text('Stocks Allocation')),
|
|
368
|
-
const DataColumn(label: Text('Return')),
|
|
369
|
-
const DataColumn(label: Text('% Return')),
|
|
370
382
|
const DataColumn(label: Text('Book Cost')),
|
|
383
|
+
const DataColumn(label: Text('Live Price')),
|
|
384
|
+
const DataColumn(label: Text('Market Value')),
|
|
385
|
+
const DataColumn(label: Text('Unrealized P/L')),
|
|
386
|
+
const DataColumn(label: Text('% P/L')),
|
|
371
387
|
if (widget.showDividends) ...[
|
|
372
388
|
const DataColumn(label: Text('Dividends')),
|
|
373
|
-
const DataColumn(label: Text('Return
|
|
374
|
-
const DataColumn(label: Text('
|
|
375
|
-
const DataColumn(label: Text('% Return Div')),
|
|
389
|
+
const DataColumn(label: Text('Total Return')),
|
|
390
|
+
const DataColumn(label: Text('% Total Return')),
|
|
376
391
|
],
|
|
392
|
+
const DataColumn(label: Text('Account %')),
|
|
393
|
+
const DataColumn(label: Text('Portfolio %')),
|
|
377
394
|
],
|
|
378
395
|
rows: _portfolioSummary.map((position) {
|
|
379
396
|
final symbol = position['symbol'] as String;
|
|
397
|
+
final shares = position['shares'] as double;
|
|
398
|
+
final avgPrice = position['avg_price'] as double;
|
|
399
|
+
final bookCost = position['book_cost'] as double;
|
|
380
400
|
final livePrice = _livePrices[symbol];
|
|
381
|
-
final
|
|
382
|
-
final
|
|
383
|
-
final
|
|
384
|
-
|
|
385
|
-
final
|
|
386
|
-
|
|
387
|
-
final
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
final
|
|
394
|
-
?
|
|
395
|
-
: null;
|
|
396
|
-
|
|
397
|
-
double? totalReturnValue,
|
|
398
|
-
percentageReturn,
|
|
399
|
-
bookCost,
|
|
400
|
-
returnPlusDividend,
|
|
401
|
-
bookCostPlusDividend,
|
|
402
|
-
percentageReturnDividend;
|
|
403
|
-
|
|
404
|
-
if (livePrice != null && shares > 0) {
|
|
405
|
-
final currentMarketValue = livePrice * shares;
|
|
406
|
-
totalReturnValue = currentMarketValue - totalPurchased;
|
|
407
|
-
bookCost = currentMarketValue;
|
|
408
|
-
if (totalPurchased > 0)
|
|
409
|
-
percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
410
|
-
if (widget.showDividends) {
|
|
411
|
-
returnPlusDividend = totalReturnValue + totalDividends;
|
|
412
|
-
bookCostPlusDividend = bookCost + totalDividends;
|
|
413
|
-
if (totalPurchased > 0)
|
|
414
|
-
percentageReturnDividend =
|
|
415
|
-
(returnPlusDividend / totalPurchased) * 100;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
final returnColor =
|
|
420
|
-
(totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
421
|
-
final returnWithDivColor =
|
|
422
|
-
(returnPlusDividend ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
401
|
+
final marketValue = position['market_value'] as double;
|
|
402
|
+
final unrealizedPL = position['unrealized_pl'] as double;
|
|
403
|
+
final percentUnrealizedPL = position['percent_unrealized_pl'] as double;
|
|
404
|
+
final totalDividends = position['total_dividends'] as double;
|
|
405
|
+
final totalReturn = position['total_return'] as double;
|
|
406
|
+
final percentTotalReturn = position['percent_total_return'] as double;
|
|
407
|
+
final accountAllocationPercent = position['account_allocation_percent'] as double;
|
|
408
|
+
final portfolioAllocationPercent = position['portfolio_allocation_percent'] as double;
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
final plColor =
|
|
412
|
+
(unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
413
|
+
final totalReturnColor =
|
|
414
|
+
(totalReturn ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
423
415
|
|
|
424
416
|
return DataRow(cells: [
|
|
425
417
|
DataCell(Text(symbol)),
|
|
426
|
-
DataCell(Text(
|
|
427
|
-
positionAge != null ? _formatDuration(positionAge) : 'N/A')),
|
|
428
|
-
DataCell(Text(position['last_activity_date'] != null
|
|
429
|
-
? DateFormat('yyyy-MM-dd')
|
|
430
|
-
.format(position['last_activity_date'])
|
|
431
|
-
: 'N/A')),
|
|
432
418
|
DataCell(Text(shares.toStringAsFixed(4))),
|
|
433
419
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
420
|
+
DataCell(Text(_formatCurrency(bookCost))),
|
|
434
421
|
DataCell(livePrice != null
|
|
435
422
|
? Text(_formatCurrency(livePrice))
|
|
436
423
|
: const Text('N/A')),
|
|
437
|
-
DataCell(Text(_formatCurrency(
|
|
438
|
-
DataCell(Text(
|
|
439
|
-
DataCell(Text('${
|
|
440
|
-
DataCell(totalReturnValue != null
|
|
441
|
-
? Text(_formatCurrency(totalReturnValue),
|
|
442
|
-
style: TextStyle(color: returnColor))
|
|
443
|
-
: const Text('N/A')),
|
|
444
|
-
DataCell(percentageReturn != null
|
|
445
|
-
? Text('${percentageReturn.toStringAsFixed(2)}%',
|
|
446
|
-
style: TextStyle(color: returnColor))
|
|
447
|
-
: const Text('N/A')),
|
|
448
|
-
DataCell(bookCost != null
|
|
449
|
-
? Text(_formatCurrency(bookCost))
|
|
450
|
-
: const Text('N/A')),
|
|
424
|
+
DataCell(Text(_formatCurrency(marketValue))),
|
|
425
|
+
DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
|
|
426
|
+
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
|
|
451
427
|
if (widget.showDividends) ...[
|
|
452
428
|
DataCell(Text(_formatCurrency(totalDividends))),
|
|
453
|
-
DataCell(
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
? Text(_formatCurrency(bookCostPlusDividend))
|
|
459
|
-
: const Text('N/A')),
|
|
460
|
-
DataCell(percentageReturnDividend != null
|
|
461
|
-
? Text('${percentageReturnDividend.toStringAsFixed(2)}%',
|
|
462
|
-
style: TextStyle(color: returnWithDivColor))
|
|
463
|
-
: const Text('N/A')),
|
|
464
|
-
]
|
|
429
|
+
DataCell(Text(_formatCurrency(totalReturn), style: TextStyle(color: totalReturnColor))),
|
|
430
|
+
DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%', style: TextStyle(color: totalReturnColor))),
|
|
431
|
+
],
|
|
432
|
+
DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
|
|
433
|
+
DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
|
|
465
434
|
]);
|
|
466
435
|
}).toList(),
|
|
467
436
|
),
|
|
@@ -621,4 +590,4 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
621
590
|
}
|
|
622
591
|
}
|
|
623
592
|
}
|
|
624
|
-
}
|
|
593
|
+
}
|
|
@@ -59,8 +59,6 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
59
59
|
final sinteticoSummaryData =
|
|
60
60
|
_calculateSinteticoSummary(contributions, totalPortfolioBookCost);
|
|
61
61
|
|
|
62
|
-
print('[DEBUG] Sintetico Summary Data: $sinteticoSummaryData');
|
|
63
|
-
|
|
64
62
|
if (!mounted) return;
|
|
65
63
|
setState(() {
|
|
66
64
|
_contributions = contributions;
|
|
@@ -85,40 +83,35 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
85
83
|
|
|
86
84
|
Map<String, dynamic> _calculateSinteticoSummary(
|
|
87
85
|
List<Map<String, dynamic>> contributions, double totalPortfolioBookCost) {
|
|
88
|
-
double
|
|
89
|
-
|
|
90
|
-
double
|
|
91
|
-
|
|
92
|
-
double totalReturn = 0;
|
|
86
|
+
double totalUserContribution = 0;
|
|
87
|
+
double totalCompanyContribution = 0;
|
|
88
|
+
double totalUnrealizedPL = 0;
|
|
93
89
|
|
|
94
90
|
for (var c in contributions) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
totalReturn += double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
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');
|
|
100
94
|
}
|
|
101
95
|
|
|
102
|
-
final
|
|
103
|
-
|
|
104
|
-
final bookCost = totalPurchased + totalReturn;
|
|
96
|
+
final totalContributed = totalUserContribution + totalCompanyContribution;
|
|
97
|
+
final marketValue = totalContributed + totalUnrealizedPL;
|
|
105
98
|
|
|
106
99
|
final summaryData = {
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
100
|
+
'user_contribution': totalUserContribution,
|
|
101
|
+
'company_contribution': totalCompanyContribution,
|
|
102
|
+
'total_contributed': totalContributed,
|
|
103
|
+
'unrealized_pl': totalUnrealizedPL,
|
|
111
104
|
'percent_return':
|
|
112
|
-
(
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
? (
|
|
105
|
+
(totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
|
|
106
|
+
'market_value': marketValue,
|
|
107
|
+
'portfolio_allocation_percent': (totalPortfolioBookCost > 0)
|
|
108
|
+
? (marketValue / totalPortfolioBookCost) * 100
|
|
116
109
|
: 0.0,
|
|
117
110
|
};
|
|
118
111
|
|
|
119
112
|
return {
|
|
120
113
|
'summary': [summaryData],
|
|
121
|
-
'total_value':
|
|
114
|
+
'total_value': marketValue
|
|
122
115
|
};
|
|
123
116
|
}
|
|
124
117
|
|
|
@@ -191,27 +184,33 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
191
184
|
}
|
|
192
185
|
|
|
193
186
|
final summary = _sinteticoSummary.first;
|
|
187
|
+
final double unrealizedPL = summary['unrealized_pl'];
|
|
188
|
+
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
189
|
+
|
|
194
190
|
|
|
195
191
|
return DataTable(
|
|
196
192
|
columns: const [
|
|
197
|
-
DataColumn(label: Text('
|
|
198
|
-
DataColumn(label: Text('
|
|
199
|
-
DataColumn(label: Text('Total
|
|
200
|
-
DataColumn(label: Text('
|
|
193
|
+
DataColumn(label: Text('User Contr.')),
|
|
194
|
+
DataColumn(label: Text('Company Contr.')),
|
|
195
|
+
DataColumn(label: Text('Total Contributed')),
|
|
196
|
+
DataColumn(label: Text('Unrealized P/L')),
|
|
201
197
|
DataColumn(label: Text('% Return')),
|
|
202
|
-
DataColumn(label: Text('
|
|
198
|
+
DataColumn(label: Text('Market Value')),
|
|
199
|
+
DataColumn(label: Text('Portfolio %')),
|
|
203
200
|
],
|
|
204
201
|
rows: [
|
|
205
202
|
DataRow(
|
|
206
203
|
cells: [
|
|
207
|
-
DataCell(Text(_formatCurrency(summary['
|
|
208
|
-
DataCell(Text(_formatCurrency(summary['
|
|
204
|
+
DataCell(Text(_formatCurrency(summary['user_contribution'] as double))),
|
|
205
|
+
DataCell(Text(_formatCurrency(summary['company_contribution'] as double))),
|
|
209
206
|
DataCell(
|
|
210
|
-
Text(_formatCurrency(summary['
|
|
211
|
-
DataCell(Text(_formatCurrency(
|
|
207
|
+
Text(_formatCurrency(summary['total_contributed'] as double))),
|
|
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))),
|
|
212
212
|
DataCell(Text(
|
|
213
|
-
'${(summary['
|
|
214
|
-
DataCell(Text(_formatCurrency(summary['book_cost'] as double))),
|
|
213
|
+
'${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%')),
|
|
215
214
|
],
|
|
216
215
|
),
|
|
217
216
|
],
|
|
@@ -231,30 +230,30 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
231
230
|
return DataTable(
|
|
232
231
|
columns: const [
|
|
233
232
|
DataColumn(label: Text('Date')),
|
|
234
|
-
DataColumn(label: Text('
|
|
235
|
-
DataColumn(label: Text('
|
|
236
|
-
DataColumn(label: Text('Total
|
|
233
|
+
DataColumn(label: Text('User Contr.')),
|
|
234
|
+
DataColumn(label: Text('Company Contr.')),
|
|
235
|
+
DataColumn(label: Text('Total Contributed')),
|
|
237
236
|
DataColumn(label: Text('Return')),
|
|
238
237
|
DataColumn(label: Text('% Return')),
|
|
239
|
-
DataColumn(label: Text('
|
|
240
|
-
DataColumn(label: Text('
|
|
241
|
-
DataColumn(label: Text('Actions')),
|
|
238
|
+
DataColumn(label: Text('Cumulative Value')),
|
|
239
|
+
DataColumn(label: Text('Portfolio %')),
|
|
240
|
+
DataColumn(label: Text('Actions')),
|
|
242
241
|
],
|
|
243
242
|
rows: _contributions.map((c) {
|
|
244
243
|
final contributionId = c['id'] as int;
|
|
245
244
|
final rrspAmount = double.parse(c['rrsp_amount'].toString());
|
|
246
245
|
final dpspAmount = double.parse(c['dpsp_amount'].toString());
|
|
247
|
-
final
|
|
246
|
+
final totalContributed = rrspAmount + dpspAmount;
|
|
248
247
|
|
|
249
248
|
final returnAmount =
|
|
250
249
|
double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
251
250
|
final percentReturn =
|
|
252
|
-
|
|
251
|
+
totalContributed > 0 ? (returnAmount / totalContributed) * 100 : 0.0;
|
|
253
252
|
|
|
254
|
-
final
|
|
253
|
+
final cumulativeValue = totalContributed + returnAmount;
|
|
255
254
|
|
|
256
|
-
final
|
|
257
|
-
? (
|
|
255
|
+
final portfolioAllocation = _totalPortfolioBookCost > 0
|
|
256
|
+
? (cumulativeValue / _totalPortfolioBookCost) * 100
|
|
258
257
|
: 0.0;
|
|
259
258
|
|
|
260
259
|
String dateStr;
|
|
@@ -272,12 +271,12 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
272
271
|
DataCell(Text(dateStr)),
|
|
273
272
|
DataCell(Text(_formatCurrency(rrspAmount))),
|
|
274
273
|
DataCell(Text(_formatCurrency(dpspAmount))),
|
|
275
|
-
DataCell(Text(_formatCurrency(
|
|
274
|
+
DataCell(Text(_formatCurrency(totalContributed))),
|
|
276
275
|
DataCell(Text(_formatCurrency(returnAmount))),
|
|
277
276
|
DataCell(Text('${percentReturn.toStringAsFixed(2)}%')),
|
|
278
|
-
DataCell(Text(_formatCurrency(
|
|
279
|
-
DataCell(Text('${
|
|
280
|
-
DataCell(
|
|
277
|
+
DataCell(Text(_formatCurrency(cumulativeValue))),
|
|
278
|
+
DataCell(Text('${portfolioAllocation.toStringAsFixed(2)}%')),
|
|
279
|
+
DataCell(
|
|
281
280
|
IconButton(
|
|
282
281
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
283
282
|
onPressed: () => _confirmAndDeleteContribution(contributionId),
|
|
@@ -337,4 +336,4 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
337
336
|
}
|
|
338
337
|
}
|
|
339
338
|
}
|
|
340
|
-
}
|
|
339
|
+
}
|