@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.2 → 1.2.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,23 @@
1
+ ## [1.2.1](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.2.0...v1.2.1) (2026-01-29)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * update SnackBar ([a8f6587](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/a8f6587eb10895eedd77eaa8bf9e15c5efa53463))
7
+
8
+ # [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.2...v1.2.0) (2025-12-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add totals back ([0abb920](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/0abb920257d8988083f08a24f8db90ad9a24d463))
14
+
15
+
16
+ ### Features
17
+
18
+ * new dividend logic ([5aed0ed](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/5aed0edd789072894c927192fb2105dd7cf1623b))
19
+ * update dividend ([67c9dc3](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/67c9dc327a15e114c7a4acc590f797e1f89ac2ed))
20
+
1
21
  ## [1.1.2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.1...v1.1.2) (2025-12-03)
2
22
 
3
23
 
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
3
3
  import 'package:provider/provider.dart';
4
4
  import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
5
5
  import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
6
+ import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
6
7
  import 'package:personal_finance_frontend_core_ui/widgets/crypto_trade_form.dart';
7
8
  import '../viewmodels/crypto_account_viewmodel.dart';
8
9
 
@@ -139,27 +140,17 @@ class CryptoAccountScreen extends StatelessWidget {
139
140
  final idForLookup =
140
141
  (position['id_crypto'] as String? ?? symbol).toLowerCase();
141
142
  final livePrice = viewModel.livePrices[idForLookup];
142
- final shares = double.parse(position['shares'].toString());
143
- final avgPrice = double.parse(position['avg_price'].toString());
144
- final totalPurchased =
145
- double.parse(position['total_cost'].toString());
146
- final accountAllocation =
147
- (position['account_allocation'] as num?)?.toDouble() ?? 0.0;
148
- final stocksAllocation =
149
- (position['stocks_allocation'] as num?)?.toDouble() ?? 0.0;
143
+ final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
144
+ final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
145
+ final totalPurchased = double.tryParse(position['total_cost']?.toString() ?? '0.0') ?? 0.0;
146
+ final accountAllocation = double.tryParse(position['account_allocation']?.toString() ?? '0.0') ?? 0.0;
147
+ final stocksAllocation = double.tryParse(position['stocks_allocation']?.toString() ?? '0.0') ?? 0.0;
150
148
 
151
- double? totalReturnValue, percentageReturn;
152
-
153
- if (livePrice != null && shares > 0) {
154
- final currentMarketValue = livePrice * shares;
155
- totalReturnValue = currentMarketValue - totalPurchased;
156
- if (totalPurchased > 0) {
157
- percentageReturn = (totalReturnValue / totalPurchased) * 100;
158
- }
159
- }
149
+ final totalReturnValue = double.tryParse(position['total_return_value']?.toString() ?? '0.0') ?? 0.0;
150
+ final percentageReturn = double.tryParse(position['percentage_return']?.toString() ?? '0.0') ?? 0.0;
160
151
 
161
152
  final returnColor =
162
- (totalReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
153
+ totalReturnValue >= 0 ? Colors.green : Colors.red;
163
154
 
164
155
  return DataRow(cells: [
165
156
  DataCell(SizedBox(
@@ -225,8 +216,8 @@ class CryptoAccountScreen extends StatelessWidget {
225
216
  DataColumn(label: Text('Actions')),
226
217
  ],
227
218
  rows: sortedTrades.map((trade) {
228
- final double shares = double.parse(trade['shares'].toString());
229
- final double price = double.parse(trade['price'].toString());
219
+ final double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
220
+ final double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
230
221
  final double total = shares * price;
231
222
  final tradeDate = DateTime.parse(trade['date'] as String);
232
223
  final int tradeId = trade['id'] as int;
@@ -259,13 +250,11 @@ class CryptoAccountScreen extends StatelessWidget {
259
250
  if (confirm == true) {
260
251
  final success = await viewModel.deleteTrade(tradeId);
261
252
  if (ScaffoldMessenger.of(context).mounted) {
262
- ScaffoldMessenger.of(context).showSnackBar(
263
- SnackBar(
264
- content: Text(success
265
- ? 'Trade deleted successfully.'
266
- : 'Failed to delete trade.'),
267
- ),
268
- );
253
+ if (success) {
254
+ showSuccessSnackBar(context, 'Trade deleted successfully.');
255
+ } else {
256
+ showErrorSnackBar(context, 'Failed to delete trade.');
257
+ }
269
258
  }
270
259
  }
271
260
  }
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
3
3
  import 'package:provider/provider.dart';
4
4
  import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
5
5
  import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
6
+ import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
6
7
  import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
7
8
  import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
8
9
  import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
@@ -114,12 +115,12 @@ class InvestmentAccountScreen extends StatelessWidget {
114
115
  ],
115
116
  ),
116
117
  const SizedBox(height: 24),
117
- Text('Trade History (Analítico)',
118
+ Text('Account History',
118
119
  style: Theme.of(context).textTheme.titleLarge),
119
120
  const SizedBox(height: 8),
120
121
  _buildAnaliticoFilter(context, viewModel),
121
122
  const SizedBox(height: 8),
122
- _buildAnaliticoTable(context, viewModel),
123
+ _buildHistoryTable(context, viewModel),
123
124
  ],
124
125
  ),
125
126
  ),
@@ -195,35 +196,22 @@ class InvestmentAccountScreen extends StatelessWidget {
195
196
  const DataColumn(label: Text('Market Value')),
196
197
  const DataColumn(label: Text('Unrealized P/L')),
197
198
  const DataColumn(label: Text('% P/L')),
198
- if (showDividends) ...[
199
- const DataColumn(label: Text('Dividends')),
200
- const DataColumn(label: Text('Total Return')),
201
- const DataColumn(label: Text('% Total Return')),
202
- ],
203
199
  const DataColumn(label: Text('Account %')),
204
200
  const DataColumn(label: Text('Portfolio %')),
205
201
  ],
206
202
  rows: viewModel.portfolioSummary.map((position) {
207
203
  final symbol = position['symbol'] as String;
208
- final shares = position['shares'] as double;
209
- final avgPrice = position['avg_price'] as double;
210
- final bookCost = position['book_cost'] as double;
204
+ final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
205
+ final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
206
+ final bookCost = double.tryParse(position['book_cost']?.toString() ?? '0.0') ?? 0.0;
211
207
  final livePrice = viewModel.livePrices[symbol];
212
- final marketValue = position['market_value'] as double;
213
- final unrealizedPL = position['unrealized_pl'] as double;
214
- final percentUnrealizedPL =
215
- position['percent_unrealized_pl'] as double;
216
- final totalDividends = position['total_dividends'] as double;
217
- final totalReturn = position['total_return'] as double;
218
- final percentTotalReturn =
219
- position['percent_total_return'] as double;
220
- final accountAllocationPercent =
221
- position['account_allocation_percent'] as double;
222
- final portfolioAllocationPercent =
223
- position['portfolio_allocation_percent'] as double;
208
+ final marketValue = double.tryParse(position['market_value']?.toString() ?? '0.0') ?? 0.0;
209
+ final unrealizedPL = double.tryParse(position['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
210
+ final percentUnrealizedPL = double.tryParse(position['percent_unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
211
+ final accountAllocationPercent = double.tryParse(position['account_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
212
+ final portfolioAllocationPercent = double.tryParse(position['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
224
213
 
225
214
  final plColor = unrealizedPL >= 0 ? Colors.green : Colors.red;
226
- final totalReturnColor = totalReturn >= 0 ? Colors.green : Colors.red;
227
215
 
228
216
  return DataRow(cells: [
229
217
  DataCell(Text(symbol)),
@@ -239,13 +227,7 @@ class InvestmentAccountScreen extends StatelessWidget {
239
227
  style: TextStyle(color: plColor))),
240
228
  DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
241
229
  style: TextStyle(color: plColor))),
242
- if (showDividends) ...[
243
- DataCell(Text(_formatCurrency(totalDividends))),
244
- DataCell(Text(_formatCurrency(totalReturn),
245
- style: TextStyle(color: totalReturnColor))),
246
- DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%',
247
- style: TextStyle(color: totalReturnColor))),
248
- ],
230
+
249
231
  DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
250
232
  DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
251
233
  ]);
@@ -254,42 +236,34 @@ class InvestmentAccountScreen extends StatelessWidget {
254
236
  );
255
237
  }
256
238
 
257
- Widget _buildAnaliticoTable(
239
+ Widget _buildHistoryTable(
258
240
  BuildContext context, InvestmentAccountViewModel viewModel) {
259
- if (viewModel.trades.isEmpty) {
241
+ if (viewModel.unifiedHistory.isEmpty) {
260
242
  return const Center(
261
243
  child: Padding(
262
244
  padding: EdgeInsets.all(16.0),
263
- child: Text('No trades found.'),
245
+ child: Text('No history found.'),
264
246
  ),
265
247
  );
266
248
  }
267
249
 
268
- final List<Map<String, dynamic>> filteredTrades =
250
+ final List<Map<String, dynamic>> filteredHistory =
269
251
  viewModel.selectedSymbolForFilter == null
270
- ? viewModel.trades
271
- : viewModel.trades
272
- .where((trade) =>
273
- trade['asset']?['symbol'] ==
274
- viewModel.selectedSymbolForFilter)
252
+ ? viewModel.unifiedHistory
253
+ : viewModel.unifiedHistory
254
+ .where((item) =>
255
+ item['symbol'] == viewModel.selectedSymbolForFilter)
275
256
  .toList();
276
257
 
277
- if (filteredTrades.isEmpty) {
258
+ if (filteredHistory.isEmpty) {
278
259
  return const Center(
279
260
  child: Padding(
280
261
  padding: EdgeInsets.all(16.0),
281
- child: Text('No trades match the selected symbol.'),
262
+ child: Text('No history matches the selected symbol.'),
282
263
  ),
283
264
  );
284
265
  }
285
266
 
286
- final sortedTrades = List<Map<String, dynamic>>.from(filteredTrades)
287
- ..sort((a, b) {
288
- final dateA = DateTime.parse(a['date'] as String);
289
- final dateB = DateTime.parse(b['date'] as String);
290
- return dateB.compareTo(dateA);
291
- });
292
-
293
267
  return DataTable(
294
268
  columns: const [
295
269
  DataColumn(label: Text('Date')),
@@ -298,71 +272,36 @@ class InvestmentAccountScreen extends StatelessWidget {
298
272
  DataColumn(label: Text('Type')),
299
273
  DataColumn(label: Text('Shares')),
300
274
  DataColumn(label: Text('Price')),
301
- DataColumn(label: Text('Live')),
302
275
  DataColumn(label: Text('Total')),
303
- DataColumn(label: Text('Return')),
304
- DataColumn(label: Text('% Return')),
305
- DataColumn(label: Text('Book Cost')),
306
276
  DataColumn(label: Text('Actions')),
307
277
  ],
308
- rows: sortedTrades.map((trade) {
309
- final double shares = double.parse(trade['shares'].toString());
310
- final double price = double.parse(trade['price'].toString());
311
- final double total = shares * price;
312
- final tradeDate = DateTime.parse(trade['date'] as String);
313
- final tradeAge = DateTime.now().difference(tradeDate);
314
- final symbol = trade['asset']?['symbol'] ?? '';
315
- final livePrice = viewModel.livePrices[symbol];
316
- final int tradeId = trade['id'] as int;
317
-
318
- double? tradeReturnValue, tradePercentageReturn, tradeBookCost;
278
+ rows: filteredHistory.map((item) {
279
+ final isTrade = item['history_type'] == 'trade';
280
+ final isDividend = item['history_type'] == 'dividend';
319
281
 
320
- if (livePrice != null && shares > 0 && trade['trade_type'] == 'buy') {
321
- final currentMarketValue = livePrice * shares;
322
- tradeReturnValue = currentMarketValue - total;
323
- tradeBookCost = currentMarketValue;
324
- if (total > 0) {
325
- tradePercentageReturn = (tradeReturnValue / total) * 100;
326
- }
327
- }
282
+ final tradeDate = DateTime.parse(item['date'] as String);
283
+ final tradeAge = DateTime.now().difference(tradeDate);
284
+ final symbol = item['symbol'] as String? ?? 'N/A';
285
+ final type = item['type'] as String? ?? 'N/A';
286
+ final int id = item['id'] as int;
328
287
 
329
- final returnColor =
330
- (tradeReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
288
+ final double shares = isTrade ? (double.tryParse(item['shares']?.toString() ?? '0.0') ?? 0.0) : 0.0;
289
+ final double price = isTrade ? (double.tryParse(item['price']?.toString() ?? '0.0') ?? 0.0) : 0.0;
290
+ final double total = isTrade ? (shares * price) : (double.tryParse(item['total']?.toString() ?? '0.0') ?? 0.0);
331
291
 
332
292
  return DataRow(cells: [
333
293
  DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
334
294
  DataCell(Text(_formatDuration(tradeAge))),
335
295
  DataCell(Text(symbol)),
336
- DataCell(Text(trade['trade_type'] ?? 'N/A')),
337
- DataCell(Text(shares.toString())),
338
- DataCell(Text(_formatCurrency(price))),
339
- DataCell(livePrice != null
340
- ? Text(_formatCurrency(livePrice))
341
- : const Text('N/A')),
296
+ DataCell(Text(type)),
297
+ DataCell(isTrade ? Text(shares.toStringAsFixed(4)) : const Text('-')),
298
+ DataCell(isTrade ? Text(_formatCurrency(price)) : const Text('-')),
342
299
  DataCell(Text(_formatCurrency(total))),
343
- DataCell(
344
- tradeReturnValue != null
345
- ? Text(_formatCurrency(tradeReturnValue),
346
- style: TextStyle(color: returnColor))
347
- : const Text('N/A'),
348
- ),
349
- DataCell(
350
- tradePercentageReturn != null
351
- ? Text('${tradePercentageReturn.toStringAsFixed(2)}%',
352
- style: TextStyle(color: returnColor))
353
- : const Text('N/A'),
354
- ),
355
- DataCell(
356
- tradeBookCost != null
357
- ? Text(_formatCurrency(tradeBookCost),
358
- style: TextStyle(color: returnColor))
359
- : const Text('N/A'),
360
- ),
361
300
  DataCell(
362
301
  IconButton(
363
302
  icon: const Icon(Icons.delete, color: Colors.red),
364
- onPressed: () => _confirmAndDeleteTrade(context, viewModel, tradeId),
365
- tooltip: 'Delete Trade',
303
+ onPressed: () => _confirmAndDeleteItem(context, viewModel, id, isTrade),
304
+ tooltip: 'Delete Item',
366
305
  ),
367
306
  ),
368
307
  ]);
@@ -370,21 +309,23 @@ class InvestmentAccountScreen extends StatelessWidget {
370
309
  );
371
310
  }
372
311
 
373
- Future<void> _confirmAndDeleteTrade(BuildContext context,
374
- InvestmentAccountViewModel viewModel, int tradeId) async {
312
+ Future<void> _confirmAndDeleteItem(BuildContext context,
313
+ InvestmentAccountViewModel viewModel, int itemId, bool isTrade) async {
314
+ final itemType = isTrade ? 'trade' : 'dividend';
375
315
  final bool? confirm =
376
- await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
316
+ await AppDialogs.showDeleteConfirmationDialog(context, 'this $itemType');
377
317
 
378
318
  if (confirm == true) {
379
- final success = await viewModel.deleteTrade(tradeId);
319
+ final success = isTrade
320
+ ? await viewModel.deleteTrade(itemId)
321
+ : await viewModel.deleteDividend(itemId);
322
+
380
323
  if (ScaffoldMessenger.of(context).mounted) {
381
- ScaffoldMessenger.of(context).showSnackBar(
382
- SnackBar(
383
- content: Text(success
384
- ? 'Trade deleted successfully.'
385
- : 'Failed to delete trade.'),
386
- ),
387
- );
324
+ if (success) {
325
+ showSuccessSnackBar(context, 'The $itemType was deleted successfully.');
326
+ } else {
327
+ showErrorSnackBar(context, 'Failed to delete the $itemType.');
328
+ }
388
329
  }
389
330
  }
390
331
  }
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
3
3
  import 'package:provider/provider.dart';
4
4
  import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
5
5
  import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
6
+ import 'package:personal_finance_frontend_core_ui/utils/app_snackbars.dart';
6
7
  import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
7
8
  import '../viewmodels/rrsp_sun_life_viewmodel.dart';
8
9
 
@@ -96,7 +97,7 @@ class RrspSunLifeScreen extends StatelessWidget {
96
97
  }
97
98
 
98
99
  final summary = viewModel.sinteticoSummary.first;
99
- final double unrealizedPL = summary['unrealized_pl'];
100
+ final double unrealizedPL = double.tryParse(summary['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
100
101
  final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
101
102
 
102
103
  return DataTable(
@@ -113,22 +114,22 @@ class RrspSunLifeScreen extends StatelessWidget {
113
114
  DataRow(
114
115
  cells: [
115
116
  DataCell(
116
- Text(_formatCurrency(summary['user_contribution'] as double))),
117
+ Text(_formatCurrency(double.tryParse(summary['user_contribution']?.toString() ?? '0.0') ?? 0.0))),
117
118
  DataCell(Text(
118
- _formatCurrency(summary['company_contribution'] as double))),
119
+ _formatCurrency(double.tryParse(summary['company_contribution']?.toString() ?? '0.0') ?? 0.0))),
119
120
  DataCell(
120
- Text(_formatCurrency(summary['total_contributed'] as double))),
121
+ Text(_formatCurrency(double.tryParse(summary['total_contributed']?.toString() ?? '0.0') ?? 0.0))),
121
122
  DataCell(Text(_formatCurrency(unrealizedPL),
122
123
  style: TextStyle(color: plColor))),
123
124
  DataCell(
124
- Text('${(summary['percent_return'] as double).toStringAsFixed(2)}%',
125
+ Text('${(double.tryParse(summary['percent_return']?.toString() ?? '0.0') ?? 0.0).toStringAsFixed(2)}%',
125
126
  style: TextStyle(color: plColor)),
126
127
  ),
127
128
  DataCell(
128
- Text(_formatCurrency(summary['market_value'] as double))),
129
+ Text(_formatCurrency(double.tryParse(summary['market_value']?.toString() ?? '0.0') ?? 0.0))),
129
130
  DataCell(
130
131
  Text(
131
- '${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%'),
132
+ '${(double.tryParse(summary['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0).toStringAsFixed(2)}%'),
132
133
  ),
133
134
  ],
134
135
  ),
@@ -161,12 +162,11 @@ class RrspSunLifeScreen extends StatelessWidget {
161
162
  ],
162
163
  rows: viewModel.contributions.map((c) {
163
164
  final contributionId = c['id'] as int;
164
- final rrspAmount = double.parse(c['rrsp_amount'].toString());
165
- final dpspAmount = double.parse(c['dpsp_amount'].toString());
165
+ final rrspAmount = double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
166
+ final dpspAmount = double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
166
167
  final totalContributed = rrspAmount + dpspAmount;
167
168
 
168
- final returnAmount =
169
- double.parse(c['return_amount']?.toString() ?? '0.0');
169
+ final returnAmount = double.tryParse(c['return_amount']?.toString() ?? '0.0') ?? 0.0;
170
170
  final percentReturn = totalContributed > 0
171
171
  ? (returnAmount / totalContributed) * 100
172
172
  : 0.0;
@@ -221,14 +221,12 @@ class RrspSunLifeScreen extends StatelessWidget {
221
221
  if (confirm == true) {
222
222
  final success = await viewModel.deleteContribution(contributionId);
223
223
  if (ScaffoldMessenger.of(context).mounted) {
224
- ScaffoldMessenger.of(context).showSnackBar(
225
- SnackBar(
226
- content: Text(success
227
- ? 'Contribution deleted successfully.'
228
- : 'Failed to delete contribution.'),
229
- ),
230
- );
224
+ if (success) {
225
+ showSuccessSnackBar(context, 'Contribution deleted successfully.');
226
+ } else {
227
+ showErrorSnackBar(context, 'Failed to delete contribution.');
228
+ }
231
229
  }
232
230
  }
233
231
  }
234
- }
232
+ }
@@ -54,9 +54,9 @@ class CryptoAccountViewModel extends ChangeNotifier {
54
54
  final results = await Future.wait(futures);
55
55
 
56
56
  _trades = results[0] as List<Map<String, dynamic>>;
57
- _cashBalance = results[1] as double;
57
+ _cashBalance = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
58
58
  _assets = results[2] as List<Map<String, dynamic>>;
59
- final totalPortfolioBookCost = results[3] as double;
59
+ final totalPortfolioBookCost = double.tryParse(results[3]?.toString() ?? '0.0') ?? 0.0;
60
60
 
61
61
  _calculateAndApplyPortfolioSummary(totalPortfolioBookCost);
62
62
 
@@ -104,8 +104,8 @@ class CryptoAccountViewModel extends ChangeNotifier {
104
104
  if (asset == null || asset['symbol'] == null) continue;
105
105
  String symbol = asset['symbol'];
106
106
  String idCrypto = asset['id_crypto'] ?? symbol;
107
- double shares = double.parse(trade['shares'].toString());
108
- double price = double.parse(trade['price'].toString());
107
+ double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
108
+ double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
109
109
  String tradeType = trade['trade_type'];
110
110
 
111
111
  if (!summary.containsKey(symbol)) {
@@ -151,14 +151,26 @@ class CryptoAccountViewModel extends ChangeNotifier {
151
151
  void _recalculateMarketValue() {
152
152
  double newTotalValue = _cashBalance;
153
153
  for (final position in _portfolioSummary) {
154
- final shares = double.parse(position['shares'].toString());
154
+ final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
155
+ final totalCost = double.tryParse(position['total_cost']?.toString() ?? '0.0') ?? 0.0;
155
156
  final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
156
157
  final livePrice = _livePrices[idForLookup];
158
+
159
+ double currentMarketValue;
157
160
  if (livePrice != null) {
158
- newTotalValue += shares * livePrice;
161
+ currentMarketValue = shares * livePrice;
162
+ newTotalValue += currentMarketValue;
159
163
  } else {
160
- newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
164
+ currentMarketValue = totalCost;
165
+ newTotalValue += totalCost;
161
166
  }
167
+
168
+ double totalReturnValue = currentMarketValue - totalCost;
169
+ double percentageReturn = (totalCost > 0) ? (totalReturnValue / totalCost) * 100 : 0.0;
170
+
171
+ position['market_value'] = currentMarketValue;
172
+ position['total_return_value'] = totalReturnValue;
173
+ position['percentage_return'] = percentageReturn;
162
174
  }
163
175
  _accountTotalValue = newTotalValue;
164
176
  }
@@ -35,6 +35,47 @@ class InvestmentAccountViewModel extends ChangeNotifier {
35
35
  String? get selectedSymbolForFilter => _selectedSymbolForFilter;
36
36
  String? get token => _token;
37
37
 
38
+ List<Map<String, dynamic>> get unifiedHistory {
39
+ List<Map<String, dynamic>> history = [];
40
+
41
+ // Add trades
42
+ for (var trade in _trades) {
43
+ history.add({
44
+ ...trade,
45
+ 'history_type': 'trade',
46
+ 'symbol': trade['asset']?['symbol'] ?? 'N/A', // Normalize symbol
47
+ 'type': trade['trade_type'] as String? ?? 'N/A', // Normalize type
48
+ });
49
+ }
50
+
51
+ // Add dividends
52
+ if (showDividends) {
53
+ for (var dividend in _dividends) {
54
+ history.add({
55
+ ...dividend,
56
+ 'history_type': 'dividend',
57
+ // Normalize fields for the table
58
+ 'total': double.tryParse(dividend['amount']?.toString() ?? '0.0') ?? 0.0,
59
+ 'type': 'Dividend',
60
+ 'symbol': dividend['asset']?['symbol'] ?? 'N/A',
61
+ });
62
+ }
63
+ }
64
+
65
+ // Sort by date descending
66
+ history.sort((a, b) {
67
+ try {
68
+ final dateA = DateTime.parse(a['date'] as String);
69
+ final dateB = DateTime.parse(b['date'] as String);
70
+ return dateB.compareTo(dateA);
71
+ } catch (e) {
72
+ return 0;
73
+ }
74
+ });
75
+
76
+ return history;
77
+ }
78
+
38
79
  InvestmentAccountViewModel({required this.accountName, required this.showDividends, required String? token}) {
39
80
  _token = token;
40
81
  fetchData();
@@ -68,10 +109,10 @@ class InvestmentAccountViewModel extends ChangeNotifier {
68
109
  final results = await Future.wait(futures);
69
110
 
70
111
  _trades = results[0] as List<Map<String, dynamic>>;
71
- _cashBalance = results[1] as double;
112
+ _cashBalance = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
72
113
  _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>>[];
114
+ _totalPortfolioBookCost = double.tryParse(results[3]?.toString() ?? '0.0') ?? 0.0;
115
+ _dividends = showDividends && results.length > 4 ? results[4] as List<Map<String, dynamic>> : <Map<String, dynamic>>[];
75
116
 
76
117
  _calculateAndApplyPortfolioSummary();
77
118
 
@@ -121,7 +162,7 @@ class InvestmentAccountViewModel extends ChangeNotifier {
121
162
  final asset = dividend['asset'];
122
163
  if (asset != null && asset['symbol'] != null) {
123
164
  String symbol = asset['symbol'];
124
- double amount = double.parse(dividend['amount'].toString());
165
+ double amount = double.tryParse(dividend['amount']?.toString() ?? '0.0') ?? 0.0;
125
166
  dividendSummary.update(symbol, (value) => value + amount, ifAbsent: () => amount);
126
167
  }
127
168
  }
@@ -132,8 +173,8 @@ class InvestmentAccountViewModel extends ChangeNotifier {
132
173
  if (asset == null || asset['symbol'] == null) continue;
133
174
  String symbol = asset['symbol'];
134
175
 
135
- double shares = double.parse(trade['shares'].toString());
136
- double price = double.parse(trade['price'].toString());
176
+ double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
177
+ double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
137
178
  String tradeType = trade['trade_type'];
138
179
 
139
180
  if (!summary.containsKey(symbol)) {
@@ -143,7 +184,6 @@ class InvestmentAccountViewModel extends ChangeNotifier {
143
184
  'industry': asset['industry'] ?? 'N/A',
144
185
  'shares': 0.0,
145
186
  'book_cost': 0.0,
146
- 'total_dividends': dividendSummary[symbol] ?? 0.0,
147
187
  };
148
188
  }
149
189
 
@@ -184,20 +224,16 @@ class InvestmentAccountViewModel extends ChangeNotifier {
184
224
  double newTotalValue = _cashBalance;
185
225
  for (final position in _portfolioSummary) {
186
226
  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;
227
+ final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
228
+ final bookCost = double.tryParse(position['book_cost']?.toString() ?? '0.0') ?? 0.0;
190
229
 
191
230
  final livePrice = _livePrices[symbol];
192
231
  double marketValue = livePrice != null ? shares * livePrice : bookCost;
193
232
  double unrealizedPL = marketValue - bookCost;
194
- double totalReturn = unrealizedPL + totalDividends;
195
233
 
196
234
  position['market_value'] = marketValue;
197
235
  position['unrealized_pl'] = unrealizedPL;
198
- position['total_return'] = totalReturn;
199
236
  position['percent_unrealized_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
200
- position['percent_total_return'] = (bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
201
237
 
202
238
  newTotalValue += marketValue;
203
239
  }
@@ -218,4 +254,19 @@ class InvestmentAccountViewModel extends ChangeNotifier {
218
254
  return false;
219
255
  }
220
256
  }
257
+
258
+ Future<bool> deleteDividend(int dividendId) async {
259
+ if (_token == null) return false;
260
+
261
+ try {
262
+ final success = await _transactionService.deleteDividend(dividendId, token: _token);
263
+ if (success) {
264
+ fetchData(); // Refresh data
265
+ }
266
+ return success;
267
+ } catch (e) {
268
+ debugPrint("Error deleting dividend: $e");
269
+ return false;
270
+ }
271
+ }
221
272
  }
@@ -45,8 +45,8 @@ class RrspSunLifeViewModel extends ChangeNotifier {
45
45
  final results = await Future.wait([contributionsFuture, balanceFuture, totalPortfolioBookCostFuture]);
46
46
 
47
47
  _contributions = results[0] as List<Map<String, dynamic>>;
48
- _rrspCashBalance = results[1] as double;
49
- _totalPortfolioBookCost = results[2] as double;
48
+ _rrspCashBalance = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
49
+ _totalPortfolioBookCost = double.tryParse(results[2]?.toString() ?? '0.0') ?? 0.0;
50
50
 
51
51
  _calculateAndApplySinteticoSummary();
52
52
 
@@ -64,9 +64,9 @@ class RrspSunLifeViewModel extends ChangeNotifier {
64
64
  double totalUnrealizedPL = 0;
65
65
 
66
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');
67
+ totalUserContribution += double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
68
+ totalCompanyContribution += double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
69
+ totalUnrealizedPL += double.tryParse(c['return_amount']?.toString() ?? '0.0') ?? 0.0;
70
70
  }
71
71
 
72
72
  final totalContributed = totalUserContribution + totalCompanyContribution;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-feature-investments",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },