@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 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.
@@ -1,59 +1,137 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:intl/intl.dart';
3
- import 'package:personal_finance_frontend_core_services/services/stock_service.dart';
4
- import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
5
- import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
6
- import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
7
- import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
8
3
  import 'package:provider/provider.dart';
9
4
  import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
10
5
  import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
6
+ import 'package:personal_finance_frontend_core_ui/widgets/trade_form.dart';
7
+ import 'package:personal_finance_frontend_core_ui/widgets/dividend_log_form.dart';
8
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
9
+ import '../viewmodels/investment_account_viewmodel.dart';
11
10
 
12
- class InvestmentAccountScreen extends StatefulWidget {
11
+ class InvestmentAccountScreen extends StatelessWidget {
13
12
  final String accountName;
14
13
  final bool showDividends;
15
14
 
16
- const InvestmentAccountScreen(
17
- {Key? key, required this.accountName, this.showDividends = true})
18
- : super(key: key);
15
+ const InvestmentAccountScreen({
16
+ Key? key,
17
+ required this.accountName,
18
+ this.showDividends = true,
19
+ }) : super(key: key);
19
20
 
20
21
  @override
21
- _InvestmentAccountScreenState createState() =>
22
- _InvestmentAccountScreenState();
23
- }
24
-
25
- class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
26
- final TransactionService _transactionService = TransactionService();
27
- final StockService _stockService = StockService();
28
-
29
- List<Map<String, dynamic>> _trades = [];
30
- List<Map<String, dynamic>> _dividends = [];
31
- List<Map<String, dynamic>> _assets = [];
32
- List<Map<String, dynamic>> _portfolioSummary = [];
33
- Map<String, double> _livePrices = {};
34
- double _cashBalance = 0.0;
35
- double _accountTotalValue = 0.0;
36
- double _totalPortfolioBookCost = 0.0;
37
- bool _isLoading = true;
38
- bool _isFetchingPrices = false;
39
- String? _selectedSymbolForFilter;
40
- String? _token;
22
+ Widget build(BuildContext context) {
23
+ final authProvider = Provider.of<AuthProvider>(context, listen: false);
41
24
 
42
- @override
43
- void initState() {
44
- super.initState();
45
- WidgetsBinding.instance.addPostFrameCallback((_) {
46
- final authProvider = Provider.of<AuthProvider>(context, listen: false);
47
- setState(() {
48
- _token = authProvider.token;
49
- });
50
- _fetchData();
51
- });
25
+ return ChangeNotifierProvider(
26
+ create: (_) => InvestmentAccountViewModel(
27
+ accountName: accountName,
28
+ showDividends: showDividends,
29
+ token: authProvider.token,
30
+ ),
31
+ child: Consumer<InvestmentAccountViewModel>(
32
+ builder: (context, viewModel, child) {
33
+ return Scaffold(
34
+ appBar: AppBar(
35
+ title: Text('$accountName Portfolio'),
36
+ actions: [
37
+ Center(
38
+ child: Padding(
39
+ padding: const EdgeInsets.only(right: 16.0),
40
+ child: Column(
41
+ mainAxisAlignment: MainAxisAlignment.center,
42
+ crossAxisAlignment: CrossAxisAlignment.end,
43
+ children: [
44
+ Text(
45
+ 'Total Portfolio: ${_formatCurrency(viewModel.accountTotalValue)}',
46
+ style: const TextStyle(
47
+ fontSize: 16, fontWeight: FontWeight.bold),
48
+ ),
49
+ Text(
50
+ 'Account Balance: ${_formatCurrency(viewModel.cashBalance)}',
51
+ style: const TextStyle(fontSize: 12),
52
+ ),
53
+ ],
54
+ ),
55
+ ),
56
+ ),
57
+ IconButton(
58
+ icon: const Icon(Icons.refresh),
59
+ onPressed: viewModel.fetchData,
60
+ tooltip: 'Refresh Data',
61
+ ),
62
+ ],
63
+ ),
64
+ body: viewModel.isLoading
65
+ ? const Center(child: CircularProgressIndicator())
66
+ : RefreshIndicator(
67
+ onRefresh: viewModel.fetchData,
68
+ child: SingleChildScrollView(
69
+ physics: const AlwaysScrollableScrollPhysics(),
70
+ padding: const EdgeInsets.all(16.0),
71
+ child: Column(
72
+ crossAxisAlignment: CrossAxisAlignment.stretch,
73
+ children: [
74
+ Row(
75
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
76
+ children: [
77
+ Text('Portfolio Summary (Sintético)',
78
+ style:
79
+ Theme.of(context).textTheme.titleLarge),
80
+ if (viewModel.isFetchingPrices)
81
+ const SizedBox(
82
+ height: 20,
83
+ width: 20,
84
+ child: CircularProgressIndicator(
85
+ strokeWidth: 2.0),
86
+ ),
87
+ ],
88
+ ),
89
+ _buildSinteticoTable(context, viewModel),
90
+ const SizedBox(height: 24),
91
+ Row(
92
+ crossAxisAlignment: CrossAxisAlignment.start,
93
+ children: [
94
+ Expanded(
95
+ child: TradeForm(
96
+ accountName: accountName,
97
+ portfolioSummary: viewModel.portfolioSummary,
98
+ assets: viewModel.assets,
99
+ onTradeCreated: (_) => viewModel.fetchData(),
100
+ token: viewModel.token,
101
+ ),
102
+ ),
103
+ if (showDividends) ...[
104
+ const SizedBox(width: 16),
105
+ Expanded(
106
+ child: DividendLogForm(
107
+ investmentAccount: accountName,
108
+ assets: viewModel.assets,
109
+ onDividendLogged: viewModel.fetchData,
110
+ token: viewModel.token,
111
+ ),
112
+ ),
113
+ ],
114
+ ],
115
+ ),
116
+ const SizedBox(height: 24),
117
+ Text('Account History',
118
+ style: Theme.of(context).textTheme.titleLarge),
119
+ const SizedBox(height: 8),
120
+ _buildAnaliticoFilter(context, viewModel),
121
+ const SizedBox(height: 8),
122
+ _buildHistoryTable(context, viewModel),
123
+ ],
124
+ ),
125
+ ),
126
+ ),
127
+ );
128
+ },
129
+ ),
130
+ );
52
131
  }
53
132
 
54
- String currencySymbol = r'$';
55
-
56
133
  String _formatCurrency(double value) {
134
+ String currencySymbol = r'$';
57
135
  return '$currencySymbol${value.toStringAsFixed(2)}';
58
136
  }
59
137
 
@@ -71,294 +149,16 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
71
149
  return parts.join(' ');
72
150
  }
73
151
 
74
- Future<void> _fetchData() async {
75
- if (_token == null) return;
76
- setState(() => _isLoading = true);
77
- try {
78
- final futures = <Future>[
79
- _transactionService.getTrades(
80
- investmentAccount: widget.accountName, token: _token),
81
- _transactionService.getAccountBalance(widget.accountName,
82
- token: _token),
83
- _transactionService.getAssets(
84
- investmentAccount: widget.accountName, token: _token),
85
- _transactionService.getTotalPortfolioBookCost(token: _token),
86
- ];
87
- if (widget.showDividends) {
88
- futures.add(_transactionService.getDividends(
89
- investmentAccount: widget.accountName, token: _token));
90
- }
91
- final results = await Future.wait(futures);
92
-
93
- final trades = results[0] as List<Map<String, dynamic>>;
94
- final balance = results[1] as double;
95
- final assets = results[2] as List<Map<String, dynamic>>;
96
- final totalPortfolioBookCost = results[3] as double;
97
- final dividends = widget.showDividends
98
- ? results[4] as List<Map<String, dynamic>>
99
- : <Map<String, dynamic>>[];
100
-
101
- final summaryData = _calculatePortfolioSummary(trades, dividends);
102
-
103
- setState(() {
104
- _trades = trades;
105
- _dividends = dividends;
106
- _cashBalance = balance;
107
- _assets = assets;
108
- _portfolioSummary = summaryData['summary'];
109
- _accountTotalValue = summaryData['total_value'] + _cashBalance;
110
- _totalPortfolioBookCost = totalPortfolioBookCost;
111
- });
112
-
113
- if (mounted) await _fetchLivePrices();
114
- } catch (e) {
115
- if (mounted)
116
- ScaffoldMessenger.of(context)
117
- .showSnackBar(SnackBar(content: Text('Error fetching data: $e')));
118
- } finally {
119
- if (mounted) setState(() => _isLoading = false);
120
- }
121
- }
122
-
123
- Future<void> _fetchLivePrices() async {
124
- if (_portfolioSummary.isEmpty || !mounted) return;
125
- setState(() => _isFetchingPrices = true);
126
-
127
- final symbolsToFetch = _portfolioSummary
152
+ Widget _buildAnaliticoFilter(
153
+ BuildContext context, InvestmentAccountViewModel viewModel) {
154
+ final symbols = viewModel.portfolioSummary
128
155
  .map((p) => p['symbol'] as String)
129
- .where((s) => s.isNotEmpty)
156
+ .toSet()
130
157
  .toList();
131
-
132
- if (symbolsToFetch.isEmpty) {
133
- setState(() => _isFetchingPrices = false);
134
- return;
135
- }
136
-
137
- final livePrices = await _stockService.getLivePrices(symbolsToFetch);
138
-
139
- if (!mounted) return;
140
-
141
- double newTotalValue = _cashBalance;
142
- for (final position in _portfolioSummary) {
143
- final shares = double.parse(position['shares'].toString());
144
- final livePrice = livePrices[position['symbol']];
145
- if (livePrice != null) {
146
- newTotalValue += shares * livePrice;
147
- } else {
148
- // If live price is not available, use book cost as a fallback for market value
149
- newTotalValue += (position['book_cost'] as num?)?.toDouble() ?? 0.0;
150
- }
151
- }
152
-
153
- setState(() {
154
- _livePrices = livePrices;
155
- _accountTotalValue = newTotalValue;
156
- _isFetchingPrices = false;
157
- });
158
- }
159
-
160
- Map<String, dynamic> _calculatePortfolioSummary(
161
- List<Map<String, dynamic>> trades, List<Map<String, dynamic>> dividends) {
162
- Map<String, dynamic> summary = {};
163
- double accountTotalBookCost = 0;
164
-
165
- Map<String, double> dividendSummary = {};
166
- if (widget.showDividends) {
167
- for (var dividend in dividends) {
168
- final asset = dividend['asset'];
169
- if (asset != null && asset['symbol'] != null) {
170
- String symbol = asset['symbol'];
171
- double amount = double.parse(dividend['amount'].toString());
172
- dividendSummary.update(symbol, (value) => value + amount,
173
- ifAbsent: () => amount);
174
- }
175
- }
176
- }
177
-
178
- for (var trade in trades) {
179
- final asset = trade['asset'];
180
- if (asset == null || asset['symbol'] == null) continue;
181
- String symbol = asset['symbol'];
182
-
183
- double shares = double.parse(trade['shares'].toString());
184
- double price = double.parse(trade['price'].toString());
185
- String tradeType = trade['trade_type'];
186
- DateTime tradeDate = DateTime.parse(trade['date']);
187
-
188
- if (!summary.containsKey(symbol)) {
189
- summary[symbol] = {
190
- 'symbol': symbol,
191
- 'name': asset['name'] ?? symbol,
192
- 'industry': asset['industry'] ?? 'N/A',
193
- 'shares': 0.0,
194
- 'book_cost': 0.0, // Renamed from total_cost for clarity
195
- 'total_dividends': dividendSummary[symbol] ?? 0.0,
196
- 'first_trade_date': tradeDate,
197
- 'last_activity_date': tradeDate,
198
- };
199
- } else {
200
- if (tradeDate.isBefore(summary[symbol]['first_trade_date'])) {
201
- summary[symbol]['first_trade_date'] = tradeDate;
202
- }
203
- if (tradeDate.isAfter(summary[symbol]['last_activity_date'])) {
204
- summary[symbol]['last_activity_date'] = tradeDate;
205
- }
206
- }
207
-
208
- if (tradeType == 'buy') {
209
- summary[symbol]['shares'] += shares;
210
- summary[symbol]['book_cost'] += shares * price;
211
- } else if (tradeType == 'sell') {
212
- double originalShares = summary[symbol]['shares'];
213
- if (originalShares > 0) {
214
- double avgPrice = summary[symbol]['book_cost'] / originalShares;
215
- summary[symbol]['book_cost'] -= shares * avgPrice;
216
- }
217
- summary[symbol]['shares'] -= shares;
218
- }
219
- }
220
-
221
- summary.removeWhere((key, value) => value['shares'] < 0.01);
222
-
223
- accountTotalBookCost =
224
- summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
225
-
226
- List<Map<String, dynamic>> result = [];
227
- summary.forEach((symbol, data) {
228
- double shares = data['shares'];
229
- double bookCost = data['book_cost'];
230
- double totalDividends = data['total_dividends'];
231
-
232
- data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
233
- data['account_allocation_percent'] = (accountTotalBookCost > 0)
234
- ? (bookCost / accountTotalBookCost) * 100
235
- : 0.0;
236
- data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0)
237
- ? (bookCost / _totalPortfolioBookCost) * 100
238
- : 0.0;
239
-
240
- // Calculate Market Value, Unrealized P/L, Total Return
241
- final livePrice = _livePrices[symbol];
242
- double marketValue = livePrice != null
243
- ? shares * livePrice
244
- : bookCost; // Fallback to bookCost if no live price
245
- double unrealizedPL = marketValue - bookCost;
246
- double totalReturn = unrealizedPL + totalDividends;
247
-
248
- data['market_value'] = marketValue;
249
- data['unrealized_pl'] = unrealizedPL;
250
- data['total_return'] = totalReturn;
251
-
252
- data['percent_unrealized_pl'] =
253
- (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
254
- data['percent_total_return'] =
255
- (bookCost > 0) ? (totalReturn / bookCost) * 100 : 0.0;
256
-
257
- result.add(data);
258
- });
259
-
260
- return {'summary': result, 'total_value': accountTotalBookCost};
261
- }
262
-
263
- @override
264
- Widget build(BuildContext context) {
265
- return Scaffold(
266
- appBar: AppBar(
267
- title: Text('${widget.accountName} Portfolio'),
268
- actions: [
269
- Center(
270
- child: Padding(
271
- padding: const EdgeInsets.only(right: 16.0),
272
- child: Column(
273
- mainAxisAlignment: MainAxisAlignment.center,
274
- crossAxisAlignment: CrossAxisAlignment.end,
275
- children: [
276
- Text(
277
- 'Total Portfolio: ${_formatCurrency(_accountTotalValue)}',
278
- style: const TextStyle(
279
- fontSize: 16, fontWeight: FontWeight.bold)),
280
- Text('Account Balance: ${_formatCurrency(_cashBalance)}',
281
- style: const TextStyle(fontSize: 12)),
282
- ],
283
- ),
284
- ),
285
- ),
286
- IconButton(
287
- icon: const Icon(Icons.refresh),
288
- onPressed: _fetchData,
289
- tooltip: 'Refresh Data'),
290
- ],
291
- ),
292
- body: _isLoading
293
- ? const Center(child: CircularProgressIndicator())
294
- : RefreshIndicator(
295
- onRefresh: _fetchData,
296
- child: SingleChildScrollView(
297
- physics: const AlwaysScrollableScrollPhysics(),
298
- padding: const EdgeInsets.all(16.0),
299
- child: Column(
300
- crossAxisAlignment: CrossAxisAlignment.stretch,
301
- children: [
302
- Row(
303
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
304
- children: [
305
- Text('Portfolio Summary (Sintético)',
306
- style: Theme.of(context).textTheme.titleLarge),
307
- if (_isFetchingPrices)
308
- const SizedBox(
309
- height: 20,
310
- width: 20,
311
- child:
312
- CircularProgressIndicator(strokeWidth: 2.0)),
313
- ],
314
- ),
315
- _buildSinteticoTable(),
316
- const SizedBox(height: 24),
317
- Row(
318
- crossAxisAlignment: CrossAxisAlignment.start,
319
- children: [
320
- Expanded(
321
- child: TradeForm(
322
- accountName: widget.accountName,
323
- portfolioSummary: _portfolioSummary,
324
- assets: _assets,
325
- onTradeCreated: (_) => _fetchData(),
326
- token: _token,
327
- )),
328
- if (widget.showDividends) ...[
329
- const SizedBox(width: 16),
330
- Expanded(
331
- child: DividendLogForm(
332
- investmentAccount: widget.accountName,
333
- assets: _assets,
334
- onDividendLogged: () => _fetchData(),
335
- token: _token,
336
- ),
337
- ),
338
- ],
339
- ],
340
- ),
341
- const SizedBox(height: 24),
342
- Text('Trade History (Analítico)',
343
- style: Theme.of(context).textTheme.titleLarge),
344
- const SizedBox(height: 8),
345
- _buildAnaliticoFilter(),
346
- const SizedBox(height: 8),
347
- _buildAnaliticoTable(),
348
- ],
349
- ),
350
- ),
351
- ),
352
- );
353
- }
354
-
355
- Widget _buildAnaliticoFilter() {
356
- final symbols =
357
- _portfolioSummary.map((p) => p['symbol'] as String).toSet().toList();
358
158
  return Padding(
359
159
  padding: const EdgeInsets.only(bottom: 8.0),
360
160
  child: AppDropdown<String?>(
361
- value: _selectedSymbolForFilter,
161
+ value: viewModel.selectedSymbolForFilter,
362
162
  hint: 'Filter by Symbol',
363
163
  items: [
364
164
  const DropdownMenuItem<String?>(
@@ -366,18 +166,21 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
366
166
  ...symbols.map((symbol) =>
367
167
  DropdownMenuItem<String?>(value: symbol, child: Text(symbol))),
368
168
  ],
369
- onChanged: (String? newValue) =>
370
- setState(() => _selectedSymbolForFilter = newValue),
169
+ onChanged: (String? newValue) => viewModel.setSymbolFilter(newValue),
371
170
  ),
372
171
  );
373
172
  }
374
173
 
375
- Widget _buildSinteticoTable() {
376
- if (_portfolioSummary.isEmpty)
174
+ Widget _buildSinteticoTable(
175
+ BuildContext context, InvestmentAccountViewModel viewModel) {
176
+ if (viewModel.portfolioSummary.isEmpty) {
377
177
  return const Center(
378
- child: Padding(
379
- padding: EdgeInsets.all(16.0),
380
- child: Text('No positions held.')));
178
+ child: Padding(
179
+ padding: EdgeInsets.all(16.0),
180
+ child: Text('No positions held.'),
181
+ ),
182
+ );
183
+ }
381
184
 
382
185
  return SingleChildScrollView(
383
186
  scrollDirection: Axis.horizontal,
@@ -392,35 +195,22 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
392
195
  const DataColumn(label: Text('Market Value')),
393
196
  const DataColumn(label: Text('Unrealized P/L')),
394
197
  const DataColumn(label: Text('% P/L')),
395
- if (widget.showDividends) ...[
396
- const DataColumn(label: Text('Dividends')),
397
- const DataColumn(label: Text('Total Return')),
398
- const DataColumn(label: Text('% Total Return')),
399
- ],
400
198
  const DataColumn(label: Text('Account %')),
401
199
  const DataColumn(label: Text('Portfolio %')),
402
200
  ],
403
- rows: _portfolioSummary.map((position) {
201
+ rows: viewModel.portfolioSummary.map((position) {
404
202
  final symbol = position['symbol'] as String;
405
- final shares = position['shares'] as double;
406
- final avgPrice = position['avg_price'] as double;
407
- final bookCost = position['book_cost'] as double;
408
- final livePrice = _livePrices[symbol];
409
- final marketValue = position['market_value'] as double;
410
- final unrealizedPL = position['unrealized_pl'] as double;
411
- final percentUnrealizedPL =
412
- position['percent_unrealized_pl'] as double;
413
- final totalDividends = position['total_dividends'] as double;
414
- final totalReturn = position['total_return'] as double;
415
- final percentTotalReturn = position['percent_total_return'] as double;
416
- final accountAllocationPercent =
417
- position['account_allocation_percent'] as double;
418
- final portfolioAllocationPercent =
419
- position['portfolio_allocation_percent'] as double;
420
-
421
- final plColor = (unrealizedPL ?? 0) >= 0 ? Colors.green : Colors.red;
422
- final totalReturnColor =
423
- (totalReturn ?? 0) >= 0 ? Colors.green : Colors.red;
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;
206
+ final livePrice = viewModel.livePrices[symbol];
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;
212
+
213
+ final plColor = unrealizedPL >= 0 ? Colors.green : Colors.red;
424
214
 
425
215
  return DataRow(cells: [
426
216
  DataCell(Text(symbol)),
@@ -431,17 +221,12 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
431
221
  ? Text(_formatCurrency(livePrice))
432
222
  : const Text('N/A')),
433
223
  DataCell(Text(_formatCurrency(marketValue))),
434
- DataCell(Text(_formatCurrency(unrealizedPL),
435
- style: TextStyle(color: plColor))),
224
+ DataCell(
225
+ Text(_formatCurrency(unrealizedPL),
226
+ style: TextStyle(color: plColor))),
436
227
  DataCell(Text('${percentUnrealizedPL.toStringAsFixed(2)}%',
437
228
  style: TextStyle(color: plColor))),
438
- if (widget.showDividends) ...[
439
- DataCell(Text(_formatCurrency(totalDividends))),
440
- DataCell(Text(_formatCurrency(totalReturn),
441
- style: TextStyle(color: totalReturnColor))),
442
- DataCell(Text('${percentTotalReturn.toStringAsFixed(2)}%',
443
- style: TextStyle(color: totalReturnColor))),
444
- ],
229
+
445
230
  DataCell(Text('${accountAllocationPercent.toStringAsFixed(2)}%')),
446
231
  DataCell(Text('${portfolioAllocationPercent.toStringAsFixed(2)}%')),
447
232
  ]);
@@ -450,38 +235,33 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
450
235
  );
451
236
  }
452
237
 
453
- Widget _buildAnaliticoTable() {
454
- if (_trades.isEmpty)
238
+ Widget _buildHistoryTable(
239
+ BuildContext context, InvestmentAccountViewModel viewModel) {
240
+ if (viewModel.unifiedHistory.isEmpty) {
455
241
  return const Center(
456
- child: Padding(
457
- padding: EdgeInsets.all(16.0), child: Text('No trades found.')));
242
+ child: Padding(
243
+ padding: EdgeInsets.all(16.0),
244
+ child: Text('No history found.'),
245
+ ),
246
+ );
247
+ }
458
248
 
459
- final List<Map<String, dynamic>> filteredTrades =
460
- _selectedSymbolForFilter == null
461
- ? _trades
462
- : _trades
463
- .where((trade) =>
464
- trade['asset']?['symbol'] == _selectedSymbolForFilter)
249
+ final List<Map<String, dynamic>> filteredHistory =
250
+ viewModel.selectedSymbolForFilter == null
251
+ ? viewModel.unifiedHistory
252
+ : viewModel.unifiedHistory
253
+ .where((item) =>
254
+ item['symbol'] == viewModel.selectedSymbolForFilter)
465
255
  .toList();
466
256
 
467
- if (filteredTrades.isEmpty)
257
+ if (filteredHistory.isEmpty) {
468
258
  return const Center(
469
- child: Padding(
470
- padding: EdgeInsets.all(16.0),
471
- child: Text('No trades match the selected symbol.')));
472
-
473
- final sortedTrades = List<Map<String, dynamic>>.from(filteredTrades)
474
- ..sort((a, b) {
475
- final idA = int.tryParse(a['id']?.toString() ?? '');
476
- final idB = int.tryParse(b['id']?.toString() ?? '');
477
- if (idA != null && idB != null) return idB.compareTo(idA);
478
- try {
479
- return DateTime.parse(b['date'] as String)
480
- .compareTo(DateTime.parse(a['date'] as String));
481
- } catch (e) {
482
- return 0;
483
- }
484
- });
259
+ child: Padding(
260
+ padding: EdgeInsets.all(16.0),
261
+ child: Text('No history matches the selected symbol.'),
262
+ ),
263
+ );
264
+ }
485
265
 
486
266
  return DataTable(
487
267
  columns: const [
@@ -491,64 +271,36 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
491
271
  DataColumn(label: Text('Type')),
492
272
  DataColumn(label: Text('Shares')),
493
273
  DataColumn(label: Text('Price')),
494
- DataColumn(label: Text('Live')),
495
274
  DataColumn(label: Text('Total')),
496
- DataColumn(label: Text('Return')),
497
- DataColumn(label: Text('% Return')),
498
- DataColumn(label: Text('Book Cost')),
499
- DataColumn(label: Text('Actions')), // New column for actions
275
+ DataColumn(label: Text('Actions')),
500
276
  ],
501
- rows: sortedTrades.map((trade) {
502
- final double shares = double.parse(trade['shares'].toString());
503
- final double price = double.parse(trade['price'].toString());
504
- final double total = shares * price;
505
- final tradeDate = DateTime.parse(trade['date'] as String);
506
- final tradeAge = DateTime.now().difference(tradeDate);
507
- final symbol = trade['asset']?['symbol'] ?? '';
508
- final livePrice = _livePrices[symbol];
509
- final int tradeId = trade['id'] as int; // Get trade ID
510
-
511
- double? tradeReturnValue, tradePercentageReturn, tradeBookCost;
277
+ rows: filteredHistory.map((item) {
278
+ final isTrade = item['history_type'] == 'trade';
279
+ final isDividend = item['history_type'] == 'dividend';
512
280
 
513
- if (livePrice != null && shares > 0 && trade['trade_type'] == 'buy') {
514
- final currentMarketValue = livePrice * shares;
515
- tradeReturnValue = currentMarketValue - total;
516
- tradeBookCost = currentMarketValue;
517
- if (total > 0)
518
- tradePercentageReturn = (tradeReturnValue / total) * 100;
519
- }
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;
520
286
 
521
- final returnColor =
522
- (tradeReturnValue ?? 0) >= 0 ? Colors.green : Colors.red;
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);
523
290
 
524
291
  return DataRow(cells: [
525
292
  DataCell(Text(DateFormat('yyyy-MM-dd').format(tradeDate))),
526
293
  DataCell(Text(_formatDuration(tradeAge))),
527
294
  DataCell(Text(symbol)),
528
- DataCell(Text(trade['trade_type'] ?? 'N/A')),
529
- DataCell(Text(shares.toString())),
530
- DataCell(Text(_formatCurrency(price))),
531
- DataCell(livePrice != null
532
- ? Text(_formatCurrency(livePrice))
533
- : 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('-')),
534
298
  DataCell(Text(_formatCurrency(total))),
535
- DataCell(tradeReturnValue != null
536
- ? Text(_formatCurrency(tradeReturnValue),
537
- style: TextStyle(color: returnColor))
538
- : const Text('N/A')),
539
- DataCell(tradePercentageReturn != null
540
- ? Text('${tradePercentageReturn.toStringAsFixed(2)}%',
541
- style: TextStyle(color: returnColor))
542
- : const Text('N/A')),
543
- DataCell(tradeBookCost != null
544
- ? Text(_formatCurrency(tradeBookCost),
545
- style: TextStyle(color: returnColor))
546
- : const Text('N/A')),
547
299
  DataCell(
548
300
  IconButton(
549
301
  icon: const Icon(Icons.delete, color: Colors.red),
550
- onPressed: () => _confirmAndDeleteTrade(tradeId),
551
- tooltip: 'Delete Trade',
302
+ onPressed: () => _confirmAndDeleteItem(context, viewModel, id, isTrade),
303
+ tooltip: 'Delete Item',
552
304
  ),
553
305
  ),
554
306
  ]);
@@ -556,33 +308,24 @@ class _InvestmentAccountScreenState extends State<InvestmentAccountScreen> {
556
308
  );
557
309
  }
558
310
 
559
- Future<void> _confirmAndDeleteTrade(int tradeId) async {
311
+ Future<void> _confirmAndDeleteItem(BuildContext context,
312
+ InvestmentAccountViewModel viewModel, int itemId, bool isTrade) async {
313
+ final itemType = isTrade ? 'trade' : 'dividend';
560
314
  final bool? confirm =
561
- await AppDialogs.showDeleteConfirmationDialog(context, 'this trade');
315
+ await AppDialogs.showDeleteConfirmationDialog(context, 'this $itemType');
562
316
 
563
317
  if (confirm == true) {
564
- if (_token == null) {
318
+ final success = isTrade
319
+ ? await viewModel.deleteTrade(itemId)
320
+ : await viewModel.deleteDividend(itemId);
321
+
322
+ if (ScaffoldMessenger.of(context).mounted) {
565
323
  ScaffoldMessenger.of(context).showSnackBar(
566
- const SnackBar(content: Text('Authentication token not available.')),
567
- );
568
- return;
569
- }
570
- try {
571
- final success =
572
- await _transactionService.deleteTrade(tradeId, token: _token);
573
- if (success) {
574
- ScaffoldMessenger.of(context).showSnackBar(
575
- const SnackBar(content: Text('Trade deleted successfully.')),
576
- );
577
- _fetchData(); // Refresh data after deletion
578
- } else {
579
- ScaffoldMessenger.of(context).showSnackBar(
580
- const SnackBar(content: Text('Failed to delete trade.')),
581
- );
582
- }
583
- } catch (e) {
584
- ScaffoldMessenger.of(context).showSnackBar(
585
- SnackBar(content: Text('Error deleting trade: $e')),
324
+ SnackBar(
325
+ content: Text(success
326
+ ? 'The $itemType was deleted successfully.'
327
+ : 'Failed to delete the $itemType.'),
328
+ ),
586
329
  );
587
330
  }
588
331
  }