@marcos_feitoza/personal-finance-frontend-feature-investments 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +21 -26
- package/lib/screens/crypto_account_screen.dart +62 -67
- package/lib/screens/investment_account_screen.dart +2 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
# [1.1.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.1...v1.1.0) (2025-11-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add confirmation dialog for deletion operations ([2972ee1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/2972ee1e7abf6bb1c183554867006ccd1a9a0909))
|
|
7
|
+
* update readme.me ([56c5303](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/56c5303681a6110d25d600b11047be38b52f0cad))
|
|
8
|
+
|
|
1
9
|
## [1.0.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.0.0...v1.0.1) (2025-11-27)
|
|
2
10
|
|
|
3
11
|
|
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.
|
|
@@ -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), // Fetch total cost
|
|
62
62
|
];
|
|
63
63
|
final results = await Future.wait(futures);
|
|
64
64
|
|
|
@@ -67,21 +67,18 @@ 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
|
-
|
|
71
|
-
final initialSummaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
70
|
+
final summaryData = _calculatePortfolioSummary(trades, totalPortfolioBookCost);
|
|
72
71
|
|
|
73
72
|
setState(() {
|
|
74
73
|
_trades = trades;
|
|
75
74
|
_cashBalance = balance;
|
|
76
75
|
_assets = assets;
|
|
77
|
-
_portfolioSummary =
|
|
78
|
-
_accountTotalValue =
|
|
76
|
+
_portfolioSummary = summaryData['summary'];
|
|
77
|
+
_accountTotalValue = summaryData['total_value'] + _cashBalance;
|
|
79
78
|
_totalPortfolioBookCost = totalPortfolioBookCost;
|
|
80
79
|
});
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
if (mounted) await _fetchLivePricesAndRecalculate();
|
|
84
|
-
|
|
81
|
+
if (mounted) await _fetchLivePrices();
|
|
85
82
|
} catch (e) {
|
|
86
83
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
|
|
87
84
|
} finally {
|
|
@@ -89,7 +86,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
Future<void>
|
|
89
|
+
Future<void> _fetchLivePrices() async {
|
|
93
90
|
if (_portfolioSummary.isEmpty || !mounted) return;
|
|
94
91
|
setState(() => _isFetchingPrices = true);
|
|
95
92
|
|
|
@@ -104,22 +101,28 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
104
101
|
|
|
105
102
|
if (!mounted) return;
|
|
106
103
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
double newTotalValue = _cashBalance;
|
|
105
|
+
for (final position in _portfolioSummary) {
|
|
106
|
+
final shares = double.parse(position['shares'].toString());
|
|
107
|
+
final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
|
|
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
|
+
}
|
|
111
115
|
|
|
112
116
|
setState(() {
|
|
113
117
|
_livePrices = livePrices;
|
|
114
|
-
|
|
115
|
-
_accountTotalValue = newTotalMarketValue + _cashBalance;
|
|
118
|
+
_accountTotalValue = newTotalValue;
|
|
116
119
|
_isFetchingPrices = false;
|
|
117
120
|
});
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost
|
|
123
|
+
Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
|
|
121
124
|
Map<String, dynamic> summary = {};
|
|
122
|
-
|
|
125
|
+
double accountPortfolioBookCost = 0;
|
|
123
126
|
|
|
124
127
|
for (var trade in trades) {
|
|
125
128
|
final asset = trade['asset'];
|
|
@@ -137,50 +140,37 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
137
140
|
'id_crypto': idCrypto,
|
|
138
141
|
'name': asset['name'] ?? symbol,
|
|
139
142
|
'shares': 0.0,
|
|
140
|
-
'
|
|
143
|
+
'total_cost': 0.0,
|
|
141
144
|
};
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
if (tradeType == 'buy') {
|
|
145
148
|
summary[symbol]['shares'] += shares;
|
|
146
|
-
summary[symbol]['
|
|
149
|
+
summary[symbol]['total_cost'] += shares * price;
|
|
147
150
|
} else if (tradeType == 'sell') {
|
|
148
151
|
double originalShares = summary[symbol]['shares'];
|
|
149
152
|
if (originalShares > 0) {
|
|
150
|
-
double avgPrice = summary[symbol]['
|
|
151
|
-
summary[symbol]['
|
|
153
|
+
double avgPrice = summary[symbol]['total_cost'] / originalShares;
|
|
154
|
+
summary[symbol]['total_cost'] -= shares * avgPrice;
|
|
152
155
|
}
|
|
153
156
|
summary[symbol]['shares'] -= shares;
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
summary.removeWhere((key, value) => value['shares'] < 0.01);
|
|
161
|
+
accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
|
|
158
162
|
|
|
159
|
-
double accountTotalBookCost = summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
|
|
160
|
-
|
|
161
163
|
List<Map<String, dynamic>> result = [];
|
|
162
164
|
summary.forEach((symbol, data) {
|
|
163
165
|
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
|
-
|
|
166
|
+
double totalCost = data['total_cost'];
|
|
167
|
+
data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
|
|
168
|
+
data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
|
|
169
|
+
data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
|
|
179
170
|
result.add(data);
|
|
180
171
|
});
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
return {'summary': result, 'total_value': accountTotalMarketValue};
|
|
173
|
+
return {'summary': result, 'total_value': accountPortfolioBookCost};
|
|
184
174
|
}
|
|
185
175
|
|
|
186
176
|
|
|
@@ -249,43 +239,48 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
249
239
|
child: DataTable(
|
|
250
240
|
columnSpacing: 24.0,
|
|
251
241
|
columns: const [
|
|
242
|
+
DataColumn(label: Text('Name')),
|
|
252
243
|
DataColumn(label: Text('Symbol')),
|
|
253
|
-
DataColumn(label: Text('
|
|
244
|
+
DataColumn(label: Text('Quantity')),
|
|
254
245
|
DataColumn(label: Text('Avg Price')),
|
|
246
|
+
DataColumn(label: Text('Live')),
|
|
255
247
|
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 %')),
|
|
248
|
+
DataColumn(label: Text('Allocation')),
|
|
249
|
+
DataColumn(label: Text('Stocks Allocation')),
|
|
250
|
+
DataColumn(label: Text('Return')),
|
|
251
|
+
DataColumn(label: Text('% Return')),
|
|
262
252
|
],
|
|
263
253
|
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
|
-
|
|
254
|
+
final symbol = position['symbol'] as String;
|
|
255
|
+
final idForLookup = (position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
256
|
+
final livePrice = _livePrices[idForLookup];
|
|
257
|
+
final shares = double.parse(position['shares'].toString());
|
|
258
|
+
final avgPrice = double.parse(position['avg_price'].toString());
|
|
259
|
+
final totalPurchased = double.parse(position['total_cost'].toString());
|
|
260
|
+
final accountAllocation = (position['account_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
261
|
+
final stocksAllocation = (position['stocks_allocation'] as num?)?.toDouble() ?? 0.0;
|
|
262
|
+
|
|
263
|
+
double? totalReturnValue, percentageReturn;
|
|
264
|
+
|
|
265
|
+
if (livePrice != null && shares > 0) {
|
|
266
|
+
final currentMarketValue = livePrice * shares;
|
|
267
|
+
totalReturnValue = currentMarketValue - totalPurchased;
|
|
268
|
+
if (totalPurchased > 0) percentageReturn = (totalReturnValue / totalPurchased) * 100;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
final returnColor = (totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
|
|
277
272
|
|
|
278
273
|
return DataRow(cells: [
|
|
274
|
+
DataCell(SizedBox(width: 150, child: Text(position['name'] as String, overflow: TextOverflow.ellipsis))),
|
|
279
275
|
DataCell(Text(symbol)),
|
|
280
276
|
DataCell(Text(shares.toStringAsFixed(6))),
|
|
281
277
|
DataCell(Text(_formatCurrency(avgPrice))),
|
|
282
|
-
DataCell(Text(_formatCurrency(bookCost))),
|
|
283
278
|
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))),
|
|
279
|
+
DataCell(Text(_formatCurrency(totalPurchased))),
|
|
287
280
|
DataCell(Text('${accountAllocation.toStringAsFixed(2)}%')),
|
|
288
|
-
DataCell(Text('${
|
|
281
|
+
DataCell(Text('${stocksAllocation.toStringAsFixed(2)}%')),
|
|
282
|
+
DataCell(totalReturnValue != null ? Text(_formatCurrency(totalReturnValue), style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
283
|
+
DataCell(percentageReturn != null ? Text('${percentageReturn.toStringAsFixed(2)}%', style: TextStyle(color: returnColor)) : const Text('N/A')),
|
|
289
284
|
]);
|
|
290
285
|
}).toList(),
|
|
291
286
|
),
|
|
@@ -355,7 +350,7 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
355
350
|
),
|
|
356
351
|
],
|
|
357
352
|
);
|
|
358
|
-
}
|
|
353
|
+
}
|
|
359
354
|
);
|
|
360
355
|
|
|
361
356
|
if (confirm == true) {
|
|
@@ -384,4 +379,4 @@ class _CryptoAccountScreenState extends State<CryptoAccountScreen> {
|
|
|
384
379
|
}
|
|
385
380
|
}
|
|
386
381
|
}
|
|
387
|
-
}
|
|
382
|
+
}
|
|
@@ -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;
|
|
@@ -544,25 +545,7 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
|
|
|
544
545
|
}
|
|
545
546
|
|
|
546
547
|
Future<void> _confirmAndDeleteTrade(int tradeId) async {
|
|
547
|
-
final bool? confirm = await
|
|
548
|
-
context: context,
|
|
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
|
-
);
|
|
548
|
+
final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
|
|
566
549
|
|
|
567
550
|
if (confirm == true) {
|
|
568
551
|
if (_token == null) {
|