@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 +20 -0
- package/lib/screens/crypto_account_screen.dart +16 -27
- package/lib/screens/investment_account_screen.dart +51 -110
- package/lib/screens/rrsp_sun_life_screen.dart +17 -19
- package/lib/viewmodels/crypto_account_viewmodel.dart +19 -7
- package/lib/viewmodels/investment_account_viewmodel.dart +64 -13
- package/lib/viewmodels/rrsp_sun_life_viewmodel.dart +5 -5
- package/package.json +1 -1
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.
|
|
143
|
-
final avgPrice = double.
|
|
144
|
-
final totalPurchased =
|
|
145
|
-
|
|
146
|
-
final
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
229
|
-
final double price = double.
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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('
|
|
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
|
-
|
|
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']
|
|
209
|
-
final avgPrice = position['avg_price']
|
|
210
|
-
final bookCost = position['book_cost']
|
|
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']
|
|
213
|
-
final unrealizedPL = position['unrealized_pl']
|
|
214
|
-
final percentUnrealizedPL =
|
|
215
|
-
|
|
216
|
-
final
|
|
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
|
-
|
|
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
|
|
239
|
+
Widget _buildHistoryTable(
|
|
258
240
|
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
259
|
-
if (viewModel.
|
|
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
|
|
245
|
+
child: Text('No history found.'),
|
|
264
246
|
),
|
|
265
247
|
);
|
|
266
248
|
}
|
|
267
249
|
|
|
268
|
-
final List<Map<String, dynamic>>
|
|
250
|
+
final List<Map<String, dynamic>> filteredHistory =
|
|
269
251
|
viewModel.selectedSymbolForFilter == null
|
|
270
|
-
? viewModel.
|
|
271
|
-
: viewModel.
|
|
272
|
-
.where((
|
|
273
|
-
|
|
274
|
-
viewModel.selectedSymbolForFilter)
|
|
252
|
+
? viewModel.unifiedHistory
|
|
253
|
+
: viewModel.unifiedHistory
|
|
254
|
+
.where((item) =>
|
|
255
|
+
item['symbol'] == viewModel.selectedSymbolForFilter)
|
|
275
256
|
.toList();
|
|
276
257
|
|
|
277
|
-
if (
|
|
258
|
+
if (filteredHistory.isEmpty) {
|
|
278
259
|
return const Center(
|
|
279
260
|
child: Padding(
|
|
280
261
|
padding: EdgeInsets.all(16.0),
|
|
281
|
-
child: Text('No
|
|
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:
|
|
309
|
-
final
|
|
310
|
-
final
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
330
|
-
|
|
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(
|
|
337
|
-
DataCell(Text(shares.
|
|
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: () =>
|
|
365
|
-
tooltip: 'Delete
|
|
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>
|
|
374
|
-
InvestmentAccountViewModel viewModel, int
|
|
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
|
|
316
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this $itemType');
|
|
377
317
|
|
|
378
318
|
if (confirm == true) {
|
|
379
|
-
final success =
|
|
319
|
+
final success = isTrade
|
|
320
|
+
? await viewModel.deleteTrade(itemId)
|
|
321
|
+
: await viewModel.deleteDividend(itemId);
|
|
322
|
+
|
|
380
323
|
if (ScaffoldMessenger.of(context).mounted) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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']
|
|
117
|
+
Text(_formatCurrency(double.tryParse(summary['user_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
117
118
|
DataCell(Text(
|
|
118
|
-
_formatCurrency(summary['company_contribution']
|
|
119
|
+
_formatCurrency(double.tryParse(summary['company_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
119
120
|
DataCell(
|
|
120
|
-
Text(_formatCurrency(summary['total_contributed']
|
|
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']
|
|
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']
|
|
129
|
+
Text(_formatCurrency(double.tryParse(summary['market_value']?.toString() ?? '0.0') ?? 0.0))),
|
|
129
130
|
DataCell(
|
|
130
131
|
Text(
|
|
131
|
-
'${(summary['portfolio_allocation_percent']
|
|
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.
|
|
165
|
-
final dpspAmount = double.
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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]
|
|
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]
|
|
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.
|
|
108
|
-
double price = double.
|
|
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.
|
|
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
|
-
|
|
161
|
+
currentMarketValue = shares * livePrice;
|
|
162
|
+
newTotalValue += currentMarketValue;
|
|
159
163
|
} else {
|
|
160
|
-
|
|
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]
|
|
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]
|
|
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.
|
|
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.
|
|
136
|
-
double price = double.
|
|
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']
|
|
188
|
-
final bookCost = position['book_cost']
|
|
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]
|
|
49
|
-
_totalPortfolioBookCost = results[2]
|
|
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.
|
|
68
|
-
totalCompanyContribution += double.
|
|
69
|
-
totalUnrealizedPL += double.
|
|
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;
|