@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.2 → 1.2.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 +13 -0
- package/lib/screens/crypto_account_screen.dart +10 -20
- package/lib/screens/investment_account_screen.dart +47 -105
- package/lib/screens/rrsp_sun_life_screen.dart +10 -11
- 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,16 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.2...v1.2.0) (2025-12-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* add totals back ([0abb920](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/0abb920257d8988083f08a24f8db90ad9a24d463))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* new dividend logic ([5aed0ed](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/5aed0edd789072894c927192fb2105dd7cf1623b))
|
|
12
|
+
* update dividend ([67c9dc3](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/commit/67c9dc327a15e114c7a4acc590f797e1f89ac2ed))
|
|
13
|
+
|
|
1
14
|
## [1.1.2](https://github.com/MarcosOps/personal-finance-frontend-feature-investments/compare/v1.1.1...v1.1.2) (2025-12-03)
|
|
2
15
|
|
|
3
16
|
|
|
@@ -139,27 +139,17 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
139
139
|
final idForLookup =
|
|
140
140
|
(position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
141
141
|
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;
|
|
142
|
+
final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
143
|
+
final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
|
|
144
|
+
final totalPurchased = double.tryParse(position['total_cost']?.toString() ?? '0.0') ?? 0.0;
|
|
145
|
+
final accountAllocation = double.tryParse(position['account_allocation']?.toString() ?? '0.0') ?? 0.0;
|
|
146
|
+
final stocksAllocation = double.tryParse(position['stocks_allocation']?.toString() ?? '0.0') ?? 0.0;
|
|
150
147
|
|
|
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
|
-
}
|
|
148
|
+
final totalReturnValue = double.tryParse(position['total_return_value']?.toString() ?? '0.0') ?? 0.0;
|
|
149
|
+
final percentageReturn = double.tryParse(position['percentage_return']?.toString() ?? '0.0') ?? 0.0;
|
|
160
150
|
|
|
161
151
|
final returnColor =
|
|
162
|
-
|
|
152
|
+
totalReturnValue >= 0 ? Colors.green : Colors.red;
|
|
163
153
|
|
|
164
154
|
return DataRow(cells: [
|
|
165
155
|
DataCell(SizedBox(
|
|
@@ -225,8 +215,8 @@ class CryptoAccountScreen extends StatelessWidget {
|
|
|
225
215
|
DataColumn(label: Text('Actions')),
|
|
226
216
|
],
|
|
227
217
|
rows: sortedTrades.map((trade) {
|
|
228
|
-
final double shares = double.
|
|
229
|
-
final double price = double.
|
|
218
|
+
final double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
219
|
+
final double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
|
|
230
220
|
final double total = shares * price;
|
|
231
221
|
final tradeDate = DateTime.parse(trade['date'] as String);
|
|
232
222
|
final int tradeId = trade['id'] as int;
|
|
@@ -114,12 +114,12 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
114
114
|
],
|
|
115
115
|
),
|
|
116
116
|
const SizedBox(height: 24),
|
|
117
|
-
Text('
|
|
117
|
+
Text('Account History',
|
|
118
118
|
style: Theme.of(context).textTheme.titleLarge),
|
|
119
119
|
const SizedBox(height: 8),
|
|
120
120
|
_buildAnaliticoFilter(context, viewModel),
|
|
121
121
|
const SizedBox(height: 8),
|
|
122
|
-
|
|
122
|
+
_buildHistoryTable(context, viewModel),
|
|
123
123
|
],
|
|
124
124
|
),
|
|
125
125
|
),
|
|
@@ -195,35 +195,22 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
195
195
|
const DataColumn(label: Text('Market Value')),
|
|
196
196
|
const DataColumn(label: Text('Unrealized P/L')),
|
|
197
197
|
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
198
|
const DataColumn(label: Text('Account %')),
|
|
204
199
|
const DataColumn(label: Text('Portfolio %')),
|
|
205
200
|
],
|
|
206
201
|
rows: viewModel.portfolioSummary.map((position) {
|
|
207
202
|
final symbol = position['symbol'] as String;
|
|
208
|
-
final shares = position['shares']
|
|
209
|
-
final avgPrice = position['avg_price']
|
|
210
|
-
final bookCost = position['book_cost']
|
|
203
|
+
final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
|
|
204
|
+
final avgPrice = double.tryParse(position['avg_price']?.toString() ?? '0.0') ?? 0.0;
|
|
205
|
+
final bookCost = double.tryParse(position['book_cost']?.toString() ?? '0.0') ?? 0.0;
|
|
211
206
|
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;
|
|
207
|
+
final marketValue = double.tryParse(position['market_value']?.toString() ?? '0.0') ?? 0.0;
|
|
208
|
+
final unrealizedPL = double.tryParse(position['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
209
|
+
final percentUnrealizedPL = double.tryParse(position['percent_unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
210
|
+
final accountAllocationPercent = double.tryParse(position['account_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
|
|
211
|
+
final portfolioAllocationPercent = double.tryParse(position['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0;
|
|
224
212
|
|
|
225
213
|
final plColor = unrealizedPL >= 0 ? Colors.green : Colors.red;
|
|
226
|
-
final totalReturnColor = totalReturn >= 0 ? Colors.green : Colors.red;
|
|
227
214
|
|
|
228
215
|
return DataRow(cells: [
|
|
229
216
|
DataCell(Text(symbol)),
|
|
@@ -239,13 +226,7 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
239
226
|
style: TextStyle(color: plColor))),
|
|
240
227
|
DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
|
|
241
228
|
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
|
-
],
|
|
229
|
+
|
|
249
230
|
DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
|
|
250
231
|
DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
|
|
251
232
|
]);
|
|
@@ -254,42 +235,34 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
254
235
|
);
|
|
255
236
|
}
|
|
256
237
|
|
|
257
|
-
Widget
|
|
238
|
+
Widget _buildHistoryTable(
|
|
258
239
|
BuildContext context, InvestmentAccountViewModel viewModel) {
|
|
259
|
-
if (viewModel.
|
|
240
|
+
if (viewModel.unifiedHistory.isEmpty) {
|
|
260
241
|
return const Center(
|
|
261
242
|
child: Padding(
|
|
262
243
|
padding: EdgeInsets.all(16.0),
|
|
263
|
-
child: Text('No
|
|
244
|
+
child: Text('No history found.'),
|
|
264
245
|
),
|
|
265
246
|
);
|
|
266
247
|
}
|
|
267
248
|
|
|
268
|
-
final List<Map<String, dynamic>>
|
|
249
|
+
final List<Map<String, dynamic>> filteredHistory =
|
|
269
250
|
viewModel.selectedSymbolForFilter == null
|
|
270
|
-
? viewModel.
|
|
271
|
-
: viewModel.
|
|
272
|
-
.where((
|
|
273
|
-
|
|
274
|
-
viewModel.selectedSymbolForFilter)
|
|
251
|
+
? viewModel.unifiedHistory
|
|
252
|
+
: viewModel.unifiedHistory
|
|
253
|
+
.where((item) =>
|
|
254
|
+
item['symbol'] == viewModel.selectedSymbolForFilter)
|
|
275
255
|
.toList();
|
|
276
256
|
|
|
277
|
-
if (
|
|
257
|
+
if (filteredHistory.isEmpty) {
|
|
278
258
|
return const Center(
|
|
279
259
|
child: Padding(
|
|
280
260
|
padding: EdgeInsets.all(16.0),
|
|
281
|
-
child: Text('No
|
|
261
|
+
child: Text('No history matches the selected symbol.'),
|
|
282
262
|
),
|
|
283
263
|
);
|
|
284
264
|
}
|
|
285
265
|
|
|
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
266
|
return DataTable(
|
|
294
267
|
columns: const [
|
|
295
268
|
DataColumn(label: Text('Date')),
|
|
@@ -298,71 +271,36 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
298
271
|
DataColumn(label: Text('Type')),
|
|
299
272
|
DataColumn(label: Text('Shares')),
|
|
300
273
|
DataColumn(label: Text('Price')),
|
|
301
|
-
DataColumn(label: Text('Live')),
|
|
302
274
|
DataColumn(label: Text('Total')),
|
|
303
|
-
DataColumn(label: Text('Return')),
|
|
304
|
-
DataColumn(label: Text('% Return')),
|
|
305
|
-
DataColumn(label: Text('Book Cost')),
|
|
306
275
|
DataColumn(label: Text('Actions')),
|
|
307
276
|
],
|
|
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;
|
|
277
|
+
rows: filteredHistory.map((item) {
|
|
278
|
+
final isTrade = item['history_type'] == 'trade';
|
|
279
|
+
final isDividend = item['history_type'] == 'dividend';
|
|
317
280
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
tradeBookCost = currentMarketValue;
|
|
324
|
-
if (total > 0) {
|
|
325
|
-
tradePercentageReturn = (tradeReturnValue / total) * 100;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
281
|
+
final tradeDate = DateTime.parse(item['date'] as String);
|
|
282
|
+
final tradeAge = DateTime.now().difference(tradeDate);
|
|
283
|
+
final symbol = item['symbol'] as String? ?? 'N/A';
|
|
284
|
+
final type = item['type'] as String? ?? 'N/A';
|
|
285
|
+
final int id = item['id'] as int;
|
|
328
286
|
|
|
329
|
-
final
|
|
330
|
-
|
|
287
|
+
final double shares = isTrade ? (double.tryParse(item['shares']?.toString() ?? '0.0') ?? 0.0) : 0.0;
|
|
288
|
+
final double price = isTrade ? (double.tryParse(item['price']?.toString() ?? '0.0') ?? 0.0) : 0.0;
|
|
289
|
+
final double total = isTrade ? (shares * price) : (double.tryParse(item['total']?.toString() ?? '0.0') ?? 0.0);
|
|
331
290
|
|
|
332
291
|
return DataRow(cells: [
|
|
333
292
|
DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
|
|
334
293
|
DataCell(Text(_formatDuration(tradeAge))),
|
|
335
294
|
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')),
|
|
295
|
+
DataCell(Text(type)),
|
|
296
|
+
DataCell(isTrade ? Text(shares.toStringAsFixed(4)) : const Text('-')),
|
|
297
|
+
DataCell(isTrade ? Text(_formatCurrency(price)) : const Text('-')),
|
|
342
298
|
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
299
|
DataCell(
|
|
362
300
|
IconButton(
|
|
363
301
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
364
|
-
onPressed: () =>
|
|
365
|
-
tooltip: 'Delete
|
|
302
|
+
onPressed: () => _confirmAndDeleteItem(context, viewModel, id, isTrade),
|
|
303
|
+
tooltip: 'Delete Item',
|
|
366
304
|
),
|
|
367
305
|
),
|
|
368
306
|
]);
|
|
@@ -370,19 +308,23 @@ class InvestmentAccountScreen extends StatelessWidget {
|
|
|
370
308
|
);
|
|
371
309
|
}
|
|
372
310
|
|
|
373
|
-
Future<void>
|
|
374
|
-
InvestmentAccountViewModel viewModel, int
|
|
311
|
+
Future<void> _confirmAndDeleteItem(BuildContext context,
|
|
312
|
+
InvestmentAccountViewModel viewModel, int itemId, bool isTrade) async {
|
|
313
|
+
final itemType = isTrade ? 'trade' : 'dividend';
|
|
375
314
|
final bool? confirm =
|
|
376
|
-
await AppDialogs.showDeleteConfirmationDialog(context, 'this
|
|
315
|
+
await AppDialogs.showDeleteConfirmationDialog(context, 'this $itemType');
|
|
377
316
|
|
|
378
317
|
if (confirm == true) {
|
|
379
|
-
final success =
|
|
318
|
+
final success = isTrade
|
|
319
|
+
? await viewModel.deleteTrade(itemId)
|
|
320
|
+
: await viewModel.deleteDividend(itemId);
|
|
321
|
+
|
|
380
322
|
if (ScaffoldMessenger.of(context).mounted) {
|
|
381
323
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
382
324
|
SnackBar(
|
|
383
325
|
content: Text(success
|
|
384
|
-
? '
|
|
385
|
-
: 'Failed to delete
|
|
326
|
+
? 'The $itemType was deleted successfully.'
|
|
327
|
+
: 'Failed to delete the $itemType.'),
|
|
386
328
|
),
|
|
387
329
|
);
|
|
388
330
|
}
|
|
@@ -96,7 +96,7 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
final summary = viewModel.sinteticoSummary.first;
|
|
99
|
-
final double unrealizedPL = summary['unrealized_pl'];
|
|
99
|
+
final double unrealizedPL = double.tryParse(summary['unrealized_pl']?.toString() ?? '0.0') ?? 0.0;
|
|
100
100
|
final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
|
|
101
101
|
|
|
102
102
|
return DataTable(
|
|
@@ -113,22 +113,22 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
113
113
|
DataRow(
|
|
114
114
|
cells: [
|
|
115
115
|
DataCell(
|
|
116
|
-
Text(_formatCurrency(summary['user_contribution']
|
|
116
|
+
Text(_formatCurrency(double.tryParse(summary['user_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
117
117
|
DataCell(Text(
|
|
118
|
-
_formatCurrency(summary['company_contribution']
|
|
118
|
+
_formatCurrency(double.tryParse(summary['company_contribution']?.toString() ?? '0.0') ?? 0.0))),
|
|
119
119
|
DataCell(
|
|
120
|
-
Text(_formatCurrency(summary['total_contributed']
|
|
120
|
+
Text(_formatCurrency(double.tryParse(summary['total_contributed']?.toString() ?? '0.0') ?? 0.0))),
|
|
121
121
|
DataCell(Text(_formatCurrency(unrealizedPL),
|
|
122
122
|
style: TextStyle(color: plColor))),
|
|
123
123
|
DataCell(
|
|
124
|
-
Text('${(summary['percent_return']
|
|
124
|
+
Text('${(double.tryParse(summary['percent_return']?.toString() ?? '0.0') ?? 0.0).toStringAsFixed(2)}%',
|
|
125
125
|
style: TextStyle(color: plColor)),
|
|
126
126
|
),
|
|
127
127
|
DataCell(
|
|
128
|
-
Text(_formatCurrency(summary['market_value']
|
|
128
|
+
Text(_formatCurrency(double.tryParse(summary['market_value']?.toString() ?? '0.0') ?? 0.0))),
|
|
129
129
|
DataCell(
|
|
130
130
|
Text(
|
|
131
|
-
'${(summary['portfolio_allocation_percent']
|
|
131
|
+
'${(double.tryParse(summary['portfolio_allocation_percent']?.toString() ?? '0.0') ?? 0.0).toStringAsFixed(2)}%'),
|
|
132
132
|
),
|
|
133
133
|
],
|
|
134
134
|
),
|
|
@@ -161,12 +161,11 @@ class RrspSunLifeScreen extends StatelessWidget {
|
|
|
161
161
|
],
|
|
162
162
|
rows: viewModel.contributions.map((c) {
|
|
163
163
|
final contributionId = c['id'] as int;
|
|
164
|
-
final rrspAmount = double.
|
|
165
|
-
final dpspAmount = double.
|
|
164
|
+
final rrspAmount = double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
165
|
+
final dpspAmount = double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
166
166
|
final totalContributed = rrspAmount + dpspAmount;
|
|
167
167
|
|
|
168
|
-
final returnAmount =
|
|
169
|
-
double.parse(c['return_amount']?.toString() ?? '0.0');
|
|
168
|
+
final returnAmount = double.tryParse(c['return_amount']?.toString() ?? '0.0') ?? 0.0;
|
|
170
169
|
final percentReturn = totalContributed > 0
|
|
171
170
|
? (returnAmount / totalContributed) * 100
|
|
172
171
|
: 0.0;
|
|
@@ -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;
|