@marcos_feitoza/personal-finance-frontend-feature-investments 1.0.1 → 1.1.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 +15 -0
- package/README.md +21 -26
- package/lib/screens/crypto_account_screen.dart +62 -84
- package/lib/screens/investment_account_screen.dart +41 -44
- package/lib/screens/rrsp_sun_life_screen.dart +30 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [1.1.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.0...v1.1.1) (2025-12-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add AppDialogs.showDeleteConfirmationDialog ([29c41f8](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/29c41f80530906111efe5fac1c3c493c78a29a62))
|
|
7
|
+
|
|
8
|
+
# [1.1.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.1...v1.1.0) (2025-11-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add confirmation dialog for deletion operations ([2972ee1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/2972ee1e7abf6bb1c183554867006ccd1a9a0909))
|
|
14
|
+
* update readme.me ([56c5303](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/56c5303681a6110d25d600b11047be38b52f0cad))
|
|
15
|
+
|
|
1
16
|
## [1.0.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.0...v1.0.1) (2025-11-27)
|
|
2
17
|
|
|
3
18
|
|
package/README.md
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
This README describes the package. If you publish this package to pub.dev,
|
|
3
|
-
this README's contents appear on the landing page for your package.
|
|
1
|
+
# Feature: Investimentos (Frontend Flutter)
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
|
3
|
+
Este pacote contém a implementação das telas para visualização e gerenciamento de todas as contas de investimento na aplicação Personal Finance Frontend.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
|
10
|
-
and the Flutter guide for
|
|
11
|
-
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
|
12
|
-
-->
|
|
5
|
+
## Propósito
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
know whether this package might be useful for them.
|
|
7
|
+
O objetivo deste pacote é fornecer ao usuário uma interface completa para acompanhar a performance de seus investimentos, registrar novas transações (compra/venda, dividendos) e gerenciar suas contribuições RRSP.
|
|
16
8
|
|
|
17
|
-
##
|
|
9
|
+
## Conteúdo Principal
|
|
18
10
|
|
|
19
|
-
|
|
11
|
+
- **`lib/screens/investment_account_screen.dart`**: Tela genérica para contas de investimento (ex: TFSA, Non-Registered, RRSP Wealthsimple), mostrando resumo e histórico de trades.
|
|
12
|
+
- **`lib/screens/crypto_account_screen.dart`**: Tela específica para contas de criptomoedas, mostrando resumo e histórico de trades de cripto.
|
|
13
|
+
- **`lib/screens/rrsp_sun_life_screen.dart`**: Tela específica para a conta RRSP Sun Life, focando em histórico de contribuições.
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
---
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
start using the package.
|
|
17
|
+
## Como Usar (Instalação como Dependência)
|
|
25
18
|
|
|
26
|
-
|
|
19
|
+
Este pacote é uma dependência local para a aplicação principal (`personal-finance-frontend`).
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
to `/example` folder.
|
|
21
|
+
No `pubspec.yaml` da aplicação principal, adicione a seguinte linha em `dependencies`:
|
|
30
22
|
|
|
31
|
-
```
|
|
32
|
-
|
|
23
|
+
```yaml
|
|
24
|
+
personal_finance_frontend_feature_investments:
|
|
25
|
+
path: ../personal-finance-frontend-feature-investments
|
|
33
26
|
```
|
|
34
27
|
|
|
35
|
-
##
|
|
28
|
+
## Features
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
- **Resumo do Portfólio (Sintético)**: Visão geral da performance dos ativos em cada conta de investimento, com métricas como "Book Cost", "Market Value", "Unrealized P/L", "Total Return" e percentuais de alocação.
|
|
31
|
+
- **Histórico de Trades (Analítico)**: Listagem detalhada de todas as operações de compra e venda por conta.
|
|
32
|
+
- **Registro de Dividendos**: Formulário para registrar dividendos recebidos.
|
|
33
|
+
- **Acompanhamento de Contribuições RRSP**: Histórico e resumo das contribuições para contas RRSP.
|
|
34
|
+
- **Exclusão Suave (Soft Delete)**: Funcionalidade para deletar trades e contribuições de forma segura, marcando-os como excluídos em vez de remover permanentemente.
|
|
@@ -6,6 +6,7 @@ import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart
|
|
|
6
6
|
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
7
7
|
import 'package:provider/provider.dart';
|
|
8
8
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
9
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
9
10
|
|
|
10
11
|
class CryptoAccountScreen extends StatefulWidget {
|
|
11
12
|
final String accountName;
|
|
@@ -58,7 +59,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
58
59
|
_transactionService.getTrades(investmentAccount: widget.accountName, token: _token),
|
|
59
60
|
_transactionService.getAccountBalance(widget.accountName, token: _token),
|
|
60
61
|
_transactionService.getAssets(investmentAccount: widget.accountName, token: _token),
|
|
61
|
-
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
62
|
+
_transactionService.getTotalPortfolioBookCost(token: _token), // Fetch total cost
|
|
62
63
|
];
|
|
63
64
|
final results = await Future.wait(futures);
|
|
64
65
|
|
|
@@ -67,21 +68,18 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
67
68
|
final assets = results[2] as List<Map<String, dynamic>>;
|
|
68
69
|
final totalPortfolioBookCost = results[3] as double;
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
final initialSummaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
71
|
+
final summaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
72
72
|
|
|
73
73
|
setState(() {
|
|
74
74
|
_trades = trades;
|
|
75
75
|
_cashBalance = balance;
|
|
76
76
|
_assets = assets;
|
|
77
|
-
_portfolioSummary =
|
|
78
|
-
_accountTotalValue =
|
|
77
|
+
_portfolioSummary = summaryData['summary'];
|
|
78
|
+
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
79
79
|
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
if (mounted) await _fetchLivePricesAndRecalculate();
|
|
84
|
-
|
|
82
|
+
if (mounted) await _fetchLivePrices();
|
|
85
83
|
} catch (e) {
|
|
86
84
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
87
85
|
} finally {
|
|
@@ -89,7 +87,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
Future<void>
|
|
90
|
+
Future<void> _fetchLivePrices() async {
|
|
93
91
|
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
94
92
|
setState(() => _isFetchingPrices = true);
|
|
95
93
|
|
|
@@ -104,22 +102,28 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
104
102
|
|
|
105
103
|
if (!mounted) return;
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
double newTotalValue = _cashBalance;
|
|
106
|
+
for (final position in _portfolioSummary) {
|
|
107
|
+
final shares = double.parse(position['shares'].toString());
|
|
108
|
+
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
109
|
+
final livePrice = livePrices[idForLookup];
|
|
110
|
+
if (livePrice != null) {
|
|
111
|
+
newTotalValue += shares * livePrice;
|
|
112
|
+
} else {
|
|
113
|
+
newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
111
116
|
|
|
112
117
|
setState(() {
|
|
113
118
|
_livePrices = livePrices;
|
|
114
|
-
|
|
115
|
-
_accountTotalValue = newTotalMarketValue + _cashBalance;
|
|
119
|
+
_accountTotalValue = newTotalValue;
|
|
116
120
|
_isFetchingPrices = false;
|
|
117
121
|
});
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost
|
|
124
|
+
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
|
|
121
125
|
Map<String, dynamic> summary = {};
|
|
122
|
-
|
|
126
|
+
double accountPortfolioBookCost = 0;
|
|
123
127
|
|
|
124
128
|
for (var trade in trades) {
|
|
125
129
|
final asset = trade['asset'];
|
|
@@ -137,50 +141,37 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
137
141
|
'id_crypto': idCrypto,
|
|
138
142
|
'name': asset['name'] ?? symbol,
|
|
139
143
|
'shares': 0.0,
|
|
140
|
-
'
|
|
144
|
+
'total_cost': 0.0,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
if (tradeType == 'buy') {
|
|
145
149
|
summary[symbol]['shares'] += shares;
|
|
146
|
-
summary[symbol]['
|
|
150
|
+
summary[symbol]['total_cost'] += shares * price;
|
|
147
151
|
} else if (tradeType == 'sell') {
|
|
148
152
|
double originalShares = summary[symbol]['shares'];
|
|
149
153
|
if (originalShares > 0) {
|
|
150
|
-
double avgPrice = summary[symbol]['
|
|
151
|
-
summary[symbol]['
|
|
154
|
+
double avgPrice = summary[symbol]['total_cost'] / originalShares;
|
|
155
|
+
summary[symbol]['total_cost'] -= shares * avgPrice;
|
|
152
156
|
}
|
|
153
157
|
summary[symbol]['shares'] -= shares;
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
162
|
+
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
158
163
|
|
|
159
|
-
double accountTotalBookCost = summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
160
|
-
|
|
161
164
|
List<Map<String, dynamic>> result = [];
|
|
162
165
|
summary.forEach((symbol, data) {
|
|
163
166
|
double shares = data['shares'];
|
|
164
|
-
double
|
|
165
|
-
|
|
166
|
-
data['
|
|
167
|
-
data['
|
|
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
|
-
|
|
167
|
+
double totalCost = data['total_cost'];
|
|
168
|
+
data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
|
|
169
|
+
data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
|
|
170
|
+
data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
179
171
|
result.add(data);
|
|
180
172
|
});
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
return {'summary': result, 'total_value': accountTotalMarketValue};
|
|
174
|
+
return {'summary': result, 'total_value': accountPortfolioBookCost};
|
|
184
175
|
}
|
|
185
176
|
|
|
186
177
|
|
|
@@ -249,43 +240,48 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
249
240
|
child: DataTable(
|
|
250
241
|
columnSpacing: 24.0,
|
|
251
242
|
columns: const [
|
|
243
|
+
DataColumn(label: Text('Name')),
|
|
252
244
|
DataColumn(label: Text('Symbol')),
|
|
253
|
-
DataColumn(label: Text('
|
|
245
|
+
DataColumn(label: Text('Quantity')),
|
|
254
246
|
DataColumn(label: Text('Avg Price')),
|
|
247
|
+
DataColumn(label: Text('Live')),
|
|
255
248
|
DataColumn(label: Text('Book Cost')),
|
|
256
|
-
DataColumn(label: Text('
|
|
257
|
-
DataColumn(label: Text('
|
|
258
|
-
DataColumn(label: Text('
|
|
259
|
-
DataColumn(label: Text('%
|
|
260
|
-
DataColumn(label: Text('Account %')),
|
|
261
|
-
DataColumn(label: Text('Portfolio %')),
|
|
249
|
+
DataColumn(label: Text('Allocation')),
|
|
250
|
+
DataColumn(label: Text('Stocks Allocation')),
|
|
251
|
+
DataColumn(label: Text('Return')),
|
|
252
|
+
DataColumn(label: Text('% Return')),
|
|
262
253
|
],
|
|
263
254
|
rows: _portfolioSummary.map((position) {
|
|
264
|
-
final
|
|
265
|
-
final
|
|
266
|
-
final
|
|
267
|
-
final
|
|
268
|
-
final
|
|
269
|
-
final
|
|
270
|
-
final
|
|
271
|
-
final
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
255
|
+
final symbol = position['symbol'] as String;
|
|
256
|
+
final idForLookup = (position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
257
|
+
final livePrice = _livePrices[idForLookup];
|
|
258
|
+
final shares = double.parse(position['shares'].toString());
|
|
259
|
+
final avgPrice = double.parse(position['avg_price'].toString());
|
|
260
|
+
final totalPurchased = double.parse(position['total_cost'].toString());
|
|
261
|
+
final accountAllocation = (position['account_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
262
|
+
final stocksAllocation = (position['stocks_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
263
|
+
|
|
264
|
+
double? totalReturnValue, percentageReturn;
|
|
265
|
+
|
|
266
|
+
if (livePrice != null && shares > 0) {
|
|
267
|
+
final currentMarketValue = livePrice * shares;
|
|
268
|
+
totalReturnValue = currentMarketValue - totalPurchased;
|
|
269
|
+
if (totalPurchased > 0) percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
final returnColor = (totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
277
273
|
|
|
278
274
|
return DataRow(cells: [
|
|
275
|
+
DataCell(SizedBox(width: 150, child: Text(position['name'] as String, overflow: TextOverflow.ellipsis))),
|
|
279
276
|
DataCell(Text(symbol)),
|
|
280
277
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
281
278
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
282
|
-
DataCell(Text(_formatCurrency(bookCost))),
|
|
283
279
|
DataCell(livePrice != null ? Text(_formatCurrency(livePrice)) : const Text('N/A')),
|
|
284
|
-
DataCell(Text(_formatCurrency(
|
|
285
|
-
DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
|
|
286
|
-
DataCell(Text('${percentPL.toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
|
|
280
|
+
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
287
281
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
288
|
-
DataCell(Text('${
|
|
282
|
+
DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
|
|
283
|
+
DataCell(totalReturnValue != null ? Text(_formatCurrency(totalReturnValue), style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
284
|
+
DataCell(percentageReturn != null ? Text('${percentageReturn.toStringAsFixed(2)}%', style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
289
285
|
]);
|
|
290
286
|
}).toList(),
|
|
291
287
|
),
|
|
@@ -338,25 +334,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
338
334
|
}
|
|
339
335
|
|
|
340
336
|
Future<void> _confirmAndDeleteTrade(int tradeId) async {
|
|
341
|
-
final bool? confirm = await
|
|
342
|
-
context: context,
|
|
343
|
-
builder: (BuildContext context) {
|
|
344
|
-
return AlertDialog(
|
|
345
|
-
title: const Text('Confirm Deletion'),
|
|
346
|
-
content: const Text('Are you sure you want to delete this trade? This action cannot be undone.'),
|
|
347
|
-
actions: <Widget>[
|
|
348
|
-
TextButton(
|
|
349
|
-
onPressed: () => Navigator.of(context).pop(false),
|
|
350
|
-
child: const Text('Cancel'),
|
|
351
|
-
),
|
|
352
|
-
TextButton(
|
|
353
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
354
|
-
child: const Text('Delete'),
|
|
355
|
-
),
|
|
356
|
-
],
|
|
357
|
-
);
|
|
358
|
-
},
|
|
359
|
-
);
|
|
337
|
+
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
360
338
|
|
|
361
339
|
if (confirm == true) {
|
|
362
340
|
if (_token == null) {
|
|
@@ -7,6 +7,7 @@ import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart
|
|
|
7
7
|
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
8
8
|
import 'package:provider/provider.dart';
|
|
9
9
|
import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
|
|
10
|
+
import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
|
|
10
11
|
|
|
11
12
|
class InvestmentAccountScreen extends StatefulWidget {
|
|
12
13
|
final String accountName;
|
|
@@ -75,9 +76,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
75
76
|
setState(() => _isLoading = true);
|
|
76
77
|
try {
|
|
77
78
|
final futures = <Future>[
|
|
78
|
-
_transactionService.getTrades(
|
|
79
|
-
|
|
80
|
-
_transactionService.
|
|
79
|
+
_transactionService.getTrades(
|
|
80
|
+
investmentAccount: widget.accountName, token: _token),
|
|
81
|
+
_transactionService.getAccountBalance(widget.accountName,
|
|
82
|
+
token: _token),
|
|
83
|
+
_transactionService.getAssets(
|
|
84
|
+
investmentAccount: widget.accountName, token: _token),
|
|
81
85
|
_transactionService.getTotalPortfolioBookCost(token: _token),
|
|
82
86
|
];
|
|
83
87
|
if (widget.showDividends) {
|
|
@@ -232,10 +236,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
232
236
|
data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0)
|
|
233
237
|
? (bookCost / _totalPortfolioBookCost) * 100
|
|
234
238
|
: 0.0;
|
|
235
|
-
|
|
239
|
+
|
|
236
240
|
// Calculate Market Value, Unrealized P/L, Total Return
|
|
237
241
|
final livePrice = _livePrices[symbol];
|
|
238
|
-
double marketValue = livePrice != null
|
|
242
|
+
double marketValue = livePrice != null
|
|
243
|
+
? shares * livePrice
|
|
244
|
+
: bookCost; // Fallback to bookCost if no live price
|
|
239
245
|
double unrealizedPL = marketValue - bookCost;
|
|
240
246
|
double totalReturn = unrealizedPL + totalDividends;
|
|
241
247
|
|
|
@@ -243,8 +249,10 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
243
249
|
data['unrealized_pl'] = unrealizedPL;
|
|
244
250
|
data['total_return'] = totalReturn;
|
|
245
251
|
|
|
246
|
-
data['percent_unrealized_pl'] =
|
|
247
|
-
|
|
252
|
+
data['percent_unrealized_pl'] =
|
|
253
|
+
(bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
|
|
254
|
+
data['percent_total_return'] =
|
|
255
|
+
(bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
|
|
248
256
|
|
|
249
257
|
result.add(data);
|
|
250
258
|
});
|
|
@@ -311,12 +319,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
311
319
|
children: [
|
|
312
320
|
Expanded(
|
|
313
321
|
child: TradeForm(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
accountName: widget.accountName,
|
|
323
|
+
portfolioSummary: _portfolioSummary,
|
|
324
|
+
assets: _assets,
|
|
325
|
+
onTradeCreated: (_) => _fetchData(),
|
|
326
|
+
token: _token,
|
|
327
|
+
)),
|
|
320
328
|
if (widget.showDividends) ...[
|
|
321
329
|
const SizedBox(width: 16),
|
|
322
330
|
Expanded(
|
|
@@ -400,16 +408,17 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
400
408
|
final livePrice = _livePrices[symbol];
|
|
401
409
|
final marketValue = position['market_value'] as double;
|
|
402
410
|
final unrealizedPL = position['unrealized_pl'] as double;
|
|
403
|
-
final percentUnrealizedPL =
|
|
411
|
+
final percentUnrealizedPL =
|
|
412
|
+
position['percent_unrealized_pl'] as double;
|
|
404
413
|
final totalDividends = position['total_dividends'] as double;
|
|
405
414
|
final totalReturn = position['total_return'] as double;
|
|
406
415
|
final percentTotalReturn = position['percent_total_return'] as double;
|
|
407
|
-
final accountAllocationPercent =
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
final accountAllocationPercent =
|
|
417
|
+
position['account_allocation_percent'] as double;
|
|
418
|
+
final portfolioAllocationPercent =
|
|
419
|
+
position['portfolio_allocation_percent'] as double;
|
|
410
420
|
|
|
411
|
-
final plColor =
|
|
412
|
-
(unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
421
|
+
final plColor = (unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
413
422
|
final totalReturnColor =
|
|
414
423
|
(totalReturn ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
415
424
|
|
|
@@ -422,12 +431,16 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
422
431
|
? Text(_formatCurrency(livePrice))
|
|
423
432
|
: const Text('N/A')),
|
|
424
433
|
DataCell(Text(_formatCurrency(marketValue))),
|
|
425
|
-
DataCell(Text(_formatCurrency(unrealizedPL),
|
|
426
|
-
|
|
434
|
+
DataCell(Text(_formatCurrency(unrealizedPL),
|
|
435
|
+
style: TextStyle(color: plColor))),
|
|
436
|
+
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
|
|
437
|
+
style: TextStyle(color: plColor))),
|
|
427
438
|
if (widget.showDividends) ...[
|
|
428
439
|
DataCell(Text(_formatCurrency(totalDividends))),
|
|
429
|
-
DataCell(Text(_formatCurrency(totalReturn),
|
|
430
|
-
|
|
440
|
+
DataCell(Text(_formatCurrency(totalReturn),
|
|
441
|
+
style: TextStyle(color: totalReturnColor))),
|
|
442
|
+
DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%',
|
|
443
|
+
style: TextStyle(color: totalReturnColor))),
|
|
431
444
|
],
|
|
432
445
|
DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
|
|
433
446
|
DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
|
|
@@ -542,27 +555,10 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
542
555
|
}).toList(),
|
|
543
556
|
);
|
|
544
557
|
}
|
|
545
|
-
|
|
558
|
+
|
|
546
559
|
Future<void> _confirmAndDeleteTrade(int tradeId) async {
|
|
547
|
-
final bool? confirm =
|
|
548
|
-
|
|
549
|
-
builder: (BuildContext context) {
|
|
550
|
-
return AlertDialog(
|
|
551
|
-
title: const Text('Confirm Deletion'),
|
|
552
|
-
content: const Text('Are you sure you want to delete this trade? This action cannot be undone.'),
|
|
553
|
-
actions: <Widget>[
|
|
554
|
-
TextButton(
|
|
555
|
-
onPressed: () => Navigator.of(context).pop(false),
|
|
556
|
-
child: const Text('Cancel'),
|
|
557
|
-
),
|
|
558
|
-
TextButton(
|
|
559
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
560
|
-
child: const Text('Delete'),
|
|
561
|
-
),
|
|
562
|
-
],
|
|
563
|
-
);
|
|
564
|
-
},
|
|
565
|
-
);
|
|
560
|
+
final bool? confirm =
|
|
561
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
566
562
|
|
|
567
563
|
if (confirm == true) {
|
|
568
564
|
if (_token == null) {
|
|
@@ -572,7 +568,8 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
572
568
|
return;
|
|
573
569
|
}
|
|
574
570
|
try {
|
|
575
|
-
final success =
|
|
571
|
+
final success =
|
|
572
|
+
await _transactionService.deleteTrade(tradeId, token: _token);
|
|
576
573
|
if (success) {
|
|
577
574
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
578
575
|
const SnackBar(content: Text('Trade deleted successfully.')),
|
|
@@ -4,6 +4,7 @@ import 'package:personal_finance_frontend_core_services/services/transaction_ser
|
|
|
4
4
|
import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
|
|
5
5
|
import 'package:provider/provider.dart';
|
|
6
6
|
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
|
|
7
8
|
|
|
8
9
|
class RrspSunLifeScreen extends StatefulWidget {
|
|
9
10
|
const RrspSunLifeScreen({Key? key}) : super(key: key);
|
|
@@ -150,29 +151,32 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
150
151
|
),
|
|
151
152
|
body: _isLoading
|
|
152
153
|
? const Center(child: CircularProgressIndicator())
|
|
153
|
-
:
|
|
154
|
-
|
|
155
|
-
child:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
|
175
|
+
),
|
|
176
|
+
const SizedBox(height: 8),
|
|
177
|
+
_buildContributionTable(),
|
|
178
|
+
],
|
|
179
|
+
),
|
|
176
180
|
),
|
|
177
181
|
),
|
|
178
182
|
);
|
|
@@ -290,24 +294,9 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
|
|
|
290
294
|
}
|
|
291
295
|
|
|
292
296
|
Future<void> _confirmAndDeleteContribution(int contributionId) async {
|
|
293
|
-
final bool? confirm = await
|
|
294
|
-
context
|
|
295
|
-
|
|
296
|
-
return AlertDialog(
|
|
297
|
-
title: const Text('Confirm Deletion'),
|
|
298
|
-
content: const Text('Are you sure you want to delete this contribution? This action cannot be undone.'),
|
|
299
|
-
actions: <Widget>[
|
|
300
|
-
TextButton(
|
|
301
|
-
onPressed: () => Navigator.of(context).pop(false),
|
|
302
|
-
child: const Text('Cancel'),
|
|
303
|
-
),
|
|
304
|
-
TextButton(
|
|
305
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
306
|
-
child: const Text('Delete'),
|
|
307
|
-
),
|
|
308
|
-
],
|
|
309
|
-
);
|
|
310
|
-
},
|
|
297
|
+
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(
|
|
298
|
+
context,
|
|
299
|
+
'this contribution', // A generic string for the item being deleted
|
|
311
300
|
);
|
|
312
301
|
|
|
313
302
|
if (confirm == true) {
|