@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 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
- For information about how to write a good package README, see the guide for
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
- For general information about developing packages, see the Dart guide for
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
- TODO: Put a short description of the package here that helps potential users
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
- ## Features
9
+ ## Conteúdo Principal
18
10
 
19
- TODO: List what your package can do. Maybe include images, gifs, or videos.
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
- ## Getting started
15
+ ---
22
16
 
23
- TODO: List prerequisites and provide or point to information on how to
24
- start using the package.
17
+ ## Como Usar (Instalação como Dependência)
25
18
 
26
- ## Usage
19
+ Este pacote é uma dependência local para a aplicação principal (`personal-finance-frontend`).
27
20
 
28
- TODO: Include short and useful examples for package users. Add longer examples
29
- to `/example` folder.
21
+ No `pubspec.yaml` da aplicação principal, adicione a seguinte linha em `dependencies`:
30
22
 
31
- ```dart
32
- const like = 'sample';
23
+ ```yaml
24
+ personal_finance_frontend_feature_investments:
25
+ path: ../personal-finance-frontend-feature-investments
33
26
  ```
34
27
 
35
- ## Additional information
28
+ ## Features
36
29
 
37
- TODO: Tell users more about the package: where to find more information, how to
38
- contribute to the package, how to file issues, what response they can expect
39
- from the package authors, and more.
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
- // Initial calculation without live prices
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 = initialSummaryData['summary'];
78
- _accountTotalValue = initialSummaryData['total_value'] + _cashBalance;
76
+ _portfolioSummary = summaryData['summary'];
77
+ _accountTotalValue = summaryData['total_value'] + _cashBalance;
79
78
  _totalPortfolioBookCost = totalPortfolioBookCost;
80
79
  });
81
80
 
82
- // Fetch live prices and recalculate summary
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> _fetchLivePricesAndRecalculate() async {
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
- // Recalculate summary with live prices
108
- final summaryDataWithLivePrices = _calculatePortfolioSummary(_trades, _totalPortfolioBookCost, livePrices: livePrices);
109
-
110
- double newTotalMarketValue = summaryDataWithLivePrices['total_value'];
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
- _portfolioSummary = summaryDataWithLivePrices['summary'];
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, {Map<String, double>? livePrices}) {
123
+ Map<String, dynamic> _calculatePortfolioSummary(List<Map<String, dynamic>> trades, double totalPortfolioBookCost) {
121
124
  Map<String, dynamic> summary = {};
122
- livePrices ??= _livePrices; // Use state's live prices if not provided
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
- 'book_cost': 0.0,
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]['book_cost'] += shares * price;
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]['book_cost'] / originalShares;
151
- summary[symbol]['book_cost'] -= shares * avgPrice;
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 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
-
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
- double accountTotalMarketValue = result.fold(0.0, (sum, item) => sum + item['market_value']);
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('Shares')),
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('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 %')),
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 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;
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(marketValue))),
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('${portfolioAllocation.toStringAsFixed(2)}%')),
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 showDialog<bool>(
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-feature-investments",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },